笔趣阁优化

专注阅读

  1. // ==UserScript==
  2. // @name 笔趣阁优化
  3. // @namespace https://gitee.com/linhq1999/OhMyScript
  4. // @version 5.2
  5. // @description 专注阅读
  6. // @author LinHQ
  7. // @match http*://www.shuquge.com/*.html
  8. // @exclude http*://www.shuquge.com/*index.html
  9. // @match http*://www.sywx8.com/*.html
  10. // @match http*://www.biqugetv.com/*.html
  11. // @match http*://www.bqxs520.com/*.html
  12. // @match https://www.dshfood.net/*.html
  13. // @grant GM_addStyle
  14. // @grant GM_openInTab
  15. // @grant GM_xmlhttpRequest
  16. // @require https://gf.qytechs.cn/scripts/427726-gbk-url-js/code/GBK_URLjs.js?version=953098
  17. // @inject-into auto
  18. // @license MIT
  19. // ==/UserScript==
  20. 'use strict';
  21. /** 配置示例
  22. * 建议在定制 search 函数时, rq 函数始终把参数写全
  23. * "sites": [
  24. * {
  25. * "desc": "shuquge", 网站链接关键字
  26. * "url": "https://....", 网站首页链接
  27. * "main": "div.reader", 主要部分选择器
  28. * "title": ".reader h1", 标题选择器
  29. * "txt": "#content", 文字部分选择器
  30. * "toc": "dd a", 目录链接选择器
  31. * "tocJump": 12, 跳过前面多少章
  32. * "filter": ["div.header", "div.nav", "div.link"], 带有此选择器的元素将被删除
  33. * "txtfilter": ["shuqu"] 带有此关键字的行将被删除
  34. * "funcFilter"?: () => void, 自定义过滤器
  35. * "nodash"?: boolean, 判断是否应该在书籍详情页链接后加额外的斜杠
  36. * "search"?: (keywords: string, baseurl:string) => Promise<Link[]> 搜索行为
  37. * }
  38. * ]
  39. */
  40. (() => {
  41. // 缺省值,一般不用修改
  42. const lineHeight = 1.3;
  43. // const defaultFont = "楷体";
  44. const defaultFont = "Source Han Sans SC VF";
  45. let C = {
  46. "sites": [
  47. {
  48. "desc": "shuquge",
  49. "url": "https://www.shuquge.com/",
  50. "main": "div.reader",
  51. "title": ".reader h1",
  52. "txt": "#content",
  53. "toc": "dd a",
  54. "tocJump": 12,
  55. "filter": [
  56. "div.header", "div.nav", "div.link", "img",
  57. "#coupletleft", "#coupletright", "#HMRichBox"
  58. ],
  59. "txtfilter": ["shuqu"],
  60. "funcFilter": () => { var _a, _b; return (_b = (_a = fd(document, "#content")) === null || _a === void 0 ? void 0 : _a.previousSibling) === null || _b === void 0 ? void 0 : _b.remove(); }
  61. },
  62. {
  63. "desc": "sywx",
  64. "url": "https://www.sywx8.com/",
  65. "main": "div#container",
  66. "title": "div>h1",
  67. "toc": "li a",
  68. "tocJump": 0,
  69. "txt": "div#BookText",
  70. "filter": ["div.top", ".link.xb", "#footer"],
  71. "txtfilter": ["最快更新", "松语", "本章完", "本章未完"],
  72. // javascript 不支持 gbk 的 uri 编码,所以无法实现
  73. // 但是用 gbk.js 就不一样了
  74. "search": async (keywords, baseurl) => {
  75. let links = [];
  76. let doc = await rq({
  77. "url": `https://www.sywx8.com/modules/article/search.php?searchkey=${$URL.encode(keywords)}`
  78. }, 8000, "GBK");
  79. for (let a of doc.querySelectorAll(".c_row .c_subject a")) {
  80. // 这个网站比较特殊,链接默认是完整的
  81. links.push({ "title": `(sywx) ${a.textContent}`, "href": attr(a, "href") });
  82. }
  83. return links;
  84. }
  85. },
  86. {
  87. "desc": "bqxs",
  88. "url": "http://www.bqxs520.com/",
  89. "main": ".box_con",
  90. "title": "div.content_read h1",
  91. "toc": "#list dd a",
  92. "tocJump": 9,
  93. "txt": "#content",
  94. "filter": [".ywtop", ".header", ".nav", ".bottem1", ".lm", "#page_set", ".bookname~.box_con"],
  95. "txtfilter": ["请记住本书", "http"],
  96. "search": async (keywords, baseurl) => {
  97. let links = [];
  98. let doc = await rq({
  99. "method": "POST",
  100. "headers": { "Content-Type": "application/x-www-form-urlencoded" },
  101. "url": encodeURI(`http://www.bqxs520.com/case.php?m=search`),
  102. "data": `&key=${encodeURI(keywords)}`
  103. }, 7000, "UTF-8");
  104. for (let a of doc.querySelectorAll(".l .s2 a")) {
  105. links.push({ "title": `(bqxs) ${a.textContent}`, "href": concatURL(baseurl, attr(a, "href")) });
  106. }
  107. return links;
  108. }
  109. },
  110. {
  111. "desc": "biqugetv",
  112. "url": "https://www.biqugetv.com/",
  113. "main": ".box_con",
  114. "title": "div.content_read h1",
  115. "toc": "#list dd a",
  116. "tocJump": 0,
  117. "txt": "#content",
  118. "filter": [".ywtop", ".header", ".nav", ".bottem1", ".lm", "#page_set"],
  119. "txtfilter": [],
  120. "search": async (keywords, baseurl) => {
  121. let links = [];
  122. let doc = await rq({
  123. "url": encodeURI(`https://www.biqugetv.com/search.php?keyword=${keywords}`)
  124. }, 6000, "UTF-8");
  125. for (let a of doc.querySelectorAll("h3 a")) {
  126. links.push({ "title": `(biqugetv) ${a.textContent}`, "href": concatURL(baseurl, attr(a, "href")) });
  127. }
  128. return links;
  129. }
  130. },
  131. {
  132. "desc": "dshfood",
  133. "url": "https://www.dshfood.net/",
  134. "main": ".box_con",
  135. "title": "div.content_read h1",
  136. "toc": "#list dd a",
  137. "tocJump": 9,
  138. "txt": "#content",
  139. "filter": [".ywtop", ".header", ".nav", ".bottem1", "#page_set", "#content>div"],
  140. "txtfilter": ["笔趣阁"],
  141. "nodash": true,
  142. "funcFilter": () => document.querySelectorAll("img")
  143. .forEach(e => { var _a; return (_a = e.parentElement) === null || _a === void 0 ? void 0 : _a.remove(); }),
  144. "search": async (keywords, baseurl) => {
  145. let links = [];
  146. let doc = await rq({
  147. "method": "POST",
  148. "headers": {
  149. "Content-Type": "application/x-www-form-urlencoded",
  150. "referer": "https://www.dshfood.net/so/"
  151. },
  152. "url": "https://www.dshfood.net/so/",
  153. // 鉴于使用了 GBK 进行编码,不能再使用 URLSearchParams
  154. "data": `?searchtype=articlename&searchkey=${$URL.encode(keywords)}&submit=`
  155. }, 6000, "GBK");
  156. for (let a of doc.querySelectorAll(".line a.blue")) {
  157. links.push({ "title": `(dshfood) ${a.textContent}`, "href": concatURL(baseurl, attr(a, "href")) });
  158. }
  159. return links;
  160. }
  161. }
  162. ],
  163. "states": {
  164. "fontSize": 16,
  165. "lineHeight": 16 * lineHeight,
  166. "toc": false,
  167. "flow": false
  168. },
  169. "style": `
  170. body {
  171. background-color: #EAEAEF !important;
  172. }
  173.  
  174. .bqg.inject.win {
  175. width: 55vw !important;
  176. min-width: 600px;
  177. border: 2px double gray !important;
  178. border-radius: 8px;
  179. }
  180.  
  181. .bqg.inject.txt {
  182. font-family: Calibri,'${defaultFont}',serif!important;
  183. background-color: #EAEAEF !important;
  184. padding: 0.5em 1em !important;
  185. margin: 0.5em auto !important;
  186. width: auto !important;
  187. white-space: pre-wrap;
  188. }
  189.  
  190. .bqg.inject.title {
  191. color: black;
  192. background-color: #EAEAEF;
  193. font-family: Calibri,'${defaultFont}',serif!important;
  194. cursor: pointer !important;
  195. }
  196.  
  197. .bqg.inject.title:hover {
  198. color: #0258d8 !important;
  199. }
  200. .hq.inject.toc {
  201. font-family: Calibri,sans-serif;
  202. width: 275px;
  203. position: fixed;
  204. top: 30px;
  205. left: 8px;
  206. /*目录默认是关闭的*/
  207. transform: translateX(-300px);
  208. opacity: 0;
  209. padding: 5px;
  210. display: flex;
  211. flex-flow: column;
  212. box-shadow: #7b7b7b 5px 4px 5px;
  213. transition-property: transform, box-shadow, opacity;
  214. transition-duration: .5s;
  215. transition-timing-function:cubic-bezier(0.35, 1.06, 0.83, 0.99);
  216. background: rgb(246 246 246 / 60%);
  217. backdrop-filter: blur(2px);
  218. border-radius: 8px;
  219. }
  220.  
  221. .hq.inject ul {
  222. height: 280px;
  223. width: 100%;
  224. /*offsetTop 计算需要*/
  225. position:relative;
  226. overflow: auto;
  227. }
  228.  
  229. .hq.inject ul li {
  230. cursor: pointer;
  231. margin: 2px;
  232. width: 95%;
  233. padding: 1px 4px;
  234. font-size: 12px;
  235. border-radius: 4px;
  236. }
  237.  
  238. .hq.inject ul li:hover {
  239. background: #0258d8;
  240. color: #f6f6f6;
  241. }
  242.  
  243. .hq.inject.toc>h3 {
  244. font-size: 1.1rem;
  245. font-weight: bold;
  246. border-radius: 2px;
  247. align-self: center;
  248. cursor: pointer;
  249. margin: 4px 0 8px 0;
  250. }
  251.  
  252. .hq.inject.toc>h3:hover {
  253. color: #ffa631 !important;
  254. }
  255.  
  256. .hq.inject.search {
  257. font-family: Calibri,sans-serif;
  258. width: 275px;
  259. position: fixed;
  260. top: 30px;
  261. padding: 5px;
  262. display: flex;
  263. flex-flow: column;
  264. transition: right 0.5s cubic-bezier(0.35, 1.06, 0.83, 0.99);
  265. background: rgb(246 246 246 / 60%);
  266. border-radius: 8px;
  267. }
  268.  
  269. .hq.inject.search input {
  270. margin: 8px auto;
  271. width: 95%;
  272. }
  273. `
  274. };
  275. // 查询已经保存的字体信息
  276. let savedStates = localStorage.getItem("bqg_cfg");
  277. // 检查是否存在已有设置且和当前版本相符
  278. let states;
  279. if (savedStates === null) {
  280. states = C.states;
  281. console.warn("当前状态已保存");
  282. }
  283. else {
  284. let cfg = JSON.parse(savedStates);
  285. let defaultStates = Object.keys(C.states);
  286. let cfg_ = Object.keys(cfg);
  287. let useSaved = true;
  288. // 检查键是否匹配
  289. if (defaultStates.length == cfg_.length) {
  290. for (let key of Object.keys(cfg)) {
  291. if (!defaultStates.includes(key)) {
  292. useSaved = false;
  293. break;
  294. }
  295. }
  296. }
  297. else {
  298. useSaved = false;
  299. }
  300. if (useSaved) {
  301. states = cfg;
  302. }
  303. else {
  304. states = C.states;
  305. console.warn("检测到版本变化,状态已重置");
  306. }
  307. }
  308. // 检测当前的网址,应用对应的设置
  309. let tmp = C.sites.filter(site => document.URL.includes(site.desc));
  310. if (tmp.length == 0) {
  311. console.warn("没有匹配的设置,脚本已终止!");
  312. return;
  313. }
  314. let currentSite = tmp[0];
  315. // 完成样式注入
  316. GM_addStyle(C.style);
  317. /**
  318. * 保存交互式状态
  319. */
  320. function saveStates() {
  321. localStorage.setItem("bqg_cfg", JSON.stringify(states));
  322. }
  323. /**
  324. * 上一章,同时移除所有 flow 拼接结果
  325. */
  326. function prevChapter() {
  327. var _a;
  328. (_a = fd(document, "a", "上一")) === null || _a === void 0 ? void 0 : _a.click();
  329. }
  330. /**
  331. * 下一章,同时移除所有 flow 拼接结果
  332. */
  333. function nextChapter() {
  334. var _a;
  335. (_a = fd(document, "a", "下一")) === null || _a === void 0 ? void 0 : _a.click();
  336. }
  337. /**
  338. * 异步,向下拼页
  339. * 绑定到事件上时务必注意重复触发的情况
  340. */
  341. async function concatNextCh() {
  342. var _a;
  343. let next = fd(document, "a", "下一");
  344. let prev = fd(document, "a", "上一");
  345. let currentText = fd(document, currentSite.txt);
  346. try {
  347. let doc = await rq({ url: next === null || next === void 0 ? void 0 : next.href });
  348. let text = fd(doc, currentSite.txt);
  349. // console.log(text.textContent)
  350. // 更好的性能
  351. currentText.insertAdjacentHTML("beforeend", "<br><hr style='border: unset;border-top: 1px solid gray; margin: ${states.lineHeight}px 0'>");
  352. currentText.insertAdjacentText("beforeend", txtFilter((_a = text.innerText) !== null && _a !== void 0 ? _a : "文本过滤错误", /(?![a-zA-Z0-9!.'"])\s+/));
  353. // /id/xxx_1.html -> /id/xxx_1
  354. let href = attr(next, "href").replace(/\.html$/, "");
  355. // 重新渲染目录,currentBookToc 不可能为 null
  356. renderTOC(JSON.parse(currentBookToc), ul, href);
  357. // 重设上一页和下一页按钮的链接
  358. prev.href = fd(doc, "a", "上一").href;
  359. next.href = fd(doc, "a", "下一").href;
  360. }
  361. catch (error) {
  362. currentText.innerText = currentText.innerText.concat("\n\n\t获取下一页错误,上下滚动以重新获取");
  363. }
  364. }
  365. // 目录切换
  366. function switchToc(open) {
  367. let toc = fd(document, ".hq.inject.toc");
  368. if (open) {
  369. toc.style.transform = "translateX(0)";
  370. toc.style.opacity = "1";
  371. toc.style.boxShadow = "box-shadow: #7b7b7b 5px 3px 4px 0px;";
  372. states.toc = true;
  373. }
  374. else {
  375. toc.style.transform = "translateX(-300px)";
  376. toc.style.opacity = "0";
  377. toc.style.boxShadow = "box-shadow: #7b7b7b 5px 2px 0px 0px;";
  378. states.toc = false;
  379. }
  380. saveStates();
  381. }
  382. // 目录开关
  383. function toggleToc() {
  384. if (states.toc) {
  385. switchToc(false);
  386. }
  387. else {
  388. switchToc(true);
  389. }
  390. }
  391. /**
  392. * 根据 site 中的条件进行过滤,同时将缩进统一
  393. *
  394. * @param itxt 需要过滤的,innerText 通用性最好
  395. * @param delim 默认的切分点,从网页解析得到的内容和 ajax 获取到的内容切分点不一致
  396. * @returns 过滤后字符串
  397. */
  398. function txtFilter(itxt, delim = /\n/g) {
  399. var _a;
  400. // innerText 相对于 textContent 保留了视觉上的换行(块的换行)
  401. return (_a = itxt === null || itxt === void 0 ? void 0 : itxt.split(delim)) === null || _a === void 0 ? void 0 : _a.filter(line => {
  402. if (/^\s*$/.test(line))
  403. return false;
  404. // 去除白行和包含的关键字
  405. for (const keyword of currentSite.txtfilter) {
  406. if (line.includes(keyword)) {
  407. return false;
  408. }
  409. }
  410. return true;
  411. }).map(line => `${" ".repeat(2)}${line.trim()}`).join("\n\n");
  412. }
  413. if (states.flow) {
  414. // 变相 throttle 一下不然顶不住
  415. let loading = false;
  416. document.onscroll = async (_) => {
  417. if (!loading && chkBoundry(true, window.innerHeight * 0.75)) {
  418. loading = true;
  419. // 意思是上一次拼页完过1.5秒才允许继续拼页,避免在加载下一页时反复调用拼页函数
  420. // 效果比固定延迟要稳定
  421. await concatNextCh();
  422. setTimeout(() => { loading = false; }, 1500);
  423. }
  424. };
  425. }
  426. // 对可变部分产生影响
  427. let doInject = function () {
  428. var _a;
  429. // 执行元素过滤
  430. currentSite.filter.forEach(filter => { var _a; return (_a = document.querySelectorAll(filter)) === null || _a === void 0 ? void 0 : _a.forEach(ele => ele.remove()); });
  431. // 执行自定义过滤
  432. if (currentSite.funcFilter) {
  433. currentSite.funcFilter();
  434. }
  435. // 应用已经保存的状态
  436. let textWin = fd(document, currentSite.txt);
  437. textWin.setAttribute("style", `font-size:${states.fontSize}px;line-height:${states.lineHeight}px`);
  438. textWin.classList.add("bqg", "inject", "txt");
  439. // 执行文字过滤
  440. textWin.textContent = txtFilter((_a = textWin.innerText) !== null && _a !== void 0 ? _a : "文本过滤错误");
  441. let mainWin = fd(document, currentSite.main);
  442. mainWin.classList.add("bqg", "inject", "win");
  443. let title = fd(document, currentSite.title);
  444. title.title = "点击显示目录";
  445. title.classList.add("bqg", "inject", "title");
  446. title.onclick = (ev) => {
  447. toggleToc();
  448. // 避免跳到上一章
  449. // 比下面的更为具体,所以有效。
  450. ev.stopPropagation();
  451. };
  452. // 阻止双击事件被捕获(双击会回到顶部)
  453. document.body.ondblclick = (ev) => ev.stopImmediatePropagation();
  454. document.body.onclick = (ev) => {
  455. let root = document.documentElement;
  456. let winHeight = window.innerHeight;
  457. // 下半屏单击下滚,反之上滚
  458. if (ev.clientY > root.clientHeight / 2) {
  459. if (chkBoundry() && !states.flow)
  460. nextChapter();
  461. window.scrollBy({ top: (window.innerHeight - lineHeight) * 1 });
  462. }
  463. else {
  464. if (chkBoundry(false)) {
  465. prevChapter();
  466. }
  467. window.scrollBy({ top: (window.innerHeight - lineHeight) * -1 });
  468. }
  469. };
  470. document.body.onkeydown = (ev) => {
  471. switch (ev.key) {
  472. case "-":
  473. states.fontSize -= 2;
  474. textWin.style.fontSize = `${states.fontSize}px`;
  475. states.lineHeight = states.fontSize * lineHeight;
  476. textWin.style.lineHeight = `${states.lineHeight}px`;
  477. saveStates();
  478. break;
  479. case "=":
  480. states.fontSize += 2;
  481. textWin.style.fontSize = `${states.fontSize}px`;
  482. states.lineHeight = states.fontSize * lineHeight;
  483. textWin.style.lineHeight = `${states.lineHeight}px`;
  484. saveStates();
  485. break;
  486. case "j":
  487. if (chkBoundry() && !states.flow) {
  488. nextChapter();
  489. }
  490. else {
  491. window.scrollBy({ top: window.innerHeight - states.lineHeight });
  492. }
  493. break;
  494. case "k":
  495. // 考虑在 flow 模式下也允许上一章
  496. if (chkBoundry(false) && !states.flow) {
  497. prevChapter();
  498. }
  499. else {
  500. window.scrollBy({ top: -1 * (window.innerHeight - states.lineHeight) });
  501. }
  502. break;
  503. case "h":
  504. prevChapter();
  505. break;
  506. case "l":
  507. nextChapter();
  508. break;
  509. case "t":
  510. toggleToc();
  511. break;
  512. case "s":
  513. toggleSearch();
  514. break;
  515. case "f":
  516. states.flow = !states.flow;
  517. saveStates();
  518. break;
  519. default:
  520. break;
  521. }
  522. };
  523. };
  524. // 先调用一次,后面是有变化时才会触发,避免有时无法起作用
  525. doInject();
  526. // 强力覆盖
  527. new MutationObserver((_, ob) => {
  528. doInject();
  529. }).observe(document.body, { childList: true });
  530. // 添加目录
  531. let toc = document.createElement("div");
  532. toc.className = "hq inject toc";
  533. toc.onclick = ev => ev.stopPropagation();
  534. // 已保存状态读取
  535. document.body.append(toc);
  536. if (states.toc)
  537. switchToc(true);
  538. // 目录状态指示灯
  539. let pointer = document.createElement("h3");
  540. // 当然也可以靠不同类名实现
  541. let pointerColors = { "loaded": "#afdd22", "loading": "#ffa631", "unload": "#ed5736" };
  542. pointer.title = "点击以重新加载目录";
  543. pointer.innerHTML = "目<span style='display: inline-block;width: 1em'></span>录";
  544. pointer.style.cursor = "pointer";
  545. pointer.style.color = pointerColors.unload;
  546. toc.append(pointer);
  547. // 目录列表
  548. let ul = document.createElement("ul");
  549. toc.append(ul);
  550. /**
  551. * 从源渲染目录到指定元素
  552. *
  553. * @param toc 目录源
  554. * @param ul 容器
  555. * @param href 定位链接,格式 http://host/id/chp.html 中最短为 /id/chp 部分
  556. */
  557. function renderTOC(toc, ul, href) {
  558. var _a;
  559. // 清空旧内容
  560. ul.innerHTML = "";
  561. let current = null;
  562. // 进度计数器
  563. let counter = 1;
  564. for (let lnk of toc) {
  565. let li = document.createElement("li");
  566. li.textContent = lnk.title;
  567. // 根据传入的 href 是否包含目录中的链接来判定,因为有的网站包含子页面 XXXX_1.html 形式
  568. // 比对时标准目录链接 lnk: /id/chp.html 之中,仅取用 chp
  569. let last = (_a = lnk.href.replace(".html", "")) !== null && _a !== void 0 ? _a : "";
  570. if (current == null && href.includes(last)) {
  571. li.innerHTML = `${lnk.title}<span style="flex: 1;"></span>${(counter / toc.length * 100).toFixed(1)}%`;
  572. current = li;
  573. }
  574. li.onclick = (ev) => {
  575. document.location.href = lnk.href;
  576. ev.stopPropagation();
  577. };
  578. ul.append(li);
  579. counter++;
  580. }
  581. // 渲染完修改指示灯状态
  582. pointer.style.color = pointerColors.loaded;
  583. // 滚动到当前位置,并高亮
  584. if (current !== null) {
  585. current.setAttribute("style", "display:flex;font-weight:bold;background: #0258d8;color: #f6f6f6;");
  586. ul.scrollTo({ top: current.offsetTop - 130 });
  587. }
  588. }
  589. /**
  590. * 获取目录信息
  591. *
  592. * @param currentBookLink 当前书的链接,用作存储的键
  593. * @param pointer 指示灯,在需要的时候修改状态
  594. */
  595. async function fetchTOC(currentBookLink, pointer) {
  596. var _a;
  597. // 修改指示灯状态
  598. pointer.style.color = pointerColors.loading;
  599. try {
  600. let doc = await rq({ url: currentBookLink });
  601. let tocs = doc.querySelectorAll(currentSite.toc);
  602. let data = [];
  603. // 序列化存储准备
  604. for (let link of tocs) {
  605. // 使用字面意义上的链接 /chapter.html 而不是 http://**/id/chapter.html 以减小存储量
  606. data.push({ "title": (_a = link.textContent) !== null && _a !== void 0 ? _a : "", "href": attr(link, "href") });
  607. }
  608. if (currentSite.tocJump)
  609. data = data.slice(currentSite.tocJump);
  610. // 缓存目录信息
  611. let stdata = JSON.stringify(data);
  612. sessionStorage.setItem(currentBookLink, stdata);
  613. // 更新变量,避免章节拼接时以为找不到
  614. currentBookToc = stdata;
  615. renderTOC(data, ul, href);
  616. }
  617. catch (_) {
  618. pointer.style.color = pointerColors.unload;
  619. }
  620. }
  621. let source = document.URL.split("/");
  622. source.pop();
  623. // 用来定位的 url
  624. let href = document.URL.replace(/\.html$/, "");
  625. // 最后加斜杠保险
  626. let currentBook = source.join("/");
  627. if (!currentSite.nodash) {
  628. currentBook += "/";
  629. }
  630. let currentBookToc = sessionStorage.getItem(currentBook);
  631. if (currentBookToc === null) {
  632. fetchTOC(currentBook, pointer);
  633. }
  634. else {
  635. renderTOC(JSON.parse(currentBookToc), ul, href);
  636. }
  637. // 单击指示灯刷新目录缓存
  638. pointer.onclick = _ => fetchTOC(currentBook, pointer);
  639. // 添加聚合搜索
  640. let searchBox = document.createElement("div");
  641. searchBox.onclick = ev => ev.stopPropagation();
  642. searchBox.onkeydown = ev => ev.stopPropagation();
  643. searchBox.className = "hq inject search";
  644. searchBox.style.right = "-300px";
  645. searchBox.innerHTML = `
  646. <input id="insearch" type="search" placeholder="至少输入两个字"/>
  647. <span style="align-self:center;margin-bottem: 4px;color: ${pointerColors.loaded}">已就绪</span>
  648. `;
  649. document.body.append(searchBox);
  650. let inputBox = fd(searchBox, "#insearch");
  651. let search_ul = document.createElement("ul");
  652. searchBox.append(search_ul);
  653. let search_pointer = fd(searchBox, "#insearch~span");
  654. // debounce 一下不然顶不住
  655. let timer = null;
  656. inputBox.oninput = _ => {
  657. if (timer !== null)
  658. clearTimeout(timer);
  659. timer = setTimeout(async () => {
  660. var _a, _b;
  661. // 放外面也可
  662. if (((_a = inputBox) === null || _a === void 0 ? void 0 : _a.value.length) < 2)
  663. return;
  664. // 更新指示灯
  665. search_pointer.textContent = `正在搜索:${inputBox.value}`;
  666. search_pointer.style.color = pointerColors.loading;
  667. let requests = [];
  668. let others = [{ "title": "没有搜索结果,也可以看看:", "href": "#" }];
  669. for (let s of C.sites) {
  670. if (s.search !== undefined) {
  671. // 搜索开始
  672. requests.push(s.search((_b = inputBox) === null || _b === void 0 ? void 0 : _b.value, s.url));
  673. }
  674. else {
  675. others.push({ "title": s.desc, "href": s.url });
  676. }
  677. }
  678. let result_count = 0, failed = 0;
  679. let list = await Promise.allSettled(requests);
  680. // 获取结果后清空旧内容
  681. search_ul.innerHTML = "";
  682. for (let site of list) {
  683. if (site.status === "fulfilled") {
  684. for (let lnk of site.value) {
  685. let li = document.createElement("li");
  686. li.textContent = lnk.title.trim();
  687. li.onclick = ev => GM_openInTab(lnk.href, { active: true });
  688. search_ul.append(li);
  689. result_count++;
  690. }
  691. }
  692. else {
  693. failed++;
  694. }
  695. }
  696. // 处理一下没有结果的情况,把没有实现 search 的网站摆上去
  697. if (result_count === 0) {
  698. for (let o of others) {
  699. let li = document.createElement("li");
  700. li.textContent = o.title;
  701. li.onclick = ev => GM_openInTab(o.href, { active: true });
  702. search_ul.append(li);
  703. }
  704. }
  705. // 更新指示
  706. search_pointer.textContent = `搜索完成:${result_count} 条结果 [${failed} 错误]`;
  707. search_pointer.style.color = pointerColors.loaded;
  708. }, 1000);
  709. };
  710. // 搜索框开关
  711. function toggleSearch() {
  712. if (parseInt(searchBox.style.right) < 0) {
  713. searchBox.style.right = "8px";
  714. }
  715. else {
  716. searchBox.style.right = "-300px";
  717. }
  718. }
  719. /*
  720. 以下是工具函数
  721. */
  722. /**
  723. * 发起请求
  724. *
  725. * @param details 油猴标准请求格式,onload,onerror,responseType 会被忽略
  726. * @param timeout 超时时间 默认:5000
  727. * @param encoding 请求数据的编码 默认:当前所在页面的编码
  728. * @returns Promise<Document>
  729. */
  730. function rq(details, timeout = 5000, encoding) {
  731. // 自动探测一手
  732. if (!encoding)
  733. encoding = document.characterSet;
  734. return new Promise((res, rej) => {
  735. details.onerror = rej;
  736. details.ontimeout = rej;
  737. details.timeout = timeout;
  738. details.responseType = "arraybuffer";
  739. details.onload = resp => {
  740. if (resp.status != 200)
  741. rej();
  742. let decoder = new TextDecoder(encoding);
  743. res(new DOMParser()
  744. .parseFromString(decoder.decode(resp.response), "text/html"));
  745. };
  746. GM_xmlhttpRequest(details);
  747. });
  748. }
  749. /**
  750. * 返回符合条件的第一个元素
  751. *
  752. * @param doc 被查找的文档
  753. * @param selector 选择器
  754. * @param text 可选 元素的文本(子字符串)
  755. * @returns 符合条件的元素
  756. */
  757. function fd(doc, selector, text) {
  758. var _a;
  759. if (text) {
  760. for (let e of doc.querySelectorAll(selector)) {
  761. if ((_a = e.textContent) === null || _a === void 0 ? void 0 : _a.includes(text)) {
  762. return e;
  763. }
  764. }
  765. }
  766. else {
  767. return doc.querySelector(selector);
  768. }
  769. return null;
  770. }
  771. /**
  772. * 拼接 URL
  773. *
  774. * @param host 网站域名
  775. * @param path 一般是链接之中的相对路径
  776. * @returns 完整的 URL
  777. */
  778. function concatURL(host, path) {
  779. let url = new URL(host);
  780. url.pathname = path;
  781. return url.toString();
  782. }
  783. /**
  784. * 如果是 a 标签,且想要获取字面上的 href,必须使用此方法,不可以用 a.href
  785. *
  786. * @param ele 标签名
  787. * @param attr 属性名
  788. * @returns 属性值
  789. */
  790. function attr(ele, attr) {
  791. var _a;
  792. return (_a = ele.getAttribute(attr)) !== null && _a !== void 0 ? _a : "";
  793. }
  794. /**
  795. * 检查当前位置是否处于边界
  796. *
  797. * @param bottom 是否检查到达底部,否则检查是否处于顶部
  798. * @param range 距离底部多少,默认是 0(最底部)
  799. * @returns boolean
  800. */
  801. function chkBoundry(bottom = true, range = 0) {
  802. let root = document.documentElement;
  803. let winHeight = window.innerHeight;
  804. if (bottom) {
  805. return (root.scrollTop + winHeight + range >= root.scrollHeight);
  806. }
  807. else {
  808. return (root.scrollTop == 0);
  809. }
  810. }
  811. })();

QingJ © 2025

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