linovelib

优化 linovelib 阅读体验

  1. // ==UserScript==
  2. // @name linovelib
  3. // @namespace https://github.com/IronKinoko/userscripts/tree/master/packages/linovelib
  4. // @version 1.4.8
  5. // @license MIT
  6. // @description 优化 linovelib 阅读体验
  7. // @author IronKinoko
  8. // @match https://www.linovelib.com/*
  9. // @match https://w.linovelib.com/*
  10. // @match https://www.bilinovel.com/*
  11. // @icon https://www.google.com/s2/favicons?domain=w.linovelib.com
  12. // @grant none
  13. // @noframes
  14. // ==/UserScript==
  15. (function () {
  16. 'use strict';
  17.  
  18. function isMobile() {
  19. const re = /iphone|ipad|ipod|android|webos|blackberry|windows phone/i;
  20. const ua = navigator.userAgent;
  21. return re.test(ua);
  22. }
  23.  
  24. function normalizeKeyEvent(e) {
  25. const SPECIAL_KEY_EN = "`-=[]\\;',./~!@#$%^&*()_+{}|:\"<>?".split("");
  26. 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("");
  27. let key = e.key;
  28. if (e.code === "Space") {
  29. key = "Space";
  30. }
  31. if (/^[a-z]$/.test(key)) {
  32. key = key.toUpperCase();
  33. } else if (SPECIAL_KEY_ZH.includes(key)) {
  34. key = SPECIAL_KEY_EN[SPECIAL_KEY_ZH.indexOf(key)];
  35. }
  36. let keyArr = [];
  37. e.ctrlKey && keyArr.push("ctrl");
  38. e.metaKey && keyArr.push("meta");
  39. e.shiftKey && !SPECIAL_KEY_EN.includes(key) && keyArr.push("shift");
  40. e.altKey && keyArr.push("alt");
  41. if (!/Control|Meta|Shift|Alt/i.test(key))
  42. keyArr.push(key);
  43. keyArr = [...new Set(keyArr)];
  44. return keyArr.join("+");
  45. }
  46. function keybind(keys, keydown, keyup) {
  47. const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
  48. keys = keys.filter((key) => !key.includes(isMac ? "ctrl" : "meta"));
  49. function createProcess(callback) {
  50. return function(e) {
  51. var _a;
  52. if (((_a = document.activeElement) == null ? void 0 : _a.tagName) === "INPUT")
  53. return;
  54. const normalizedKey = normalizeKeyEvent(e).toLowerCase();
  55. for (const key of keys) {
  56. if (key.toLowerCase() === normalizedKey)
  57. callback(e, key);
  58. }
  59. };
  60. }
  61. window.addEventListener("keydown", createProcess(keydown));
  62. if (keyup)
  63. window.addEventListener("keyup", createProcess(keyup));
  64. }
  65.  
  66. function matcher(source, regexp) {
  67. if (typeof regexp === "string")
  68. return source.includes(regexp);
  69. return !!source.match(regexp);
  70. }
  71. function router(config) {
  72. const opts = {
  73. domain: "",
  74. routes: []
  75. };
  76. if ("routes" in config) {
  77. opts.domain = config.domain;
  78. opts.routes = config.routes;
  79. } else {
  80. opts.routes = Array.isArray(config) ? config : [config];
  81. }
  82. if (opts.domain) {
  83. const domains = Array.isArray(opts.domain) ? opts.domain : [opts.domain];
  84. const match = domains.some(
  85. (domain) => matcher(window.location.origin, domain)
  86. );
  87. if (!match)
  88. return;
  89. }
  90. const pathSource = window.location.pathname + window.location.search + window.location.hash;
  91. if (typeof opts.routes === "function") {
  92. opts.routes();
  93. return;
  94. }
  95. const routes = Array.isArray(opts.routes) ? opts.routes : [opts.routes];
  96. const runRoutes = routes.filter((route) => {
  97. let match = true;
  98. if (route.path) {
  99. match = matcher(pathSource, route.path);
  100. }
  101. if (route.pathname) {
  102. match = matcher(window.location.pathname, route.pathname);
  103. }
  104. if (route.search) {
  105. match = matcher(window.location.search, route.search);
  106. }
  107. if (route.hash) {
  108. match = matcher(window.location.hash, route.hash);
  109. }
  110. return match;
  111. });
  112. runRoutes.forEach((route) => {
  113. if (route.setup)
  114. route.setup();
  115. });
  116. function run() {
  117. runRoutes.forEach((route) => {
  118. if (route.run)
  119. route.run();
  120. });
  121. }
  122. if (window.document.readyState === "complete") {
  123. run();
  124. } else {
  125. window.addEventListener("load", run);
  126. }
  127. }
  128.  
  129. async function main$1() {
  130. if (!window.ReadTools)
  131. return;
  132. resetPageEvent();
  133. if (isMobile())
  134. injectMovePageEvent();
  135. else
  136. injectShortcuts();
  137. }
  138. async function fixADBlock() {
  139. var _a;
  140. const res = await fetch(window.location.href);
  141. const text = await res.text();
  142. const html = new DOMParser().parseFromString(text, "text/html");
  143. const volumes = html.querySelector("#volumes");
  144. if (!volumes)
  145. return;
  146. (_a = document.querySelector("#volumes")) == null ? void 0 : _a.replaceWith(volumes);
  147. try {
  148. document.getElementById(
  149. "bookmarkX"
  150. ).innerHTML = `<div class="chapter-bar">\u9605\u8BFB\u8FDB\u5EA6</div><span class="historyChapter"><a href="/novel/${targetRecord.articleid}/${targetRecord.chapterid}_${targetRecord.page}.html" class="chapter-li-a "><span class="chapter-index blue">${targetRecord.chaptername}</span></a></span>`;
  151. } catch (error) {
  152. }
  153. }
  154. function resetPageEvent() {
  155. const $body = document.body;
  156. $body.onclick = (e) => {
  157. const toolsId = ["#toptools", "#bottomtools", "#readset"];
  158. if (toolsId.some(
  159. (id) => {
  160. var _a;
  161. return (_a = document.querySelector(id)) == null ? void 0 : _a.contains(e.target);
  162. }
  163. )) {
  164. return;
  165. }
  166. window.ReadPages.PageClick();
  167. };
  168. }
  169. function injectMovePageEvent() {
  170. let left, startX, startY, diffX, startTime, isMoved, direction;
  171. const $page = document.getElementById("apage");
  172. const isDisabled = (e) => {
  173. return window.ReadTools.pagemid != 1 || e.touches.length > 1 || window.visualViewport && window.visualViewport.scale !== 1 || window.getSelection() && window.getSelection().toString().length > 0;
  174. };
  175. window.addEventListener("touchstart", (e) => {
  176. if (isDisabled(e))
  177. return;
  178. left = parseFloat($page.style.left.replace("px", "")) || 0;
  179. startX = e.touches[0].clientX;
  180. startY = e.touches[0].clientY;
  181. startTime = Date.now();
  182. isMoved = false;
  183. direction = "";
  184. });
  185. window.addEventListener(
  186. "touchmove",
  187. (e) => {
  188. if (isDisabled(e))
  189. return;
  190. isMoved = true;
  191. diffX = e.touches[0].clientX - startX;
  192. let diffY = e.touches[0].clientY - startY;
  193. if (direction === "") {
  194. direction = Math.abs(diffX) > Math.abs(diffY) ? "x" : "y";
  195. }
  196. if (direction === "x") {
  197. e.preventDefault();
  198. $page.style.left = left + diffX + "px";
  199. $page.style.transition = "initail";
  200. }
  201. },
  202. { passive: false }
  203. );
  204. window.addEventListener("touchend", (e) => {
  205. if (isDisabled(e))
  206. return;
  207. if (!isMoved || direction === "y")
  208. return;
  209. const diffTime = Date.now() - startTime;
  210. const threshold = diffTime < 300 ? 10 : document.documentElement.clientWidth * 0.3;
  211. $page.style.transition = "";
  212. if (Math.abs(diffX) > threshold) {
  213. const type = diffX > 0 ? "previous" : "next";
  214. window.ReadPages.ShowPage(type);
  215. if (window.ReadPages.currentPage > window.ReadPages.totalPages || window.ReadPages.currentPage < 1) {
  216. window.ReadPages.ShowPage();
  217. }
  218. } else {
  219. window.ReadPages.ShowPage();
  220. }
  221. });
  222. }
  223. function injectShortcuts() {
  224. keybind(["a", "s", "w", "d", "space", "shift+space"], (e, key) => {
  225. if (window.ReadTools.pagemid != 1)
  226. return;
  227. switch (key) {
  228. case "shift+space":
  229. case "a":
  230. case "w":
  231. window.ReadPages.ShowPage("previous");
  232. break;
  233. case "space":
  234. case "d":
  235. case "s":
  236. window.ReadPages.ShowPage("next");
  237. break;
  238. }
  239. });
  240. }
  241.  
  242. function main() {
  243. removeSelectEvent();
  244. if (document.body.id === "readbg") {
  245. injectEvent();
  246. }
  247. }
  248. function removeSelectEvent() {
  249. const dom = document.createElement("style");
  250. dom.innerHTML = `* { user-select: initial !important; }`;
  251. document.body.append(dom);
  252. document.body.removeAttribute("onselectstart");
  253. }
  254. function injectEvent() {
  255. const scripts = Array.from(document.scripts);
  256. const script = scripts.find(
  257. (script2) => script2.innerHTML.includes('prevpage="')
  258. );
  259. if (!script)
  260. return;
  261. const res = script.innerHTML.match(
  262. new RegExp('prevpage="(?<pre>.*?)";.*nextpage="(?<next>.*?)";')
  263. );
  264. if (!(res == null ? void 0 : res.groups))
  265. return;
  266. const { pre, next } = res.groups;
  267. keybind(
  268. ["w", "s", "a", "d"],
  269. (e, key) => {
  270. switch (key) {
  271. case "w":
  272. case "s":
  273. const direction = key === "w" ? -1 : 1;
  274. if (e.repeat)
  275. scroll.start(direction * 15);
  276. else
  277. window.scrollBy({ behavior: "smooth", top: direction * 200 });
  278. break;
  279. case "a":
  280. case "d":
  281. window.location.pathname = key === "a" ? pre : next;
  282. break;
  283. }
  284. },
  285. (e, key) => {
  286. switch (key) {
  287. case "w":
  288. case "s":
  289. scroll.stop();
  290. break;
  291. }
  292. }
  293. );
  294. }
  295. const scroll = (() => {
  296. let handle;
  297. function stop() {
  298. if (!handle)
  299. return;
  300. cancelAnimationFrame(handle);
  301. handle = void 0;
  302. }
  303. function start(step) {
  304. if (handle)
  305. return;
  306. function animate() {
  307. handle = requestAnimationFrame(animate);
  308. window.scrollBy({ top: step });
  309. }
  310. handle = requestAnimationFrame(animate);
  311. }
  312. return { start, stop };
  313. })();
  314.  
  315. 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}}
  316.  
  317. 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}";
  318. n(css,{});
  319.  
  320. document.body.classList.add("k-wrapper");
  321. router({ domain: ["//www.linovelib.com"], routes: [{ run: main }] });
  322. router({
  323. domain: ["//w.linovelib.com", "//www.bilinovel.com"],
  324. routes: [
  325. { run: main$1 },
  326. {
  327. pathname: /(\/novel\/.*\/catalog)|(\/download\/.*\.html)/,
  328. run: fixADBlock
  329. }
  330. ]
  331. });
  332.  
  333. })();