linovelib

优化 linovelib 阅读体验

当前为 2023-04-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name linovelib
  3. // @namespace https://github.com/IronKinoko/userscripts/tree/master/packages/linovelib
  4. // @version 1.4.2
  5. // @license MIT
  6. // @description 优化 linovelib 阅读体验
  7. // @author IronKinoko
  8. // @match https://www.linovelib.com/*
  9. // @match https://w.linovelib.com/*
  10. // @icon https://www.google.com/s2/favicons?domain=w.linovelib.com
  11. // @grant none
  12. // @noframes
  13. // ==/UserScript==
  14. (function () {
  15. 'use strict';
  16.  
  17. function isMobile() {
  18. const re = /iphone|ipad|ipod|android|webos|blackberry|windows phone/i;
  19. const ua = navigator.userAgent;
  20. return re.test(ua);
  21. }
  22.  
  23. function normalizeKeyEvent(e) {
  24. const SPECIAL_KEY_EN = "`-=[]\\;',./~!@#$%^&*()_+{}|:\"<>?".split("");
  25. const SPECIAL_KEY_ZH = "\xB7-=\u3010\u3011\u3001\uFF1B\u2018\uFF0C\u3002/\uFF5E\uFF01@#\xA5%\u2026&*\uFF08\uFF09\u2014+\u300C\u300D\uFF5C\uFF1A\u201C\u300A\u300B\uFF1F".split("");
  26. let key = e.key;
  27. if (e.code === "Space") {
  28. key = "Space";
  29. }
  30. if (/^[a-z]$/.test(key)) {
  31. key = key.toUpperCase();
  32. } else if (SPECIAL_KEY_ZH.includes(key)) {
  33. key = SPECIAL_KEY_EN[SPECIAL_KEY_ZH.indexOf(key)];
  34. }
  35. let keyArr = [];
  36. e.ctrlKey && keyArr.push("ctrl");
  37. e.metaKey && keyArr.push("meta");
  38. e.shiftKey && !SPECIAL_KEY_EN.includes(key) && keyArr.push("shift");
  39. e.altKey && keyArr.push("alt");
  40. if (!/Control|Meta|Shift|Alt/i.test(key))
  41. keyArr.push(key);
  42. keyArr = [...new Set(keyArr)];
  43. return keyArr.join("+");
  44. }
  45. function keybind(keys, keydown, keyup) {
  46. const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
  47. keys = keys.filter((key) => !key.includes(isMac ? "ctrl" : "meta"));
  48. function createProcess(callback) {
  49. return function(e) {
  50. var _a;
  51. if (((_a = document.activeElement) == null ? void 0 : _a.tagName) === "INPUT")
  52. return;
  53. const normalizedKey = normalizeKeyEvent(e).toLowerCase();
  54. for (const key of keys) {
  55. if (key.toLowerCase() === normalizedKey)
  56. callback(e, key);
  57. }
  58. };
  59. }
  60. window.addEventListener("keydown", createProcess(keydown));
  61. if (keyup)
  62. window.addEventListener("keyup", createProcess(keyup));
  63. }
  64.  
  65. function matcher(source, regexp) {
  66. if (typeof regexp === "string")
  67. return source.includes(regexp);
  68. return !!source.match(regexp);
  69. }
  70. function router(config) {
  71. const opts = {
  72. domain: "",
  73. routes: []
  74. };
  75. if ("routes" in config) {
  76. opts.domain = config.domain;
  77. opts.routes = config.routes;
  78. } else {
  79. opts.routes = Array.isArray(config) ? config : [config];
  80. }
  81. if (opts.domain) {
  82. const domains = Array.isArray(opts.domain) ? opts.domain : [opts.domain];
  83. const match = domains.some(
  84. (domain) => matcher(window.location.origin, domain)
  85. );
  86. if (!match)
  87. return;
  88. }
  89. const pathSource = window.location.pathname + window.location.search + window.location.hash;
  90. if (typeof opts.routes === "function") {
  91. opts.routes();
  92. return;
  93. }
  94. const routes = Array.isArray(opts.routes) ? opts.routes : [opts.routes];
  95. routes.forEach((route) => {
  96. let match = true;
  97. if (route.path) {
  98. match = matcher(pathSource, route.path);
  99. }
  100. if (route.pathname) {
  101. match = matcher(window.location.pathname, route.pathname);
  102. }
  103. if (route.search) {
  104. match = matcher(window.location.search, route.search);
  105. }
  106. if (route.hash) {
  107. match = matcher(window.location.hash, route.hash);
  108. }
  109. if (match)
  110. route.run();
  111. });
  112. }
  113.  
  114. async function main$1() {
  115. router([{ pathname: /novel\/\d+\/catalog/, run: injectDownloadSection }]);
  116. if (!window.ReadTools)
  117. return;
  118. resetPageEvent();
  119. if (isMobile())
  120. injectMovePageEvent();
  121. else
  122. injectShortcuts();
  123. }
  124. function injectDownloadSection() {
  125. const bookId = window.location.pathname.match(/\d+/)[0];
  126. document.querySelectorAll("#volumes .chapter-bar").forEach((node, idx) => {
  127. const api = `https://www.zhidianbao.cn:8443/qs_xq_epub/api/catalog/${bookId}/${idx}/sync`;
  128. node.innerHTML = `
  129. <span>${node.textContent}</span>
  130. <button class="download-btn"></button>
  131. <div class="progress">
  132. <div hidden></div>
  133. </div>
  134. `;
  135. const $btn = node.querySelector(".download-btn");
  136. const $progress = node.querySelector(".progress div");
  137. const setProgress = (progress) => {
  138. if (progress) {
  139. $progress.hidden = false;
  140. const { asset, chapter } = progress;
  141. $progress.style.width = (chapter.progress + asset.progress) * 100 / 2 + "%";
  142. } else {
  143. $progress.hidden = true;
  144. $progress.style.width = "0";
  145. }
  146. };
  147. $btn.onclick = () => {
  148. $btn.disabled = true;
  149. (async function fn() {
  150. const res = await fetch(api).then((res2) => res2.json());
  151. setProgress(res.progress);
  152. if (res.code !== 0) {
  153. alert(res.message);
  154. $btn.disabled = false;
  155. } else {
  156. if (res.done) {
  157. window.location.href = new URL(
  158. res.downloadURL,
  159. "https://www.zhidianbao.cn:8443"
  160. ).toString();
  161. setTimeout(() => setProgress(void 0), 100);
  162. $btn.disabled = false;
  163. } else {
  164. setTimeout(fn, 300);
  165. }
  166. }
  167. })();
  168. };
  169. });
  170. }
  171. function resetPageEvent() {
  172. const $body = document.body;
  173. $body.onclick = (e) => {
  174. const toolsId = ["#toptools", "#bottomtools", "#readset"];
  175. if (toolsId.some(
  176. (id) => {
  177. var _a;
  178. return (_a = document.querySelector(id)) == null ? void 0 : _a.contains(e.target);
  179. }
  180. )) {
  181. return;
  182. }
  183. window.ReadPages.PageClick();
  184. };
  185. }
  186. function injectMovePageEvent() {
  187. let left, startX, startY, diffX, startTime, isMoved, direction;
  188. const $page = document.getElementById("apage");
  189. const isDisabled = (e) => {
  190. return window.ReadTools.pagemid != 1 || e.touches.length > 1 || window.visualViewport && window.visualViewport.scale !== 1 || window.getSelection() && window.getSelection().toString().length > 0;
  191. };
  192. window.addEventListener("touchstart", (e) => {
  193. if (isDisabled(e))
  194. return;
  195. left = parseFloat($page.style.left.replace("px", "")) || 0;
  196. startX = e.touches[0].clientX;
  197. startY = e.touches[0].clientY;
  198. startTime = Date.now();
  199. isMoved = false;
  200. direction = "";
  201. });
  202. window.addEventListener(
  203. "touchmove",
  204. (e) => {
  205. if (isDisabled(e))
  206. return;
  207. isMoved = true;
  208. diffX = e.touches[0].clientX - startX;
  209. let diffY = e.touches[0].clientY - startY;
  210. if (direction === "") {
  211. direction = Math.abs(diffX) > Math.abs(diffY) ? "x" : "y";
  212. }
  213. if (direction === "x") {
  214. e.preventDefault();
  215. $page.style.left = left + diffX + "px";
  216. $page.style.transition = "initail";
  217. }
  218. },
  219. { passive: false }
  220. );
  221. window.addEventListener("touchend", (e) => {
  222. if (isDisabled(e))
  223. return;
  224. if (!isMoved || direction === "y")
  225. return;
  226. const diffTime = Date.now() - startTime;
  227. const threshold = diffTime < 300 ? 10 : document.documentElement.clientWidth * 0.3;
  228. $page.style.transition = "";
  229. if (Math.abs(diffX) > threshold) {
  230. const type = diffX > 0 ? "previous" : "next";
  231. window.ReadPages.ShowPage(type);
  232. if (window.ReadPages.currentPage > window.ReadPages.totalPages || window.ReadPages.currentPage < 1) {
  233. window.ReadPages.ShowPage();
  234. }
  235. } else {
  236. window.ReadPages.ShowPage();
  237. }
  238. });
  239. }
  240. function injectShortcuts() {
  241. keybind(["a", "s", "w", "d", "space", "shift+space"], (e, key) => {
  242. if (window.ReadTools.pagemid != 1)
  243. return;
  244. switch (key) {
  245. case "shift+space":
  246. case "a":
  247. case "w":
  248. window.ReadPages.ShowPage("previous");
  249. break;
  250. case "space":
  251. case "d":
  252. case "s":
  253. window.ReadPages.ShowPage("next");
  254. break;
  255. }
  256. });
  257. }
  258.  
  259. function main() {
  260. removeSelectEvent();
  261. if (/novel\/\d+\.html/.test(window.location.pathname)) {
  262. injectDownload();
  263. }
  264. if (document.body.id === "readbg") {
  265. injectEvent();
  266. }
  267. }
  268. function removeSelectEvent() {
  269. const dom = document.createElement("style");
  270. dom.innerHTML = `* { user-select: initial !important; }`;
  271. document.body.append(dom);
  272. document.body.removeAttribute("onselectstart");
  273. }
  274. function injectEvent() {
  275. const scripts = Array.from(document.scripts);
  276. const script = scripts.find(
  277. (script2) => script2.innerHTML.includes('prevpage="')
  278. );
  279. if (!script)
  280. return;
  281. const res = script.innerHTML.match(
  282. new RegExp('prevpage="(?<pre>.*?)";.*nextpage="(?<next>.*?)";')
  283. );
  284. if (!(res == null ? void 0 : res.groups))
  285. return;
  286. const { pre, next } = res.groups;
  287. keybind(
  288. ["w", "s", "a", "d"],
  289. (e, key) => {
  290. switch (key) {
  291. case "w":
  292. case "s":
  293. const direction = key === "w" ? -1 : 1;
  294. if (e.repeat)
  295. scroll.start(direction * 15);
  296. else
  297. window.scrollBy({ behavior: "smooth", top: direction * 200 });
  298. break;
  299. case "a":
  300. case "d":
  301. window.location.pathname = key === "a" ? pre : next;
  302. break;
  303. }
  304. },
  305. (e, key) => {
  306. switch (key) {
  307. case "w":
  308. case "s":
  309. scroll.stop();
  310. break;
  311. }
  312. }
  313. );
  314. }
  315. function injectDownload() {
  316. const bookId = window.location.pathname.split("/").pop().split(".").shift();
  317. const dom = document.querySelector(".fr.link-group");
  318. const a = document.createElement("a");
  319. dom == null ? void 0 : dom.prepend(a);
  320. a.outerHTML = `<a class="all-catalog" href="http://www.zhidianbao.cn:8088/qs_xq_epub?bookId=${bookId}" target="_blank"><em></em>Epub\u4E0B\u8F7D</a>`;
  321. }
  322. const scroll = (() => {
  323. let handle;
  324. function stop() {
  325. if (!handle)
  326. return;
  327. cancelAnimationFrame(handle);
  328. handle = void 0;
  329. }
  330. function start(step) {
  331. if (handle)
  332. return;
  333. function animate() {
  334. handle = requestAnimationFrame(animate);
  335. window.scrollBy({ top: step });
  336. }
  337. handle = requestAnimationFrame(animate);
  338. }
  339. return { start, stop };
  340. })();
  341.  
  342. var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}}
  343.  
  344. var css = "@charset \"UTF-8\";\n.k-wrapper #catelogX .module-header-r {\n min-width: 0;\n}\n.k-wrapper #catelogX .module-header-r .module-header-btn {\n position: static;\n padding: 0;\n}\n.k-wrapper #volumes .chapter-bar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n position: relative;\n}\n.k-wrapper #volumes .chapter-bar::after {\n content: none;\n}\n.k-wrapper #volumes .chapter-bar .download-btn {\n border-radius: 4px;\n background-color: rgba(255, 57, 84, 0.1);\n padding: 4px 12px;\n font-weight: 500;\n color: #ff3955;\n border: 0;\n white-space: nowrap;\n margin-left: 16px;\n}\n.k-wrapper #volumes .chapter-bar .download-btn::after {\n content: \"下载\";\n}\n.k-wrapper #volumes .chapter-bar .download-btn:disabled {\n opacity: 0.5;\n}\n.k-wrapper #volumes .chapter-bar .download-btn:disabled::after {\n content: \"下载中...\";\n}\n.k-wrapper #volumes .chapter-bar .progress,\n.k-wrapper #volumes .chapter-bar .progress > div {\n position: absolute;\n pointer-events: none;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n}\n.k-wrapper #volumes .chapter-bar .progress > div {\n background-color: rgba(255, 57, 84, 0.1);\n transition: all 0.2s linear;\n}";
  345. n(css,{});
  346.  
  347. document.body.classList.add("k-wrapper");
  348. if (window.location.host.includes("www.")) {
  349. main();
  350. } else
  351. main$1();
  352.  
  353. })();