// ==UserScript== // @name zhihu optimizer // @namespace https://github.com/Kyouichirou // @version 2.5.2.4 // @description make zhihu clean and tidy, for better experience // @author HLA // @run-at document-start // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @grant GM_unregisterMenuCommand // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_registerMenuCommand // @grant GM_notification // @match https://*.zhihu.com/* // @compatible chrome 80+; test on chrome 64(x86), some features don't work // @license MIT // @noframes // @note more spam users of zhihu, https://zhuanlan.zhihu.com/p/127021293, it is recommended to block all of these users. /* !*******************zhihu*******************************! www.zhihu.com/api/v4/creator/read_count_statistics* www.zhihu.com/api/v4/me?include=ad_type* www.zhihu.com/api/v4/search/top_search www.zhihu.com/zbst/events/r www.zhihu.com/api/v4/me/switches?include=is_creator www.zhihu.com/api/v4/commercial/ecommerce www.zhihu.com/api/v4/search/preset_words !*******************zhihu*******************************!*/ //note add these rules to ublock or adblock => ensure the input box clear // @downloadURL none // ==/UserScript== (() => { "use strict"; let blackName = null; const blackKey = ["留学中介", "肖战"]; const mergeArray = (origin, target) => { origin = origin.concat(target); const newArr = []; const tmpObj = {}; for (const e of origin) { if (!tmpObj[e]) { newArr.push(e); tmpObj[e] = 1; } } return newArr; }; const getSelection = () => { const select = window.getSelection(); return select ? select.toString().trim() : null; }; const Notification = (content = "", title = "", duration = 2500, func) => { GM_notification({ text: content, title: title, timeout: duration, onclick: func, }); }; const zhihu = { getData() { blackName = GM_getValue("blackname"); (!blackName || !Array.isArray(blackName)) && (blackName = []); }, clipboardClear() { const cs = [ /。/g, /:/g, /;/g, /?/g, /!/g, /(/g, /)/g, /“/g, /”/g, /、/g, /,/g, ]; const es = [ ". ", ": ", "; ", "? ", "! ", "(", ")", '"', '"', ", ", ", ", ]; document.oncopy = (e) => { e.preventDefault(); e.stopImmediatePropagation(); let copytext = getSelection(); if (!copytext) return; cs.forEach((s, i) => (copytext = copytext.replace(s, es[i]))); window.navigator.clipboard.writeText(copytext); }; }, turnPage: { main(mode) { const overlap = 100; const wh = window.innerHeight; let height = wh - overlap; height < 0 && (height = 0); let top = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; if (mode) top += height; else top < height ? (top = 0) : (top -= height); window.scrollTo(0, top); }, start(mode) { //n => scroll down ; u => scroll top window.requestAnimationFrame(this.main.bind(this, mode)); }, }, multiSearch(keyCode) { const Names = { 65: "AboutMe", 68: "Douban", 71: "Google", 72: "Github", 77: "MDN", 66: "BiliBili", 90: "Zhihu", }; const methods = { Protocols: "https://", Search(url) { const select = getSelection(); if (!select || select.length > 75) return; url += encodeURIComponent(select); window.open(this.Protocols + url, "_blank"); }, Google() { this.Search("www.dogedoge.com/results?q="); }, Douban() { this.Search("www.douban.com/search?q="); }, Zhihu() { this.Search("www.zhihu.com/search?q"); }, MDN() { this.Search("developer.mozilla.org/zh-CN/search?q="); }, Github() { this.Search("github.com/search?q="); }, BiliBili() { this.Search("search.bilibili.com/all?keyword="); }, AboutMe() { zhihu.shade.Support.main(); }, }; const name = Names[keyCode]; name && methods[name](); }, noteHightlight: { editable: null, EditDoc() { const m = document.body.contentEditable === "true" ? "inherit" : "true"; document.body.contentEditable = m; this.editable = m; const t = m === "true" ? "page editable mode" : "exit editable mode"; Notification(t, "Editable"); }, get Selection() { return window.getSelection(); }, setMark(color, text) { return `${text}`; }, get createElement() { return document.createElement("markspan"); }, appendNewNode(node, color) { const text = node.nodeValue; const span = this.createElement; node.parentNode.replaceChild(span, node); span.outerHTML = this.setMark(color, text); }, getTextNode(node, color) { node.nodeType === 3 && this.appendNewNode(node, color); }, Marker(keyCode) { const cname = { 82: "red", 89: "yellow", 80: "purple", 71: "green", }; let color = cname[keyCode]; if (!color) return; const select = this.Selection; if (!select.anchorNode || select.isCollapsed) return; const colors = { red: "rgb(255, 128, 128)", green: "rgb(170, 255, 170)", yellow: "rgb(255, 255, 170)", purple: "rgb(255, 170, 255)", }; let i = select.rangeCount; const r = select.getRangeAt(--i); let start = r.startContainer; const end = r.endContainer; const offs = r.startOffset; const offe = r.endOffset; color = colors[color]; let nodeValue = r.startContainer.nodeValue; if (start !== end) { //start part let next = start.nextSibling; let p = start.parentNode; if (p.className !== "AssistantMark") { const text = nodeValue.slice(offs); const span = this.createElement; p.replaceChild(span, start); span.outerHTML = nodeValue.slice(0, offs) + this.setMark(color, text); } //mid part while (true) { if (next) { start = next; } else { next = p.nextSibling; while (!next) { p = p.parentNode; next = p.nextSibling; } start = next; } //get the deepest level node while (start.childNodes.length > 0) start = start.childNodes[0]; if (start === end) break; p = start.parentNode; next = p.nextSibling; p.className !== "AssistantMark" && this.getTextNode(start, color); } //end part nodeValue = start.nodeValue; start = start.parentNode; if (start.className === "AssistantMark") return; const text = nodeValue.slice(0, offe); const epan = this.createElement; start.replaceChild(epan, end); epan.outerHTML = this.setMark(color, text) + nodeValue.slice(offe); } else { //all value in one node; const text = nodeValue.slice(offs, offe); const span = this.createElement; start.parentNode.replaceChild(span, start); span.outerHTML = nodeValue.slice(0, offs) + this.setMark(color, text) + nodeValue.slice(offe); } }, Restore(node) { const p = node.parentNode; if (p.className === "AssistantMark") { p.parentNode.innerHTML = p.parentNode.innerText; return true; } return false; }, removeMark() { const select = this.Selection; if (!select.anchorNode || select.isCollapsed) return; let i = select.rangeCount; const r = select.getRangeAt(--i); let start = r.startContainer; const end = r.endContainer; if (start !== end) { let t = start.nodeType; if (t !== 3 && r.collapsed) { const nodes = start.getElementsByClassName( "AssistantMark" ); let i = nodes.length; if (i > 0) { for (i; i--; ) { const p = nodes[i].parentNode; p.innerhHTML = p.innerText; } } return; } while (start.childNodes.length > 0) start = start.childNodes[0]; let p = start.parentNode.parentNode; let next = start.nextSibling; let result = this.Restore(start); //if this is mark node, will be removed, so we need get the parentnode to backup, if it is not mark node, restore the parentnode !result && (p = start.parentNode); while (true) { if (next) { start = next; } else { next = p.nextSibling; while (!next) { p = p.parentNode; next = p.nextSibling; } start = next; } while (start.childNodes.length > 0) start = start.childNodes[0]; if (start === end) break; p = start.parentNode.parentNode; next = start.nextSibling; result = this.Restore(start); !result && (p = start.parentNode); } } this.Restore(start); }, }, autoScroll: { stepTime: 40, keyCount: 1, scrollState: false, scrollTime: null, scrollPos: null, bottom: 100, pageScroll(TimeStamp) { const position = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset; if (this.scrollTime) { this.scrollPos = this.scrollPos !== null ? this.scrollPos + (TimeStamp - this.scrollTime) / this.stepTime : position; window.scrollTo(0, this.scrollPos); } this.scrollTime = TimeStamp; if (this.scrollState) { let h = document.documentElement.scrollHeight || document.body.scrollHeight; h = h - window.innerHeight - this.bottom; position < h ? window.requestAnimationFrame( this.pageScroll.bind(this) ) : this.stopScroll(); } }, stopScroll() { if (this.scrollState) { this.scrollPos = null; this.scrollTime = null; this.scrollState = false; this.keyCount = 1; } }, speedUP() { this.stepTime < 5 ? (this.stepTime = 5) : (this.stepTime -= 5); }, slowDown() { this.stepTime > 100 ? (this.stepTime = 100) : (this.stepTime += 5); }, start() { this.keyCount += 1; if (this.keyCount % 2 === 0) return; this.scrollState ? this.stopScroll() : ((this.scrollState = true), window.requestAnimationFrame(this.pageScroll.bind(this))); }, Others(keyCode, shift) { shift ? keyCode === 67 ? this.noteHightlight.removeMark() : this.noteHightlight.Marker(keyCode) : keyCode === 113 ? this.noteHightlight.EditDoc() : keyCode === 78 ? this.turnPage.start(true) : keyCode === 85 ? this.turnPage.start(false) : this.multiSearch(keyCode); }, keyBoardEvent() { window.onkeydown = (e) => { if (e.ctrlKey || e.altKey) return; const className = e.target.className; if (className && className.includes("DraftEditor")) return; const keyCode = e.keyCode; const shift = e.shiftKey; if (keyCode === 68 || (shift && keyCode === 71)) { //68, default is login shortcut of zhihu //71 + shift, default is scroll to the bottom of webpage e.preventDefault(); e.stopPropagation(); } shift ? this.Others.call(zhihu, keyCode, shift) : keyCode === 192 ? this.start() : keyCode === 187 ? this.speedUP() : keyCode === 189 ? this.slowDown() : this.Others.call(zhihu, keyCode); }; }, }, shade: { Support: { interval: 0, support: null, tips: null, opacity: null, opacityChange(opacity) { const target = document.getElementById( "screen_shade_cover" ); target && (this.opacity === null ? (this.opacity = target.style.opacity) : target.style.opacity !== opacity) && (target.style.opacity = opacity); }, creatPopup() { this.opacityChange(0); const mt = -5; const html = `
Support Me!
Make Thing Better && Simpler!
15s, this Tips will be automatically closed or can you just click
Github: Kyouichirou
`; document.documentElement.insertAdjacentHTML( "beforeend", html ); this.support = document.getElementById("support_me"); this.tips = this.support.getElementsByClassName( "timeout" )[0]; let time = 15; this.interval = setInterval(() => { time--; this.tips.innerText = `${time}s, this Tips will be automatically closed or you can just click`; time === 0 && this.remove(); }, 1000); this.support.onclick = () => setTimeout(() => this.remove(), 120); }, remove() { clearInterval(this.interval); this.opacityChange(this.opacity); this.opacity = null; this.interval = null; this.support.remove(); this.support = null; this.tips = null; }, main() { this.support ? this.remove() : this.creatPopup(); }, }, cover(color, opacity = 0.5) { const html = `
`; document.documentElement.insertAdjacentHTML("afterbegin", html); }, menu(e) { const target = document.getElementById("screen_shade_cover"); target && target.style.background !== this[e] && (target.style.background = this[e]) && arguments.length === 2 && GM_setValue("color", e); }, get opacity() { const date = new Date(); const m = date.getMonth(); const h = date.getHours(); const [start, a] = m > 9 ? [15, 0.08] : [16, 0.12]; let opacity = h > 20 ? h > 22 ? 0.6 : 0.5 : h < 8 ? 0.65 : h > start ? h === 18 ? 0.35 : h === 19 ? 0.45 : h === 20 ? 0.5 : 0.3 : 0.15; return (opacity += opacity < 0.2 ? 0 : a); }, opacityMonitor() { const opacity = GM_getValue("opacity"); const target = document.getElementById("screen_shade_cover"); target && opacity && target.style.opacity !== opacity && (target.style.opacity = opacity); }, supportID: null, SupportMenu() { this.supportID = GM_registerMenuCommand( "Support || Donation", this.Support.main.bind(this.Support), "d4" ); }, disableShade: { id: null, cmenu() { this.id = GM_registerMenuCommand( "Switch", this.func.bind(this), "s5" ); }, rmenu() { GM_unregisterMenuCommand(this.id); }, func() { const target = document.getElementById( "screen_shade_cover" ); target && (target.style.display = target.style.display === "block" ? "none" : "block"); }, }, menuID: null, Switchfunc() { const target = document.getElementById("screen_shade_cover"); let result = false; if (target) { if (arguments.length > 0 && !arguments[0]) return; target.remove(); result = true; this.disableShade.rmenu(); let i = this.menuID.length; GM_removeValueChangeListener(this.opacitylistenID); GM_removeValueChangeListener(this.colorlistenID); for (i; i--; ) GM_unregisterMenuCommand(this.menuID[i]); this.menuID = null; } else { //rebuild menu if (arguments.length > 0 && arguments[0]) return; if (this.menuID) return; GM_unregisterMenuCommand(this.switchID); GM_unregisterMenuCommand(this.supportID); this.createShade(); this.SwitchMenu(); this.SupportMenu(); } arguments.length === 0 && GM_setValue("turnoff", result); }, switchID: null, SwitchMenu() { this.switchID = GM_registerMenuCommand( "Turn(On/Off)", this.Switchfunc.bind(this), "t6" ); }, turnoffID: null, start() { !GM_getValue("turnoff") && this.createShade(); this.SwitchMenu(); this.SupportMenu(); this.turnoffID = GM_addValueChangeListener( "turnoff", (name, oldValue, newValue, remote) => { if (!remote || oldValue === newValue) return; this.Switchfunc(newValue, true); } ); }, colorlistenID: null, opacitylistenID: null, createShade() { const colors = { yellow: "rgb(247, 232, 176)", green: "rgb(202 ,232, 207)", grey: "rgb(182, 182, 182)", olive: "rgb(207, 230, 161)", }; let color = GM_getValue("color"); (color && (color = colors[color])) || (color = colors.yellow); const opacity = this.opacity; this.cover(color, opacity); const UpperCase = (e) => e.slice(0, 1).toUpperCase() + e.slice(1); this.menuID = []; for (const c of Object.entries(colors)) { const id = GM_registerMenuCommand( UpperCase(c[0]), this.menu.bind(colors, c[0], true), c[0] ); this.menuID.push(id); } //note, who is the "this" in the GM_registerMenuCommand? take care of "this", must bind (function => this) this.colorlistenID = GM_addValueChangeListener( "color", (name, oldValue, newValue, remote) => { if (!remote || oldValue === newValue) return; this.menu.call(colors, newValue); } ); GM_setValue("opacity", opacity); this.opacitylistenID = GM_addValueChangeListener( "opacity", this.opacityMonitor ); this.disableShade.cmenu(); }, }, antiRedirect() { const links = Object.getOwnPropertyDescriptors( HTMLAnchorElement.prototype ).href; Object.defineProperty(HTMLAnchorElement.prototype, "href", { ...links, get() { let href = decodeURIComponent(links.get.call(this)); href = href.split("link.zhihu.com/?target="); if (href.length > 1) { this.href = href[1]; return href[1]; } return href[0]; }, }); }, antiLogin() { /* note: the timing of the js injection is uncertain, and for some reason the injection maybe late, so that the occurrence of the event cannot be accurately captured don't use dom load event => */ let mo = new MutationObserver((events) => events.forEach((e) => e.addedNodes.forEach((node) => { if ( node.getElementsByClassName("signFlowModal") .length > 0 ) { node.style.display = "none"; setTimeout(() => { const cancel = node.getElementsByClassName( "Modal-backdrop" ); if (cancel.length === 0) { console.log("get cancel login id fail"); return; } cancel[0].click(); mo.disconnect(); mo = null; }, 0); } }) ) ); document.body ? mo.observe(document.body, { childList: true }) : (document.onreadystatechange = () => mo && mo.observe(document.body, { childList: true })); }, Filter: { checked: null, //click the ico of button svgCheck(node, targetElements) { let pnode = node.parentNode; if (pnode.className === targetElements.buttonClass) { return pnode; } else { pnode = pnode.parentNode; let className = pnode.className; let ic = 0; while (className !== targetElements.buttonClass) { pnode = pnode.parentNode; if (!node || ic > 2) return null; className = pnode.className; ic++; } return pnode; } }, getiTem(target, targetElements) { let item = target.parentNode; if (item.className === targetElements.itemClass) { return item; } else { item = item.parentNode; let ic = 0; let className = item.className; while (className !== targetElements.itemClass) { item = item.parentNode; if (!item || ic > 3) return null; className = item.className; ic++; } return item; } }, //get the url id of the answer || article getTargetID(item) { const a = item.getElementsByTagName("a"); if (a.length === 0) return null; const pathname = a[0].pathname; return pathname.slice(pathname.lastIndexOf("/") + 1); }, //checks if the part of answer is expanded checkExpand(item) { return ( item.getElementsByClassName( "RichContent is-collapsed RichContent--unescapable" ).length > 0 ); }, /* 0, normal 1, searchpage => check username 2, click => check all content 3, articel page => check expand if the item has been checked, return */ contentCheck(item, targetElements, mode) { let id = ""; if (mode === 2) { id = this.getTargetID(item); if (id && this.checked.includes(id)) return false; } const content = item.getElementsByClassName( targetElements.contentID ); if (content.length === 0) { console.log("get content fail"); return false; } const text = content[0].innerText; if (mode === 1) { const name = text.startsWith("匿名用户:") ? "" : text.slice(0, text.indexOf(":")); if (name && blackName.includes(name)) { console.log( `%cuser of ${name} has been blocked`, "color: red;" ); item.style.display = "none"; return true; } } const result = blackKey.some((e) => text.includes(e)); if (result) { console.log("%citem has been blocked", "color: red;"); item.style.display = "none"; } else if ( mode === 2 || (targetElements.index < 2 && !this.checkExpand(item)) ) { (id || (id = this.getTargetID(item))) && this.checked.push(id); } return result; }, userCheck(item, targetElements) { const user = item.getElementsByClassName(targetElements.userID); if (user.length === 0) { console.log("get user fail, anonymous user"); return false; } let i = user.length - 1; i = i > 1 ? 1 : i; /* const pathname = user[i].pathname; if (!pathname) return false; const id = pathname.slice(pathname.lastIndexOf("/") + 1); let result = blackID.includes(id); if (result) { console.log( `%cuser of ${id} has been blocked`, "color: red;" ); item.style.display = "none"; return result; } */ const name = user[i].innerText; const result = blackName.includes(name); if (result) { console.log( `%cuser of ${name} has been blocked`, "color: red;" ); item.style.display = "none"; } return result; }, check(item, targetElements, mode) { let result = false; if (targetElements.index === 3) { result = this.contentCheck(item, targetElements, 1); } else { result = this.userCheck(item, targetElements); !result && this.contentCheck(item, targetElements, mode); } }, checkURL(targetElements) { if (targetElements.index < 2) return true; const href = location.href; return targetElements.zone.some((e) => href.includes(e)); }, clickCheck(item, targetElements) { // user without userid when in the search page, if the answer is not expanded if (targetElements.index === 3) { const result = this.userCheck(item, targetElements); if (result) return; } setTimeout( () => this.contentCheck(item, targetElements, 2), 300 ); }, //check the content when the content expanded clickMonitor(node, targetElements) { node.onclick = (e) => { const target = e.target; const className = target.className; let item = null; //click the expand button if (className === targetElements.buttonClass) { item = this.getiTem(target, targetElements); //click the ico of expand button } else if (target.localName === "svg") { true; const button = this.svgCheck(target, targetElements); button && (item = this.getiTem(button, targetElements)); //click the answser, the content will be automatically expanded } else { if (className !== targetElements.expand) return; for (const node of e.path) { const className = node.className; if ( className === targetElements.itemClass || className === targetElements.answerID ) { item = node; break; } } } item && this.clickCheck(item, targetElements); }; }, topicAndquestion(targetElements, info) { const items = document.getElementsByClassName( "ContentItem-meta" ); let n = items.length; for (n; n--; ) { const item = items[n]; const a = item.getElementsByClassName("UserLink-link"); let i = a.length; if (i > 0) { const username = a[--i].innerText; if (username === info.username) { const t = this.getiTem(item, targetElements); t && this.setDisplay(t, info); } } } }, setDisplay(t, info) { if (info.mode === "block") { t.style.display !== "none" && (t.style.display = "none"); } else { t.style.display === "none" && (t.style.display = "block"); } }, userChange(index) { const info = GM_getValue("blacknamechange"); if (!info) return; const targetElements = this.getTagetElements( index === 0 ? 1 : index ); index === 0 && (targetElements.itemClass = "ContentItem AnswerItem"); if (!this.checkURL(targetElements)) return; if (index === 3) { const items = document.getElementsByClassName( targetElements.itemClass ); let n = items.length; for (n; n--; ) { debugger; const item = items[n]; const a = item.getElementsByClassName( targetElements.userID ); let i = a.length; if (i > 0) { const name = a[--i].innerText; name === info.username && this.setDisplay(item, info); } else { const content = item.getElementsByClassName( targetElements.contentID ); if (content.length === 0) continue; const text = content[0].innerText; const name = text.startsWith("匿名用户:") ? "" : text.slice(0, text.indexOf(":")); name && name === info.username && this.setDisplay(item, info); } } } else this.topicAndquestion(targetElements, info); }, monitor(targetElements) { let node = document.getElementById(targetElements.mainID); if (!node) { node = document.getElementsByClassName( targetElements.backupClass ); if (node.length === 0) { console.log("%cget main id fail", "color: red;"); return; } else { node = node[0]; } } const mo = new MutationObserver((e) => { if (!this.checkURL(targetElements)) return; e.forEach((item) => { if (item.addedNodes.length > 0) { const additem = item.addedNodes[0]; if (additem.className === targetElements.itemClass) this.check(additem, targetElements, 0); } }); }); mo.observe(node, { childList: true, subtree: true }); this.clickMonitor(node, targetElements); }, getTagetElements(index) { const pos = { 1: "questionPage", 2: "topicPage", 3: "searchPage", 0: "answerPage", }; this.checked = []; const targetElements = this[pos[index]](index); return targetElements; }, main(index) { this.checked = []; const targetElements = this.getTagetElements(index); targetElements && this.firstRun(targetElements); }, firstRun(targetElements) { if (!this.checkURL(targetElements)) { this.monitor(targetElements); return; } let ic = 0; let id = setInterval(() => { const items = document.getElementsByClassName( targetElements.itemClass ); if (items.length > 4 || ic > 10) { clearInterval(id); for (const item of items) this.check(item, targetElements, 0); this.monitor(targetElements); } ic++; }, 20); }, answerPage() { const targetElements = this.questionPage(1); const items = document.getElementsByClassName( targetElements.header ); for (const item of items) this.check(item, targetElements, 0); const node = document.getElementsByClassName( targetElements.backupClass ); this.clickMonitor(node, targetElements); const all = document.getElementsByClassName( "QuestionMainAction ViewAll-QuestionMainAction" ); for (const button of all) button.onclick = () => setTimeout(() => this.firstRun(targetElements), 300); }, questionPage(index) { const targetElements = { button: "Button ContentItem-rightButton ContentItem-expandButton Button--plain", itemClass: "List-item", mainID: "QuestionAnswers-answers", contentID: "RichText ztext CopyrightRichText-richText", userID: "UserLink-link", backupClass: "Question-main", header: "ContentItem AnswerItem", expand: "RichText ztext CopyrightRichText-richText", answerID: "ContentItem AnswerItem", index: index, }; return targetElements; }, searchPage(index) { const nocontent = document.getElementsByClassName( "SearchNoContent-title" ); if (nocontent.length > 0) return null; const targetElements = { buttonClass: "Button ContentItem-more Button--plain", itemClass: "Card SearchResult-Card", mainID: "SearchMain", contentID: "RichText ztext CopyrightRichText-richText", expand: "RichContent-inner", userID: "UserLink-link", zone: ["type=content"], index: index, }; return targetElements; }, topicPage(index) { const targetElements = { buttonClass: "Button ContentItem-more Button--plain", itemClass: "List-item TopicFeedItem", mainID: "TopicMain", userID: "UserLink-link", contentID: "RichText ztext CopyrightRichText-richText", expand: "RichContent-inner", zone: ["/top-answers", "/hot"], index: index, }; return targetElements; }, }, addStyle(index) { const common = ` span.RichText.ztext.CopyrightRichText-richText{text-align: justify !important;} body{text-shadow: #a9a9a9 0.025em 0.015em 0.02em;}`; const contentstyle = ` html{overflow: auto !important;} div.Question-mainColumn{margin: auto !important;width: 100% !important;} div.Question-sideColumn,.Kanshan-container{display: none !important;} figure{max-width: 70% !important;} .RichContent-inner{ line-height: 30px !important; margin: 40px 60px !important; padding: 40px 50px !important; border: 6px dashed rgba(133,144,166,0.2) !important; border-radius: 6px !important; } .Pc-word, .RichText-MCNLinkCardContainer{display: none !important;} .Comments{padding: 12px !important; margin: 60px !important;}`; const inpustyle = ` input::-webkit-input-placeholder { font-size: 0px !important; text-align: right; }`; const hotsearch = ".Card.TopSearch{display: none !important;}"; GM_addStyle( common + (index < 2 ? contentstyle + inpustyle : index === 3 ? inpustyle + hotsearch : inpustyle) ); }, clearStorage() { const rubbish = {}; rubbish.timeStamp = Date.now(); rubbish.words = []; //localstorage must storage this info to ensure the history show for (let i = 0; i < 5; i++) rubbish.words.push({ displayQuery: "", query: "" }); localStorage.setItem("search::top-search", JSON.stringify(rubbish)); localStorage.setItem("search:preset_words", ""); localStorage.setItem("zap:SharedSession", ""); }, inputBox: { box: null, controlEventListener() { const windowEventListener = window.addEventListener; const eventTargetEventListener = EventTarget.prototype.addEventListener; function addEventListener(type, listener, useCapture) { //take care const NewEventListener = this instanceof Window ? windowEventListener : eventTargetEventListener; //block original keyboard event to prevent blank search(ads) if ( type.startsWith("key") && !listener.toString().includes("(fuckzhihu)") ) return; Reflect.apply(NewEventListener, this, [ type, listener, useCapture, ]); //this => who lauch this function, eg, window, document, htmlelement... } window.addEventListener = EventTarget.prototype.addEventListener = addEventListener; }, monitor() { this.box = document.getElementsByTagName("input")[0]; this.box.placeholder = ""; unsafeWindow.addEventListener( "popstate", (e) => { e.preventDefault(); e.stopPropagation(); }, true ); unsafeWindow.addEventListener( "visibilitychange", (e) => { e.preventDefault(); this.box.placeholder = ""; e.stopPropagation(); }, true ); let button = document.getElementsByClassName( "Button SearchBar-searchButton Button--primary" ); if (button.length > 0) { button[0].onclick = (e) => { if (this.box.value.length === 0) { e.preventDefault(); e.stopPropagation(); } }; } button = null; this.box.addEventListener( "keydown", (fuckzhihu) => { if (fuckzhihu.keyCode !== 13) return; if ( this.box.value.length === 0 || this.box.value.trim().length === 0 ) { fuckzhihu.preventDefault(); fuckzhihu.stopImmediatePropagation(); fuckzhihu.stopPropagation(); } else { const url = `http://www.zhihu.com/search?q=${this.box.value}&type=content`; window.open(url, "_blank"); } }, true ); this.box.onfocus = () => { this.box.value.length === 0 && (this.box.placeholder = ""); localStorage.setItem("zap:SharedSession", ""); }; this.box.onblur = () => (this.box.placeholder = ""); this.firstRun(); }, firstRun() { let mo = new MutationObserver((e) => { if (e.length !== 1 || e[0].addedNodes.length !== 1) return; const target = e[0].addedNodes[0]; const p = target.getElementsByClassName("Popover-content"); if (p.length === 0) return; const tmp = p[0].getElementsByClassName( "AutoComplete-group" ); if (tmp.length === 0) return; this.AutoComplete = tmp[0]; if (p[0].innerText.startsWith("知乎热搜")) this.AutoComplete.style.display = "none"; mo.disconnect(); mo = null; this.secondRun(p[0].parentNode); }); mo.observe(document.body, { childList: true }); }, AutoComplete: null, secondRun(target) { const mo = new MutationObserver((e) => { if (e.length === 1) { if (e[0].addedNodes.length !== 1) { this.AutoComplete = null; return; } const t = e[0].addedNodes[0]; this.AutoComplete = t.getElementsByClassName( "AutoComplete-group" )[0]; if (t.innerText.startsWith("知乎热搜")) this.AutoComplete.style.display = "none"; } else { const style = this.box.value.length > 0 ? "inline" : "none"; this.AutoComplete.style.display !== style && (this.AutoComplete.style.display = style); } }); mo.observe(target, { childList: true, subtree: true }); }, }, zhuanlanStyle(mode) { //font, the pic of header, main content, sidebar, main content letter spacing, comment zone, .. const article = ` body{text-shadow: #a9a9a9 0.025em 0.015em 0.02em;} .TitleImage{width: 500px !important} .Post-Main .Post-RichText{text-align: justify !important;} .Post-SideActions{left: calc(50vw - 560px) !important;} .RichText.ztext.Post-RichText{letter-spacing: 0.1px;} .RichContent-actions .ContentItem-actions{position: inherit !important} .Comments-container, .Post-RichTextContainer{width: 900px !important;} span.LinkCard-content.LinkCard-ecommerceLoadingCard, .RichText-MCNLinkCardContainer{display: none !important}`; const list = `.Card:last-child,.css-8txec3{width: 900px !important;}`; GM_addStyle(mode ? article : list); mode && (window.onload = () => { this.colorAssistant.main(); this.autoScroll.keyBoardEvent(); }); }, colorIndicator() { let lasttarget = null; const colors = ["green", "red", "blue", "purple"]; let i = 0; let change = false; const tags = ["blockquote", "p", "br", "li"]; document.onclick = (e) => { const target = e.target; const localName = target.localName; if (!tags.includes(localName)) return; if (target.style.color) return; if (lasttarget) { lasttarget.style.color = ""; lasttarget.style.fontSize = ""; lasttarget.style.letterSpacing = ""; if (change) lasttarget.style.fontWeight = "normal"; } target.style.color = colors[i]; target.style.fontSize = "16px"; target.style.letterSpacing = "0.3px"; if (target.style.fontWeight !== 600) { target.style.fontWeight = 600; change = true; } else { change = false; } i = i > 2 ? 0 : ++i; lasttarget = target; }; }, colorAssistant: { index: 0, arr: null, blue: true, grad: 5, get rgbRed() { this.index -= this.grad; if (this.index < 0 || this.index > 255) { this.index = 0; this.blue = true; } const s = this.index > 233 ? 5 : this.index > 182 ? 4 : this.index > 120 ? 3 : this.index > 88 ? 2 : this.index > 35 ? 1 : 0; return `rgb(${this.index}, ${s}, 0)`; }, redc: false, get rgbBlue() { this.index += this.grad; if (this.index > 255) { this.blue = false; if (this.redc) { this.index = 0; this.grad *= -1; this.redc = false; } else { this.grad < 0 && (this.grad *= -1); this.index = 255; this.redc = true; } } return `rgb(0, 0, ${this.index})`; }, get textColor() { return this.blue ? this.rgbBlue : this.rgbRed; }, setColor(text, color) { return `${text}`; }, setcolorGrad(tlength) { this.grad = tlength > 500 ? 1 : tlength > 300 ? 2 : tlength > 180 ? 3 : tlength > 120 ? 4 : tlength > 80 ? 5 : 6; }, num: 0, textDetach(text) { this.setcolorGrad(text.length); const reg = /((\d+[\.-\/]\d+([\.-\/]\d+)?)|\d{2,}|[a-z]{2,})/gi; let result = null; let start = 0; let end = 0; let tmp = ""; result = reg.exec(text); const numaColors = ["green", "#8B008B"]; if (result) { while (result) { tmp = result[0]; end = reg.lastIndex - tmp.length; for (start; start < end; start++) this.arr.push( this.setColor(text[start], this.textColor) ); start = reg.lastIndex; this.arr.push(this.setColor(tmp, numaColors[this.num])); this.num = this.num ^ 1; result = reg.exec(text); } end = text.length; for (start; start < end; start++) this.arr.push( this.setColor(text[start], this.textColor) ); } else { for (let t of text) this.arr.push(this.setColor(t, this.textColor)); } }, nodeCount: 0, getItem(node) { //tags will be ignored const localName = node.localName; const tags = ["a", "br", "b", "span", "code", 'strong']; if (localName && tags.includes(localName)) { this.arr.push(node.outerHTML); this.nodeCount += 1; return; } else { const className = node.className; if (className && className === "UserLink") { this.arr.push(node.outerHTML); this.nodeCount += 1; return; } } if (node.childNodes.length === 0) { const text = node.nodeValue; text && this.textDetach(text); } else { //this is a trick, no traversal of textnode, maybe some nodes will lost content, take care for (const item of node.childNodes) this.getItem(item); this.arr.length > 0 && (node.childNodes.length - this.nodeCount < this.nodeCount + 2) && (node.innerHTML = this.arr.join("")); this.arr = []; } }, resetColor() { this.blue = !this.blue; this.index = this.blue ? 0 : 255; }, codeHightlight(node) { const keywords = [ "abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", "volatile", "while", "with", "yield", ]; const code = node.getElementsByClassName("language-text"); if (code.length === 0 || code[0].childNodes.length > 1) return; let html = code[0].innerHTML; const reg = /(["'])(.+?)(["'])/g; const keyReg = /([a-z]+(?=[\s\(]))/g; const i = html.length; const h = (match, color) => `${match}`; html = html.replace( reg, (e) => e[0] + h(e.slice(1, -1), "#FA842B") + e.slice(-1) ); html = html.replace(keyReg, (e) => { if (e && keywords.includes(e)) return h(e, "rgb(0, 0, 252)"); return e; }); const Reg = /\/\//g; html = html.replace(Reg, (e) => h(e, "green")); i !== html.length && (code[0].innerHTML = html); }, main() { let holder = document.getElementsByClassName( "RichText ztext Post-RichText" ); if (holder.length === 0) { console.log("get content fail"); return; } this.blue = Math.ceil(Math.random() * 100) % 2 === 0; !this.blue && (this.index = 255); holder = holder[0]; const tags = ["p", "ul", "li", "ol", "blockquote"]; const textNode = []; let i = -1; for (const node of holder.childNodes) { i++; const type = node.nodeType; //text node deal with separately if (type === 3) { textNode.push(i); continue; } else if (!tags.includes(node.tagName.toLowerCase())) { //the continuity of content is interrupted, reset the color; this.resetColor(); if (node.className === "highlight") this.codeHightlight(node); continue; } this.arr = []; this.nodeCount = 0; this.getItem(node); } i = textNode.length; if (i > 0) { for (i; i--; ) { let node = holder.childNodes[textNode[i]]; const text = node.nodeValue; if (text) { this.arr = []; this.textDetach(text); const iNode = document.createElement("colorspan"); holder.insertBefore(iNode, node); iNode.outerHTML = this.arr.join(""); node.remove(); } } } this.arr = null; }, }, userPage: { username: null, userManage(mode) { let text = ""; const info = {}; if (mode === "Block") { info.mode = "block"; blackName.push(this.username); text = "add user to blackname successfully"; } else { info.mode = "unblock"; const i = blackName.indexOf(this.username); i > -1 && blackName.splice(i, 1); text = "remove user from blackname successfully"; } info.username = this.username; GM_setValue("blacknamechange", info); GM_setValue("blackname", blackName); Notification(text, "blackName"); }, injectButton(name) { const html = `
`; document.documentElement.insertAdjacentHTML("beforeend", html); let assist = document.getElementById("assist-button-container"); assist.children[1].onclick = function () { const n = this.innerText; zhihu.userPage.userManage(n); this.innerText = n === "Block" ? "unBlock" : "Block"; }; assist = null; }, changeButton(mode) { const button = document.getElementById( "assist-button-container" ); if (!button) return; const name = button.children[1].innerText; if ((mode && name === "unBlock") || (!mode && name === "Block")) return; button.children[1].innerText = blackName.includes(this.name) ? "unBlock" : "Block"; }, main() { const profile = document.getElementsByClassName( "ProfileHeader-name" ); if (profile.length === 0) { console.log("get usename id fail"); return; } this.username = `${profile[0].innerText}`; this.username && this.injectButton( blackName.includes(this.username) ? "unBlock" : "Block" ); }, }, pageOfQA(index, href) { //inject as soon as possible; may be need to concern about some eventlisteners and MO this.inputBox.controlEventListener(); this.addStyle(index); index < 2 && this.antiLogin(); this.clearStorage(); window.onload = () => { if (index !== 7) { this.getData(); this.blackUserMonitor(index); (index < 4 ? !(index === 1 && href.endsWith("/waiting")) : false) && (setTimeout(() => this.Filter.main(index), 100), this.colorIndicator()); index === 6 && this.userPage.main(); } this.inputBox.monitor(); }; }, blackUserMonitor(index) { GM_addValueChangeListener( "blackname", (name, oldValue, newValue, remote) => { if (!remote) return; //mode => add user to blockname blackName = newValue; if (index === 6) { const mode = !oldValue || oldValue.length < newValue.length; this.userPage.changeButton(mode); } else { this.Filter.userChange(index); } } ); }, start() { const pos = [ "/answer/", "/question/", "/topic/", "/search", "/column", "/zhuanlan", "/people/", "/www", ]; const href = location.href; const index = pos.findIndex((e) => href.includes(e)); let w = true; let z = false; ( (z = index === 5) ? (w = !href.includes("/write")) : index === 4 ? true : false ) ? this.zhuanlanStyle(z && href.includes("/p/")) : index < 0 ? null : this.pageOfQA(index, href); w && this.antiRedirect(); this.shade.start(); this.clipboardClear(); }, }; zhihu.start(); })();