雪阅模式|SNOREAD (无滚动版)

【原版滚动有问题的请用这个版本】【雪阅模式|SNOREAD】像读报纸一样纵览这个世界吧!豪华广角宽屏视角 / 刷知乎神器 / 2D排版 / 快速提升视觉维度 / 横向滚动阅读模式 / 翻页模式 / 充分利用屏幕空间 / 快阅速读插件 / 雪阅模式 / 宽屏必备 / 带鱼屏专属 | 使用说明:按 Escape 退出雪阅模式 | 【欢迎加入QQ群交流 1043957595 或 官方TG群组 https://t.me/snoread 】

  1. // ==UserScript==
  2. // @name 雪阅模式|SNOREAD (无滚动版)
  3. // @namespace https://userscript.snomiao.com/
  4. // @version 1.3(20200719)
  5. // @description 【原版滚动有问题的请用这个版本】【雪阅模式|SNOREAD】像读报纸一样纵览这个世界吧!豪华广角宽屏视角 / 刷知乎神器 / 2D排版 / 快速提升视觉维度 / 横向滚动阅读模式 / 翻页模式 / 充分利用屏幕空间 / 快阅速读插件 / 雪阅模式 / 宽屏必备 / 带鱼屏专属 | 使用说明:按 Escape 退出雪阅模式 | 【欢迎加入QQ群交流 1043957595 或 官方TG群组 https://t.me/snoread 】
  6. // @author snomiao@gmail.com
  7. // @match http://*/*
  8. // @match https://*/*
  9. // @exclude https://*.1688.com/*
  10. // @exclude https://*.tmall.com/*
  11. // @exclude https://*.taobao.com/*
  12. // @grant none
  13. // ==/UserScript==
  14. //
  15. // (20200717)脚本作者snomiao正在寻找一份可远程的工作,现坐标上海。
  16. // 意向技术栈:nodejs、typescript 相关。联系方式 snomiao@gmail.com
  17. //
  18.  
  19. (function () {
  20. 'use strict';
  21. 'esversion: 6';
  22. let 用户意向_雪阅模式 = true;
  23. let DEBUG_SNOREAD = false;
  24.  
  25. const 新元素 = (HTML, 属性 = {}) => {
  26. const e = document.createElement("div");
  27. e.innerHTML = HTML;
  28. return Object.assign(e.children[0], 属性)
  29. }
  30. const = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  31. const 取窗口高 = () => document.body.parentElement.clientHeight;
  32. const 取窗口宽 = () => document.body.parentElement.clientWidth;
  33.  
  34. const 更新样式 = () => {
  35. const 窗口高 = 取窗口高(), 窗口宽 = 取窗口宽()
  36. var 样式盒 = document.querySelector("div.snomiao-article-style")
  37. if (!样式盒) {
  38. 样式盒 = document.createElement("div");
  39. 样式盒.classList.add("snomiao-article-style");
  40. 样式盒.style.display = "none";
  41. document.body.appendChild(样式盒)
  42. }
  43. // 规避 iframe 的 innerHeight 超长问题
  44. 样式盒.innerHTML = `
  45. <style>
  46.  
  47. div#main-wrapper:after, .clearfix:after {
  48. display:block;
  49. content:"clear";
  50. clear:both;
  51. line-height:0;
  52. visibility:hidden;
  53. }
  54.  
  55. .snomiao-article::-webkit-scrollbar { width: 0 !important }
  56. .snomiao-article{ -ms-overflow-style: none; }
  57. .snomiao-article{ overflow: -moz-scrollbars-none; }
  58. .snomiao-article:before{
  59. content: "雪阅 | SNOREAD";
  60. background: rgba(0,0,0,0.1);
  61. color: rgba(0,0,0,0.5);
  62. position: absolute;
  63. padding: 0.5rem 1.5rem;
  64. left: 3px;
  65. top: 3px;
  66. text-align: center;
  67. }
  68. .snomiao-article{
  69. position: relative !important;
  70. /* top: 0; */
  71. box-sizing: border-box !important;
  72. height: ${窗口高}px !important;
  73. width: ${窗口宽}px !important;
  74. max-width: ${窗口宽}px !important;
  75.  
  76. display: flex !important;
  77. flex-flow: column !important;
  78. flex-wrap: wrap !important;
  79. align-content: flex-start !important;
  80.  
  81. overflow-x: auto !important;
  82. overflow-y: hidden !important;
  83.  
  84. z-index:1 !important;
  85. /* 双框 */
  86. box-shadow: 00 0 0rem 1px black inset, 0 0 0rem 2px white inset, 0 0 0rem 3px black inset !important;
  87. background-color: rgba(255,255,255,0.8) !important;
  88. color: black !important;
  89.  
  90. text-align: justify !important;
  91. text-indent: 0 !important;
  92. padding: 10% 1rem;
  93. }
  94. .snomiao-article>*{
  95. /* display: block !important; */
  96. background-color: rgba(255,255,255,0.8) !important;
  97. max-width: 40rem !important;
  98. }
  99. .snomiao-article>*:not(li){
  100. padding: 0 2rem 1rem 0 !important;
  101. margin: 0 -1rem 0 0 !important;
  102. min-width: 32rem !important;
  103. width: min-content !important;
  104.  
  105. max-height: 100% !important;
  106. height:auto !important;
  107. overflow-x: auto !important;
  108. overflow-y: auto !important;
  109. }
  110. /* 解决pre换行问题 */
  111. .snomiao-article pre{
  112. white-space: pre-wrap;
  113. }
  114. /* 知乎侧边推送精准置底 */
  115. /* https://www.zhihu.com/ */
  116. .Question-sideColumn,.ContentLayout-sideColumn{
  117. z-index: 0;
  118. }
  119.  
  120. </style>`;
  121. }
  122.  
  123. const 点击定位到文章监听 = 元素 => {
  124. if (元素.标记_点击切换雪阅模式) return;
  125. 元素.标记_点击切换雪阅模式 = true
  126. // 点击定位到文章
  127. 元素.addEventListener("click", function (事件) {
  128. (元素.scrollIntoViewIfNeeded || 元素.scrollIntoView).call(e)
  129. }, false);
  130. }
  131. const 元素可见性修复解除 = (元素) => {
  132. 元素.parentElement && 元素可见性修复解除(元素.parentElement)
  133. if (!元素.标记_元素可见性修复完成) return;
  134. 元素.标记_元素可见性修复完成 = false
  135.  
  136. // 父元素 overflow: visible
  137. if (元素.origin_overflow) { 元素.style.overflow = 元素.origin_overflow }
  138. }
  139. const 元素可见性修复 = (元素) => {
  140. 元素.parentElement && 元素可见性修复(元素.parentElement)
  141. if (元素.标记_元素可见性修复完成) return;
  142. 元素.标记_元素可见性修复完成 = true
  143.  
  144. // 父元素 overflow: visible
  145. if (window.getComputedStyle(元素).getPropertyValue('overflow') == "hidden") {
  146. 元素.origin_overflow = "hidden"
  147. 元素.style.overflow = "visible"
  148. }
  149. }
  150. const 元素位置到屏幕适配 = (元素) => {
  151. 元素.setAttribute("style", `left: 0`);
  152. const { left } = 元素.getBoundingClientRect()
  153. 元素.classList.add("snomiao-article")
  154. 元素.setAttribute("style", `left: calc(${-left}px)`);
  155. }
  156. // 元素位置到屏幕适配(temp1)
  157.  
  158. const 内含文本节点向段落替换 = (元素) => [...元素.childNodes].filter(e => !e.tagName).forEach(e => {
  159. if (!e.textContent.trim()) return null
  160. e.parentElement.insertBefore(新元素(`<p class='snomiao-replaced'>${e.textContent}</p>`), e)
  161. e.remove()
  162. })
  163. const 段落向文本节点还原 = (元素) => [...元素.querySelectorAll("p.snomiao-replaced")].forEach(e => {
  164. if (!e.textContent.trim()) return null
  165. e.parentElement.insertBefore(document.createTextNode(e.textContent), e)
  166. e.remove()
  167. })
  168. const 进入雪阅模式 = (元素) => {
  169. 退出雪阅模式(元素)
  170. window.snomiao_article = 元素
  171. 点击定位到文章监听(元素)
  172. 元素可见性修复(元素);
  173. // 为了对齐
  174. 内含文本节点向段落替换(元素)
  175. 更新样式()
  176. 元素位置到屏幕适配(元素)
  177. // ref: 适配此页面 https://medium.com/s/story/why-sleep-on-it-is-the-most-useful-advice-for-learning-and-also-the-most-neglected-86b20249f06d
  178. // 未知原因错位,不过写2次就能正常了
  179. 元素位置到屏幕适配(元素)
  180. 元素.标记_雪阅模式 = true
  181. console.debug(元素, "进入雪阅模式");
  182. }
  183. const 退出雪阅模式 = 元素 => {
  184. 段落向文本节点还原(元素)
  185. 元素.setAttribute("style", ``);
  186. 元素.classList.remove("snomiao-article")
  187. 元素可见性修复解除(元素)
  188. 元素.标记_雪阅模式 = false
  189. console.debug(元素, "退出雪阅模式");
  190. }
  191. const 退出雪阅模式_临时 = 元素 => {
  192. 元素.setAttribute("style", ``);
  193. 元素.classList.remove("snomiao-article")
  194. 元素可见性修复解除(元素)
  195. }
  196.  
  197. const 切换雪阅模式 = (元素) => 元素.classList.contains("snomiao-article") && (退出雪阅模式(元素), true) || 进入雪阅模式(元素)
  198. const 更新雪阅模式 = (元素) => 元素.classList.contains("snomiao-article") != 元素.标记_雪阅模式 && 切换雪阅模式(元素)
  199.  
  200. const 恢复所有文章样式_临时 = () => [...document.querySelectorAll(".snomiao-article")].map(退出雪阅模式_临时)
  201.  
  202. // 解决span取到offsetHeight为0的问题
  203. const 取元素投影高 = (元素) => 元素.offsetHeight || 元素.getBoundingClientRect().height
  204. const 取元素投影宽 = (元素) => 元素.offsetWidth || 元素.getBoundingClientRect().width
  205. const 取元素投影顶 = (元素) => 元素.getBoundingClientRect().top
  206. const 取元素面积 = (元素) => 取元素投影高(元素) * 取元素投影宽(元素)
  207. const 取最大值序 = (列) => 列.indexOf(Math.max(...列))
  208. const 压平 = => 列.flat()
  209. const 排序按 = 函数 => => 列.sort((a, b) => 函数(a) - 函数(b))
  210. const 取距离按 = 函数 => (a, b) => 函数(a) - 函数(b)
  211. const 翻转矩阵 = 矩阵 => 矩阵[0].map((列, 列号) => 矩阵.map(行 => 行[列号]));
  212. const 取相邻对 = => 翻转矩阵([列.slice(1), 列.slice(0, -1)])
  213. const 取相邻关系按 = 关系 => => 取相邻对(列).map(对 => 关系(...对))
  214. const 文章树取元素 = (文章树) => [文章树.元素, ...(文章树.子树列 && 文章树.子树列.map(文章树取元素) || [])]
  215. const 元素包含判断 = (父元素, 子孙元素) => 父元素.contains(子孙元素)
  216. const 检测重叠冲突 = (文章树) => {
  217. const 窗口高 = 取窗口高(),
  218. 窗口宽 = 取窗口宽()
  219. const 元素列 = 文章树取元素(文章树).flat(Infinity)
  220. const 文章列 = 排序按(取元素投影顶)(元素列.filter(e => e.标记_是文章))
  221. const 相邻对列 = 取相邻对(文章列).map(对 => (对.距离 = 取距离按(取元素投影顶)(...对), 对))
  222. const 异常对列 = 相邻对列.filter(对 => 对.距离 < 窗口高)
  223. const 冲突元素列 = 异常对列.map(异常对 => 排序按(取元素面积)(异常对))
  224. 冲突元素列.forEach(([弱势元素, 强势元素]) => {
  225. // 若子元素与父元素冲突,则子元素不算弱势;换句话说,若弱势元素为强势元素的子元素,则强弱关系互换
  226. if (元素包含判断(强势元素, 弱势元素)) {
  227. const tmp = 弱势元素
  228. 弱势元素 = 强势元素
  229. 强势元素 = tmp
  230. }
  231. 强势元素.标记_冲突弱势元素 = 强势元素.标记_冲突弱势元素 || false
  232. 弱势元素.标记_冲突弱势元素 = 弱势元素.标记_冲突弱势元素 || true
  233.  
  234. if (DEBUG_SNOREAD) {
  235. 强势元素.冲突元素对 = [弱势元素, 强势元素]
  236. 弱势元素.冲突元素对 = [弱势元素, 强势元素]
  237. 弱势元素.setAttribute('冲突弱势元素', true)
  238. }
  239. })
  240. }
  241. const 取文章树 = (元素, 层数 = 0) => {
  242. const 窗口高 = 取窗口高(),
  243. 窗口宽 = 取窗口宽();
  244.  
  245. const 元素外高 = 取元素投影高(元素);
  246. const 子元素 = [...元素.children]
  247. const 子元素高于屏 = 子元素.filter(e => 取元素投影高(e) > 窗口高)
  248. const 主要的子元素 = 子元素高于屏.filter(e => 取元素投影高(e) / 元素外高 > 0.5)
  249.  
  250. const 元素宽度占比够小 = 元素.clientWidth < 窗口宽 * 0.95
  251. const 正确的元素类型 = !['IMG', 'PRE', 'TBODY'].includes(元素.tagName)
  252. const 是文章 = !主要的子元素.length && 元素宽度占比够小 && 子元素.length >= 3 && 正确的元素类型
  253.  
  254. const 子树列 = 子元素高于屏.map(e => 取文章树(e, 层数 + 1)) || []
  255.  
  256. const 占比 = 取元素投影高(元素) / 取元素投影高(元素.parentElement)
  257. 元素.标记_是文章 = 是文章
  258.  
  259. if (DEBUG_SNOREAD) {
  260. 元素.setAttribute("len子元素", 子元素.length)
  261. 元素.setAttribute("len子元素高于屏", 子元素高于屏.length)
  262. 元素.setAttribute("len主要的子元素", 主要的子元素.length)
  263. 元素.setAttribute("是文章", 是文章)
  264. 元素.setAttribute("占比", 占比)
  265. 是文章 && console.debug(元素, "是文章");
  266. }
  267. return { 元素, 是文章, 占比, 子树列 }
  268. }
  269. var 输出文章树 = (树) => {
  270. return [树.元素, 树.是文章, 树.占比, ...(树.子树列 && 树.子树列.map(输出文章树) || [])]
  271. }
  272. var 转换文章树 = ({ 元素, 是文章, 子树列 }) => {
  273. var 子树有文章 = !!子树列.map(转换文章树).filter(e => e).length
  274. if (子树有文章) return true;
  275. if (元素 == document.body) return;
  276.  
  277. if (!元素.标记_是文章) return;
  278. // if (!是文章) return;
  279. if (元素.标记_冲突弱势元素) return;
  280. 更新雪阅模式(元素)
  281. return true
  282. }
  283. var 正在扫描并转换文章树 = false;
  284. const 文章树扫描并转换 = async () => {
  285. console.info("[雪阅] 激活");
  286. 正在扫描并转换文章树 = 1
  287. // 进行操作的时候不要监听自已
  288. window.SNOREAD_observer && window.SNOREAD_observer.disconnect()
  289. await 恢复所有文章样式_临时()
  290. if (!用户意向_雪阅模式) return null;
  291.  
  292. const 文章树 = 取文章树(document.body)
  293. if (DEBUG_SNOREAD) {
  294. window.调试文章树1 = 文章树
  295. window.调试文章树2 = 输出文章树(文章树)
  296. console.debug(输出文章树(文章树))
  297. }
  298. 检测重叠冲突(文章树)
  299. 转换文章树(文章树)
  300. 正在扫描并转换文章树 = 0
  301. window.SNOREAD_observer && window.SNOREAD_observer.observe(document.querySelector('body'), { childList: true, subtree: true });
  302. }
  303.  
  304.  
  305. const 节流防抖化 = (函数, 间隔 = 1000) => {
  306. // 本函数的作用是结合节流和防抖的特性,只保留间隔内的首次和末次调用
  307. // 执行示意(比如间隔 4 字符)
  308. // 外部调用
  309. // ----!--!!!!!!-!---------!----
  310. // 内部调用
  311. // ----!-------------!-----!----
  312. let 冷却中 = false
  313. let 时钟号 = null
  314. const 冷却开始 = () => {
  315. 冷却中 = true
  316. 时钟号 = setTimeout(() => { 冷却中 = false }, 间隔);
  317. }
  318. return (...参数) => new Promise((resolve, _) => {
  319. const 现在时间 = +new Date()
  320. // 若本次是首次触发,则直接执行
  321. if (!冷却中) {
  322. resolve(函数(...参数))
  323. 冷却开始()
  324. } else {
  325. // 若短时间再次触发则进入防抖
  326. if (时钟号 !== null) clearTimeout(时钟号);
  327. 时钟号 = setTimeout(() => {
  328. resolve(函数(...参数))
  329. 冷却开始()
  330. }, 间隔);
  331. }
  332. })
  333. }
  334. const 页面可见时才运行化 = (函数) => async (...参) => {
  335. if (document.hidden) return;
  336. return await 函数(...参)
  337. }
  338. const 开始 = 页面可见时才运行化(节流防抖化(文章树扫描并转换, 1000))
  339.  
  340. // 窗口载入
  341. window.addEventListener('load', 开始, false)
  342. // 页面大小变化
  343. window.addEventListener("resize", 开始, false)
  344. window.SNOREAD_observer = new MutationObserver(function (mutations, observe) {
  345. 开始()
  346. });
  347. SNOREAD_observer.observe(document.querySelector('body'), { childList: true, subtree: true });
  348.  
  349. console.info("[雪阅] 加载完成");
  350. 开始()
  351.  
  352.  
  353. const 用户意向退出雪阅模式 = () => {
  354. 用户意向_雪阅模式 = false;
  355. 开始()
  356. }
  357. window.addEventListener("keydown", e => e.code == "Escape" && 用户意向退出雪阅模式())
  358.  
  359. })();
  360. // 雪星今天也要努力活下去吖!

QingJ © 2025

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