zhi-hu

知知乎乎(收藏夹双列;隐藏视频回答;加宽;区分问题和视频;多图预警)

当前为 2025-01-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name zhi-hu
  3. // @namespace https://greasyfork.org/zh-CN/scripts/438709-zhi-hu
  4. // @version 0.1.1
  5. // @description 知知乎乎(收藏夹双列;隐藏视频回答;加宽;区分问题和视频;多图预警)
  6. // @author Song
  7. // @match *://www.zhihu.com/*
  8. // @match *://zhuanlan.zhihu.com/*
  9. // @license MIT
  10. // @grant none
  11. // ==/UserScript==
  12. (function () {
  13.  
  14. function domesticate(text) {
  15. const doc = new DOMParser().parseFromString(text, 'text/html');
  16. return doc.body.firstChild;
  17. }
  18.  
  19. function debounce(func, wait, immediate) {
  20. let timeout;
  21. let result;
  22. const later = () => setTimeout(() => {
  23. timeout = null;
  24. if (!immediate) result = func.apply(this, arguments);
  25. }, wait);
  26. return function (...args) {
  27. const context = this;
  28. const callNow = immediate && !timeout;
  29. clearTimeout(timeout);
  30. timeout = later();
  31. if (callNow) result = func.apply(context, args);
  32. return result;
  33. };
  34. }
  35.  
  36. //region style
  37.  
  38. function addStyleElement() {
  39. let el = document.createElement('style');
  40. el.setAttribute('name', 'zhi_zhi_hu_hu');
  41. document.head.appendChild(el);
  42. }
  43.  
  44. /**
  45. *
  46. * @param {string} selector
  47. * @param {Partial<CSSStyleDeclaration>} style
  48. * @param {string[]} [important]
  49. */
  50. function createRule(selector, style, important) {
  51. const s = document.createElement('div').style;
  52. Object.assign(s, style);
  53. const text = s.cssText;
  54. if (important && important.length > 0) {
  55. throw new Error('un supported important');
  56. }
  57. return selector + `{${text}}`;
  58. }
  59.  
  60. /**
  61. * 插入样式表
  62. */
  63. function insertCSS() {
  64. let styleSheet = document.styleSheets[document.styleSheets.length - 1];
  65.  
  66. /**
  67. *
  68. * @param {string} selector
  69. * @param {Partial<CSSStyleDeclaration>} style
  70. * @param {string[]} [important]
  71. */
  72. function appendRule(selector, style, important) {
  73. styleSheet.insertRule(createRule(selector, style, important));
  74. }
  75.  
  76. /*收藏栏的样式,变成双列*/
  77. styleSheet.insertRule('.Modal--large.FavlistsModal {width: 600px;}');
  78. styleSheet.insertRule('.Favlists-content .Favlists-item {width: 230px; float: left;}');
  79. styleSheet.insertRule(' .Favlists-content .Favlists-item:nth-child(even){margin-left: 60px;}');
  80.  
  81. /*隐藏视频回答*/
  82. styleSheet.insertRule('.VideoAnswerPlayer, .VideoAnswerPlayer video, .VideoAnswerPlayer-video, .VideoAnswerPlayer-iframe {height: 2px;}');
  83. // styleSheet.insertRule('.ZVideoItem {height: 2px;}');
  84. styleSheet.insertRule('.ContentItem.ZVideoItem {height: 8px;}');
  85. styleSheet.insertRule('.ContentItem.EduSectionItem {height: 8px;}');
  86. styleSheet.insertRule('.ZvideoItem .RichContent-cover{ height:8px; }');
  87. styleSheet.insertRule('.ZvideoItem .RichContent-cover-inner{height:4px; }');
  88. styleSheet.insertRule('.VideoAnswerPlayer video, nav.TopstoryTabs > a[aria-controls="Topstory-zvideo"]{height:4px; }');
  89.  
  90.  
  91. /*区分问题 和 视频*/
  92. let style = `font-weight: bold;font-size: 13px;padding: 1px 4px 0;border-radius: 2px;display: inline-block;vertical-align: top;margin: ${(location.pathname === '/search') ? '2' : '4'}px 4px 0 0;`
  93. let styles = [
  94. `.AnswerItem .ContentItem-title a:not(.zhihu_e_toQuestion)::before {content:'回答';color: #f68b83;background-color: #f68b8333;${style}}`,
  95. `.TopstoryQuestionAskItem .ContentItem-title a:not(.zhihu_e_toQuestion)::before {content:'回答';color: #ff5a4e;background-color: #ff5a4e33;${style}}`,
  96. `.ZVideoItem .ContentItem-title a::before, .ZvideoItem .ContentItem-title a::before {content:'视频';color: #00BCD4;background-color: #00BCD433;${style}}`,
  97. `.ArticleItem .ContentItem-title a::before {content:'文章';color: #2196F3;background-color: #2196F333;${style}}`
  98. ];
  99. styles.forEach(s => styleSheet.insertRule(s));
  100.  
  101. /*视频*/
  102. styleSheet.insertRule('.ZVideoItem .RichContent{opacity: 0.5; color: #666 !important; font-style:italic !important;}');
  103.  
  104. /*调整列表中专栏文章的样式*/
  105. styleSheet.insertRule('.ContentItem[itemprop=article]{opacity: 0.5; color: #666;font-style:italic;}');
  106. styleSheet.insertRule('.ContentItem[itemprop=article] .ContentItem-title{color: #666; }');
  107.  
  108.  
  109. /* 隐藏 footer */
  110. styleSheet.insertRule('footer.Footer{display:none !important;}')
  111.  
  112. /* 多图预警的样式 */
  113. appendRule('.cloakroom', {
  114. position: 'relative', overflow: 'hidden',
  115. width: '100%', height: '196px',
  116. background: 'rgba(128, 160, 160, 0.8)'
  117. });
  118. /* appendRule('.cloak-image', {
  119. position: 'absolute', margin: '0.5rem',
  120. height: 'calc(100% - 1rem) !important', width: 'auto !important',
  121. });*/
  122. styleSheet.insertRule('.cloak-image { position: absolute; margin: 0.5rem !important; height: calc(100% - 1rem) !important; width: auto !important;}');
  123. appendRule('.cloak-info', {
  124. marginLeft: '20rem', fontSize: '14px',
  125. display: 'flex', flexDirection: 'column', gap: '1rem',
  126. alignItems: 'center', justifyContent: 'center',
  127. color: 'rgb(255, 255, 255)', height: '100%',
  128. });
  129. appendRule('.div-btn', {
  130. cursor: 'default', padding: '4px 1em',
  131. border: '1px solid rgba(255, 255, 255, 0.5)', borderRadius: '999px',
  132. });
  133.  
  134. /* 多图预警 小图 */
  135. appendRule('.cloakroom.scarf', {height: '48px'});
  136. appendRule('.scarf .scarf-hidden, .scarf-show', {display: 'none'});
  137. appendRule('.scarf .scarf-show', {display: 'block'});
  138. appendRule('.scarf .cloak-info', {flexDirection: 'row'});
  139. }
  140.  
  141. /**
  142. * 增宽
  143. * @param {number} maxWidth
  144. */
  145. function widening(maxWidth) {
  146. const ww = window.innerWidth - 30;
  147. if (ww < 1000) return;
  148.  
  149. let w = ww > maxWidth ? maxWidth : ww;
  150. let styleSheet = document.styleSheets[document.styleSheets.length - 1];
  151. styleSheet.insertRule('.Topstory-container, .QuestionHeader-main, .Question-main{min-width:' + w + 'px !important;}');
  152. styleSheet.insertRule('.Topstory-mainColumn, .Question-mainColumn{width:' + (w - 300) + 'px !important;}');
  153. styleSheet.insertRule('.QuestionHeader-content{padding-left:' + ((ww - w) / 2) + 'px !important;}');
  154. // document.querySelector('.Topstory-container').style.minWidth = w + 'px';
  155. // document.querySelector('.Topstory-mainColumn').style.width = (w - 300) + 'px';
  156. // 专栏文章
  157. styleSheet.insertRule('.Post-Main .Post-RichTextContainer {min-width:' + w + 'px !important;}');
  158. }
  159.  
  160. //endregion
  161.  
  162. //region 多图预警
  163.  
  164. function createCloak(index, total) {
  165. const text = `
  166. <div tabindex="${index}" class="cloak-info" role="button">
  167. <div>多图预警(<span>${index + 1}</span>/<span>${total}</span>)</div>
  168. <div class="div-btn scarf-hidden" data-action="hide_me">隐藏此图</div>
  169. <div class="div-btn scarf-hidden" data-action="hide_all">隐藏所有</div>
  170. <div class="div-btn scarf-show" data-action="thumb_me">预览此图</div>
  171. <div class="div-btn scarf-show" data-action="thumb_all">预览所有</div>
  172. <div class="div-btn" data-action="collapse">收起</div>
  173. </div>
  174. </div>`
  175. return domesticate(text);
  176. }
  177.  
  178. /**
  179. *
  180. * @param {HTMLDivElement} item
  181. */
  182. function handleContent(item) {
  183. const processed = item.querySelectorAll('figure div.cloakroom').length > 0;
  184. const meta = item.querySelector('.ContentItem-meta');
  185. console.info('[zhi-hu]', '处理文章', processed, meta.textContent);
  186. if (processed) return;
  187. const rc = item.querySelector('.RichContent');
  188. const collapsed = rc.classList.contains('is-collapsed');
  189. const r = rc.querySelector('.RichContent-inner');
  190. const figures = r.querySelectorAll('figure');
  191. const len = figures.length;
  192. if (len < 2) {
  193. return;
  194. }
  195. // item.classList.add('cloak-dagger');
  196. if (collapsed) {
  197. const len = r.querySelectorAll('p').length;
  198. const btn = rc.querySelector('.ContentItem-expandButton');
  199. btn.textContent = `多图预警(${len} ${len} 图)`;
  200. }
  201. for (let i = 0; i < len; i++) {
  202. const figure = figures[i];
  203. if (figure.querySelectorAll('img').length > 1) {
  204. // 图片不适
  205. continue;
  206. }
  207. const d = figure.querySelector('div');
  208. const img = d.querySelector('img');
  209. if (img.height / img.width < 0.2) {
  210. // 跳过较矮的图片
  211. d.classList.add('cloak-skip');
  212. continue;
  213. }
  214. if (len > 4) {
  215. d.classList.add('cloakroom', 'scarf');
  216. } else {
  217. d.classList.add('cloakroom');
  218. }
  219. img.classList.add('cloak-image', 'scarf-hidden');
  220. const info = createCloak(i, len)
  221. d.appendChild(info);
  222. info.addEventListener('click', (e) => {
  223. if (!e.target.classList.contains('div-btn')) return;
  224. const action = e.target.dataset.action;
  225.  
  226. switch (action) {
  227. case 'hide_me': {
  228. d.classList.add('scarf');
  229. break;
  230. }
  231. case 'hide_all': {
  232. const elements = item.querySelectorAll('figure div.cloakroom');
  233. for (let j = 0; j < elements.length; j++) {
  234. elements[j].classList.add('scarf');
  235. }
  236. console.info('[zhi-hu]', 'hide all', elements.length);
  237. break;
  238. }
  239. case 'thumb_me': {
  240. d.classList.remove('scarf');
  241. break;
  242. }
  243. case 'thumb_all': {
  244. const elements = item.querySelectorAll('figure div.cloakroom');
  245. for (let j = 0; j < elements.length; j++) {
  246. elements[j].classList.remove('scarf');
  247. }
  248. console.info('[zhi-hu]', 'thumb all', elements.length);
  249. break;
  250. }
  251. case 'collapse': {
  252. const el = item.querySelector('.RichContent-collapsedText');
  253. if (el) {
  254. el.parentElement.click();
  255. }
  256. }
  257. }
  258. })
  259.  
  260.  
  261. }
  262. }
  263.  
  264. function handleList() {
  265.  
  266. const process = debounce(() => {
  267. const list = document.querySelectorAll('.ContentItem');
  268. console.info('[zhi-hu]', 'Content items', list.length);
  269. for (let i = 0; i < list.length; i++) {
  270. handleContent(list[i]);
  271. }
  272. }, 800)
  273.  
  274. // 在 main Column 监听 ,有可能失效。
  275. // const question = document.querySelector('.Question-mainColumn');
  276.  
  277. const observer = new MutationObserver((mutations) => {
  278. console.info('[zhi-hu]', 'observer mutations', mutations.length);
  279. process();
  280. });
  281. observer.observe(document, {attributes: false, childList: true, subtree: true});
  282.  
  283. process();
  284. }
  285.  
  286.  
  287. //endregion
  288.  
  289. addStyleElement();
  290. widening(1200);
  291. insertCSS();
  292. handleList();
  293. })();