Fishhawk Enhancement

让轻小说机翻站真正好用!

  1. // ==UserScript==;
  2. // @name Fishhawk Enhancement
  3. // @namespace http://tampermonkey.net/
  4. // @version 2024-01-10
  5. // @description 让轻小说机翻站真正好用!
  6. // @author VoltaXTY
  7. // @match https://books.fishhawk.top/*
  8. // @icon http://fishhawk.top/favicon.ico
  9. // @grant none
  10. // ==/UserScript==
  11. //一些CSS,主要用于在线阅读页的单/双栏切换
  12. const WaitUntilSuccess = async (func, args, options = {}) => {
  13. const {isSuccess, interval, count} = {
  14. isSuccess: (result) => result,
  15. interval: 1000,
  16. count: 9999,
  17. ...options,
  18. };
  19.  
  20. let counter = 0;
  21. while(counter++ < count){
  22. try{
  23. const result = await func(...args);
  24. if(isSuccess(result)) return result;
  25. else if(interval > 0) await new Promise(res => setTimeout(_ => res(), interval));
  26. }
  27. catch(err){
  28. console.error(err);
  29. await new Promise(res => setTimeout(_ => res(), interval));
  30. }
  31. }
  32. };
  33. const Fetch = (...args) => {
  34. if(args.length === 1){
  35. return fetch(args[0], {
  36. headers: {
  37. "authorization": "Bearer " + GetAuth(),
  38. }
  39. })
  40. }
  41. else if(args.length === 2){
  42. return fetch(args[0], {
  43. ...args[1],
  44. ...(args[1].headers ? {headers: {...args[1].headers, ...{"authorization": "Bearer " + GetAuth()}}} : {headers: {"authorization" : "Bearer " + GetAuth()}}),
  45. })
  46. }
  47. };
  48. const origin = "https://books.fishhawk.top";
  49. const css =
  50. String.raw`
  51. #chapter-content{
  52. display: grid;
  53. grid-template-columns: 1fr 1fr;
  54. gap: 5px;
  55. }
  56. #chapter-content > *{
  57. grid-column: 1 / 3;
  58. height: 0px;
  59. }
  60. #chapter-content > p.n-p {
  61. grid-column: revert;
  62. height: revert;
  63. margin: 0px;
  64. }
  65. div.n-flex.always-working > button:nth-child(1){
  66. background-color: #18a058;
  67. color: #fff;
  68. }
  69. `;
  70. //插入上面的CSS
  71. const InsertStyleSheet = (style) => {
  72. const s = new CSSStyleSheet();
  73. s.replaceSync(style);
  74. document.adoptedStyleSheets = [...document.adoptedStyleSheets, s];
  75. };
  76. InsertStyleSheet(css);
  77. //调试用函数暴露在这个object里面
  78. window.ujsConsole = {};
  79. //创建新Element的便携函数
  80. const HTML = (tagname, attrs, ...children) => {
  81. if(attrs === undefined) return document.createTextNode(tagname);
  82. const ele = document.createElement(tagname);
  83. if(attrs) for(const [key, value] of Object.entries(attrs)){
  84. if(value === null || value === undefined) continue;
  85. if(key.charAt(0) === "_"){
  86. const type = key.slice(1);
  87. ele.addEventListener(type, value);
  88. }
  89. else if(key === "eventListener"){
  90. for(const listener of value){
  91. ele.addEventListener(listener.type, listener.listener, listener.options);
  92. }
  93. }
  94. else ele.setAttribute(key, value);
  95. }
  96. for(const child of children) if(child) ele.append(child);
  97. return ele;
  98. };
  99. const GetSakuraWorkspace = () => JSON.parse(localStorage.getItem("sakura-workspace"));
  100. const SortWorkspace = (workspace) => (workspace.jobs.sort((job1, job2) => (job1.priority ?? 20) - (job2.priority ?? 20)), workspace);
  101. const SetSakuraWorkspace = (workspace) => {
  102. workspace = SortWorkspace(workspace);
  103. const event = new StorageEvent("storage", {
  104. key: "sakura-workspace",
  105. oldValue: JSON.stringify(GetSakuraWorkspace()),
  106. newValue: JSON.stringify(workspace),
  107. url: window.location.toString(),
  108. storageArea: localStorage,
  109. });
  110. localStorage.setItem("sakura-workspace", JSON.stringify(workspace));
  111. window.dispatchEvent(event);
  112. };
  113. const InsertNewJob = async (tasks, insertPos = 0) => {
  114. const workspace = GetSakuraWorkspace();
  115. if(!(tasks instanceof Array)) tasks = [tasks];
  116. const workspaceTasks = new Set(workspace.jobs.map(job => job.task));
  117. workspace.jobs.splice(insertPos, 0, ...tasks.map(task => {
  118. const taskstr = StringifyTask(task);
  119. if(workspaceTasks.has(taskstr)){
  120. console.log("已有任务", taskstr);
  121. return null;
  122. }
  123. return {
  124. task: taskstr,
  125. createdAt: new Date().getTime(),
  126. ...task.options,
  127. };
  128. }).filter(result => result));
  129. SetSakuraWorkspace(workspace);
  130. }
  131. const GetAuth = () => isServer ? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2ZjAxYWVlNGU4MTkwM2JiZGUzZTFkYiIsImVtYWlsIjoieGlhdGlhbnl1MjAwMkAxNjMuY29tIiwidXNlcm5hbWUiOiJWb2x0YSIsInJvbGUiOiJub3JtYWwiLCJjcmVhdGVBdCI6MTcyNzAxMTU2NiwiZXhwIjoxNzM1NTYzMTc0fQ.zUrcId4N59bhMh7I_FiduFY0Qva-ABLcmFHTaz3sA0k" :JSON.parse(localStorage.getItem("authInfo"))?.profile?.token;
  132. const GetBlackList = () => JSON.parse((localStorage.getItem("blacklist") ?? "[]"));
  133. const CheckForUntranslated = async (type = 1, limit = 50) => {
  134. const auth = GetAuth();
  135. const pageSize = 48;
  136. let page = null, pageCount = 1;
  137. for(let pageNumber = 1; pageNumber <= pageCount && pageNumber <= limit; pageNumber += 1){
  138. page = await (await Fetch(`https://books.fishhawk.top/api/wenku?page=${pageNumber - 1}&pageSize=${pageSize}&query=&level=${type}`, {
  139. headers: {
  140. "Accept": "application/json",
  141. "authorization": "Bearer " + auth,
  142. }
  143. })).json();
  144. pageCount = page.pageNumber;
  145. const blackList = new Set(GetBlackList());
  146. const itemWorker = async (item) => {
  147. const id = item.id;
  148. const detail = await (await Fetch(`https://books.fishhawk.top/api/wenku/${id}`, {
  149. headers: {
  150. "Accept": "application/json",
  151. "authorization": "Bearer " + auth,
  152. }
  153. })).json();
  154. for(const volume of detail.volumeJp){
  155. if(volume.sakura < volume.total && volume.gpt < volume.total && !blackList.has(id)){
  156. InsertNewJob({
  157. type: "wenku",
  158. id: id,
  159. bookname: volume.volumeId,
  160. options: {
  161. description: volume.volumeId,
  162. priority: type * 10 + 10 - 10 / pageNumber,
  163. },
  164. }, 0);
  165. }
  166. }
  167. }
  168. await Promise.allSettled(page.items.map(item => itemWorker(item)));
  169. }
  170. };
  171. ujsConsole.CheckForUntranslated = CheckForUntranslated;
  172. const minimumWebCheckInterval = 7200_000;
  173. const CheckUntranslatedPopularWeb = async (limit = 100, dry = false) => {
  174. const auth = GetAuth();
  175. const lastChecked = Number(localStorage.getItem("web-checked-timestamp") ?? 0);
  176. const currTime = new Date().getTime();
  177. if(currTime - lastChecked < minimumWebCheckInterval) return;
  178. const pageSize = 100;
  179. let pageCount = 1;
  180. const jobs = [];
  181. const pointsMult = new Map([
  182. ["kakuyomu", x => x],
  183. ["syosetu", x => x * 0.1],
  184. ["novelup", x => x * 0.01],
  185. ["hameln", x => x * 0.1],
  186. ["alphapolis", x => x * 0.001],
  187. ])
  188. for(let pageNumber = 0; pageNumber < pageCount && pageNumber < limit; pageNumber += 1){
  189. const pageReq = await Fetch(`https://books.fishhawk.top/api/novel?${new URLSearchParams({
  190. page: pageNumber,
  191. pageSize: pageSize,
  192. query: "",
  193. provider: "kakuyomu,syosetu,novelup,hameln,alphapolis",
  194. type: 0,
  195. level: 1,
  196. translate: 0,
  197. sort: 1,
  198. })}`,{
  199. headers: {
  200. "authorization": "Bearer " + auth,
  201. }
  202. });
  203. const page = await pageReq.json();
  204. pageCount = page.pageNumber;
  205. await Promise.allSettled(page.items.map(async (novel) =>{
  206. if(!(novel.sakura < novel.jp && novel.gpt < novel.jp)) return;
  207. const detailReq = await Fetch(`https://books.fishhawk.top/api/novel/${novel.providerId}/${novel.novelId}`);
  208. const detail = await detailReq.json();
  209. jobs.push({
  210. points: detail.points,
  211. visited: detail.visited,
  212. load: novel.jp - novel.sakura,
  213. job: {
  214. task: `web/${novel.providerId}/${novel.novelId}?level=normal&forceMetadata=false&startIndex=0&endIndex=65536`,
  215. description: detail.titleZh ?? detail.titleJp,
  216. createdAt: new Date().getTime(),
  217. priority: 50,
  218. }
  219. })
  220. }))
  221. }
  222. const EvalPriority = (job) => job.points + job.visited * 10;
  223. jobs.sort((a, b) => EvalPriority(b) - EvalPriority(a));
  224. if(dry){
  225. console.log(jobs);
  226. return;
  227. }
  228. };
  229. ujsConsole.CheckUntranslatedPopularWeb = CheckUntranslatedPopularWeb;
  230. const CheckForumUntranslated = async () => {
  231. const pageSize = 20;
  232. const forumPageReq = await Fetch(`https://books.fishhawk.top/api/article?${new URLSearchParams({
  233. page: 0,
  234. pageSize: 20,
  235. category: "General",
  236. }).toString()}`);
  237. const forumPage = await forumPageReq.json();
  238. for(const post of forumPage){
  239. if(post.pinned) continue;
  240. const pid = post.id;
  241. //TODO
  242. }
  243. }
  244. //添加「检查未翻译条目」按钮
  245. const AddWenkuCheckerButton = () => {
  246. if(window.location.pathname !== "/wenku") return;
  247. document.querySelectorAll("h1").forEach((ele) => {
  248. if(ele.hasAttribute("modified") || ele.textContent !== "文库小说") return;
  249. ele.setAttribute("modified", "");
  250. ele.insertAdjacentElement("afterend",
  251. HTML("button", {class: "n-button n-button--default-type n-button--medium-type", "_click": CheckForUntranslated}, "检查未翻译条目")
  252. );
  253. });
  254. }
  255. const ExtendWorkerItem = () => {
  256. if(window.location.pathname !== "/workspace/sakura") return;
  257. document.querySelectorAll("button.__button-131ezvy-dfltmd.n-button.n-button--default-type.n-button--tiny-type.n-button--secondary").forEach((ele) => {
  258. const parent = ele.parentElement;
  259. if(parent.hasAttribute("modified")) return;
  260. parent.setAttribute("modified", "");
  261. ele.parentElement.insertAdjacentElement("afterbegin",
  262. HTML("button", {class: "__button-131ezvy-dfltmd n-button n-button--default-type n-button--tiny-type n-button--secondary", tabindex: 0, type: "button", _click: () => {
  263. if(parent.classList.contains("always-working")) parent.classList.remove("always-working");
  264. else parent.classList.add("always-working");
  265. }}, "始终工作")
  266. );
  267. });
  268. }
  269. const RunStalledWorker = () => {
  270. if(window.location.pathname !== "/workspace/sakura"){ return; }
  271. let runningCount = GetRunningCount();
  272. const workspace = GetSakuraWorkspace();
  273. const jobCount = (workspace.jobs?.length) ?? 0;
  274. document.querySelectorAll("div.n-flex.always-working").forEach((ele) => {
  275. if(runningCount >= jobCount) return;
  276. const child = ele.children[1];
  277. if(child.textContent !== " 停止 "){
  278. runningCount += 1;
  279. child.click();
  280. }
  281. });
  282. if(runningCount >= 5) fetch("http://localhost:17353/end-sharing");
  283. else if(runningCount < 5) fetch("http://localhost:17353/start-sharing");
  284. };
  285. const GetRunningCount = () => {
  286. let ret = 0;
  287. document.querySelectorAll("div.n-flex.always-working").forEach((ele) => {
  288. if(ele.children[1].textContent === " 停止 ") ret += 1;
  289. });
  290. return ret;
  291. }
  292. const RetryFailedTasks = () => {
  293. if(window.location.pathname !== "/workspace/sakura") return;
  294. const workspace = GetSakuraWorkspace();
  295. const workspaceClone = structuredClone(workspace);
  296. if(!workspace.uncompletedJobs) return;
  297. for(let i = 0; i < workspace.uncompletedJobs.length;){
  298. const completed = workspace.uncompletedJobs[i];
  299. if(!completed.progress || completed.progress.finished < completed.progress.total){
  300. console.log("发现未完成任务:", completed);
  301. workspace.uncompletedJobs.splice(i, 1);
  302. workspace.jobs.splice(0, 0, {
  303. task: completed.task,
  304. description: completed.description,
  305. createdAt: new Date().getTime(),
  306. priority: 0,
  307. ...completed.progress ? {progress: {
  308. finished: 0,
  309. error: 0,
  310. total: completed.progress.total - completed.progress.finished,
  311. }} : {},
  312. });
  313. }
  314. else i++;
  315. }
  316. SetSakuraWorkspace(workspace);
  317. const event = new StorageEvent("storage", {
  318. key: "sakura-workspace",
  319. oldValue: JSON.stringify(workspaceClone),
  320. newValue: JSON.stringify(workspace),
  321. url: window.location.toString(),
  322. storageArea: localStorage,
  323. });
  324. window.dispatchEvent(event);
  325. };
  326. const TaskDetailAPI = (job) => {
  327. const task = job.task ?? job;
  328. const taskURL = new URL(`${origin}/${task}`);
  329. const path = taskURL.pathname.split("/");
  330. if(path[1] === "wenku"){
  331. return queryURL = `${origin}/api/wenku/${path[2]}/translate-v2/sakura/${path[3]}`;
  332. }
  333. else if(path[1] === "web"){
  334. return queryURL = `${origin}/api/novel/${path[2]}/${path[3]}/translate-v2/sakura`;
  335. }
  336. };
  337. let RemoveFinishedLock = false;
  338. const RemoveFinishedTasks = async () => {
  339. if(window.location.pathname !== "/workspace/sakura") return;
  340. if(RemoveFinishedLock) return;
  341. RemoveFinishedLock = true;
  342. try{
  343. const workspace = GetSakuraWorkspace();
  344. const toRemove = new Set();
  345. if(!workspace.jobs) return;
  346. const querys = new Set(workspace.jobs.map(TaskDetailAPI).filter(url => url));
  347. const queryResults = [...querys.keys()].map(async url => {
  348. try{
  349. const response = await Fetch(url, {headers: {"Accept": "application/json"}});
  350. if(response.status === 404) return [url, 404];
  351. else return [url, await response.json()];
  352. }
  353. catch(e){
  354. return [url, "error"];
  355. }
  356. });
  357. const queryResultMap = new Map(await Promise.all(queryResults));
  358. workspace.jobs.forEach((job) => {
  359. if(job.progress && job.progress.finished >= job.progress.total){
  360. console.log("发现已完成任务:", job);
  361. toRemove.add(job.task);
  362. return;
  363. }
  364. const query = TaskDetailAPI(job);
  365. const result = queryResultMap.get(query);
  366. if(!result){
  367. console.warn("???", job);
  368. return;
  369. }
  370. else if(result === "error") return;
  371. else if(result === 404){
  372. console.log("发现不存在任务", job);
  373. toRemove.add(job.task);
  374. return;
  375. }
  376. const hasUnfinished = GetUntranslated(job.task, result);
  377. if(hasUnfinished) return;
  378. console.log("发现已完成任务:", job);
  379. toRemove.add(job.task);
  380. });
  381. const currWorkspace = GetSakuraWorkspace();
  382. for(let i = 0; i < currWorkspace.jobs.length;){
  383. const job = currWorkspace.jobs[i];
  384. if(toRemove.has(job.task)){
  385. currWorkspace.jobs.splice(i, 1);
  386. currWorkspace.uncompletedJobs.splice(-1, 0, {
  387. task: job.task,
  388. description: job.description,
  389. createdAt: job.createdAt,
  390. finishedAt: new Date().getTime(),
  391. progress: {
  392. finished: 999,
  393. error: 0,
  394. total: 999,
  395. },
  396. priority: 0 ,
  397. });
  398. }
  399. else i++;
  400. }
  401. SetSakuraWorkspace(currWorkspace);
  402. }finally{
  403. setTimeout(() => RemoveFinishedLock = false, 5000);
  404. }
  405. };
  406. //在线小说阅读器里,存在一部分<br>元素非常麻烦,替换为空的<p class="line-break">元素
  407. const ReplaceBrElement = () => {
  408. if(!window.location.pathname.startsWith("/novel")) return;
  409. console.log("ReplaceBr");
  410. document.querySelectorAll("#chapter-content > br").forEach((br) => {
  411. br.replaceWith(HTML("p", {class: "line-break"}));
  412. })
  413. };
  414. let _CheckNewWenkuLockLock = false;
  415. let _SkipNextCheckNewWenkuCall = false;
  416. const CheckNewWenkuChannel = new BroadcastChannel("CheckNewWenku");
  417. CheckNewWenkuChannel.addEventListener("message", (ev) => {
  418. if(ev.data === "Checked" && ev.origin === origin){
  419. _SkipNextCheckNewWenkuCall = true;
  420. }
  421. })
  422. const GetUntranslated = (task, query, getIndex = false) => {
  423. const taskURL = new URL(`${origin}/${task}`);
  424. const isNormal = taskURL.searchParams.has("level", "normal");
  425. const isRetranslate = !isNormal && !taskURL.searchParams.has("level", "expire");
  426. const startIndex = taskURL.searchParams.get("startIndex") ?? 0;
  427. const endIndex = taskURL.searchParams.get("endIndex") ?? 65535;
  428. const glossaryId = query.glossaryUuid ?? query.glossaryId;
  429. const indexes = !query.toc ? [] :
  430. query.toc
  431. .filter(chap => chap.chapterId !== undefined)
  432. .map((chap, index) => {return {...chap, index: index, ...(chap.glossaryUuid ? {glossaryId: chap.glossaryUuid} : {})}})
  433. .filter((_, index) => index >= startIndex && index < endIndex)
  434. .filter(chap => isRetranslate ? true : (isNormal ? chap.glossaryId === undefined : (chap.glossaryId !== glossaryId && chap.glossaryId !== undefined)))
  435. if(getIndex) return indexes.map(chap => chap.index);
  436. else if(indexes.length > 0) return true;
  437. else return false;
  438. };
  439. const CheckNewWenku = () => {
  440. if(_CheckNewWenkuLockLock || window.location.pathname !== "/workspace/sakura") return;
  441. const Worker = async () => {
  442. try{
  443. if(_SkipNextCheckNewWenkuCall){
  444. _SkipNextCheckNewWenkuCall = false;
  445. }
  446. else{
  447. console.log("检查未翻译新文库本");
  448. await CheckForUntranslated(1, 1);
  449. await CheckForUntranslated(2, 1);
  450. await CheckForUntranslated(3, 1);
  451. CheckNewWenkuChannel.postMessage("Checked");
  452. }
  453. }
  454. finally{
  455. setTimeout(Worker, 5000);
  456. }
  457. };
  458. setTimeout(Worker, 5000);
  459. _CheckNewWenkuLockLock = true;
  460. };
  461. const AddJobQueuer = () => {
  462. if(!window.location.pathname.startsWith("/novel")) return;
  463. const ele = document.querySelector("button.__button-131ezvy-lmmd.n-button.n-button--default-type.n-button--medium-type");
  464. if(!ele || ele.hasAttribute("modified")) return;
  465. ele.setAttribute("modified", "");
  466. const 范围 = [...document.querySelectorAll("span.n-text.__text-131ezvy-d3")].find(ele => ele.textContent === "范围");
  467. if(!范围) return;
  468. const startInput = 范围.nextElementSibling.children[0].children[0].children[1].children[0].children[0].children[0].children[0];
  469. const endInput = 范围.nextElementSibling.children[0].children[0].children[3].children[0].children[0].children[0].children[0];
  470. ele.insertAdjacentElement("afterend",
  471. HTML("button", {
  472. class: "__button-131ezvy-lmmd n-button n-button--default-type n-button--medium-type",
  473. tabindex: "1",
  474. type: "button",
  475. _click: async () => {
  476. const paths = window.location.pathname.split("/");
  477. const [ , , provider, id] = paths;
  478. const title = document.querySelector("h3 a.n-a.__a-131ezvy").textContent;
  479. let mode = "normal", metadata = false;
  480. document.querySelectorAll(".__tag-131ezvy-ssc,.__tag-131ezvy-wsc").forEach(div => {switch(div.textContent){
  481. case "常规": mode = "normal"; break;
  482. case "过期": mode = "expire"; break;
  483. case "重翻": mode = "all"; break;
  484. case "源站同步": mode = "sync"; break;
  485. case "重翻目录": metadata = true; break;
  486. }});
  487. const taskObj = {
  488. type: "web",
  489. provider: provider,
  490. id: id,
  491. startIndex: Number(startInput.value),
  492. endIndex: Number(endInput.value),
  493. mode: mode,
  494. forceMetadata: metadata,
  495. };
  496. const queryURL = `${origin}/api/novel/${provider}/${id}/translate-v2/sakura`;
  497. const query = await Fetch(queryURL);
  498. const queryResult = await query.json();
  499. InsertNewJob(GetUntranslated(StringifyTask(taskObj), queryResult, true).map(index => ({
  500. ...taskObj,
  501. startIndex: index,
  502. endIndex: index + 1,
  503. options: {
  504. description: title,
  505. priority: 5 + index / 1000,
  506. },
  507. })), 0);
  508. },
  509. }, "逐章排队")
  510. );
  511. }
  512. const ParseTask = (taskstr) => {
  513. const [pathname, paramstr] = taskstr.split("?")
  514. const paths = pathname.split("/");
  515. const param = new URLSearchParams(paramstr ?? "");
  516. return {
  517. ...(paths[0] === "web" ? {
  518. type: "web",
  519. provider: paths[1],
  520. id: paths[2],
  521. } : paths[0] === "wenku" ? {
  522. type: "wenku",
  523. id: paths[1],
  524. bookname: paths[2],
  525. } : {
  526. path: pathname,
  527. }),
  528. startIndex: Number(param.get("startIndex") ?? 0),
  529. endIndex: Number(param.get("endIndex") ?? 65535),
  530. mode: pathname.get("level") ?? "normal",
  531. forceMetadata: Boolean(pathname.get("forceMetadata") ?? false),
  532. };
  533. };
  534. const StringifyTask = (taskobj) => {
  535. return `${taskobj.type === "web" ? `web/${taskobj.provider}/${taskobj.id}` : taskobj.type === "wenku" ? `wenku/${taskobj.id}/${taskobj.bookname}` : taskobj.path}?level=${taskobj.mode ?? "normal"}&forceMetadata=${taskobj.forceMetadata ?? false}&startIndex=${taskobj.startIndex ?? 0}&endIndex=${taskobj.endIndex ?? 65535}`
  536. }
  537. const MergeFinishedTasks = () => {
  538. const workspace = GetSakuraWorkspace();
  539. }
  540. const AddCustomSearchTag = () => {
  541. const target = document.querySelector("div.n-tag");
  542. if(!target || target.hasAttribute("modified")) return;
  543. target.setAttribute("modified", "");
  544. const text = "-TS -性転換 -男の娘 -TS";
  545. target.insertAdjacentElement("beforebegin",
  546. HTML("div", {class: "n-tag __tag-131ezvy-ssc", style: "cursor: pointer;", modified: "", _click: () => {
  547. const input = document.querySelector("input.n-input__input-el");
  548. input.value = input.value + "" + text;
  549. }},
  550. HTML("span", {}, text)
  551. )
  552. )
  553. }
  554. const AdvancedSearch = () => {
  555. const loc = window.location.toString();
  556. if(!(loc.includes("query") && loc.includes("novel"))) return;
  557. document.querySelectorAll(".n-list-item").forEach(item => {
  558. if(item.hasAttribute("modified")) return;
  559. item.setAttribute("modified", "");
  560. const link = item.children[0].children[0].children[2];
  561. const [provider, id] = link.textContent.split(".");
  562. const main = item.children[0];
  563. main.insertAdjacentElement("afterend",
  564. HTML("button", {class: "expand-detail", _click: async (ev) => {
  565. const target = ev.target;
  566. const detailReq = await WaitUntilSuccess(Fetch, [`https://books.fishhawk.top/api/novel/${provider}/${id}`], {isSuccess: res => res.status === 200});
  567. const detail = await detailReq.json();
  568. target.replaceWith(
  569. HTML("div", {class: "detail-container"},
  570. HTML("div", {class: "detail-meta"}, `${detail.points} pt / ${detail.visited} 点击 / ${detail.totalCharacters}`),
  571. HTML("div", {class: "detail-description"}, detail.introductionZh ?? detail.introductionJp)
  572. )
  573. )
  574. }}, "显示详情")
  575. )
  576. })
  577. }
  578. //页面变化时立刻调用上面的功能
  579. const OnMutate = async (mutlist, observer) => {
  580. observer.disconnect(); //避免无限嵌套
  581. if(isServer) return;
  582. ReplaceBrElement();
  583. AdvancedSearch();
  584. AddWenkuCheckerButton();
  585. AddJobQueuer();
  586. ExtendWorkerItem();
  587. RunStalledWorker();
  588. RetryFailedTasks();
  589. RemoveFinishedTasks();
  590. //MergeFinishedTasks();
  591. //StartCustomTranslator(9);
  592. observer.observe(document, {subtree: true, childList: true});
  593. };
  594. new MutationObserver(OnMutate).observe(document, {subtree: true, childList: true});
  595. console.log("hello world");
  596.  
  597. const Range = (start, end) => {
  598. if(end < start) throw new RangeError("end should >= start");
  599. const arr = new Array(end - start);
  600. for(let i = start; i < end; i++){
  601. arr[i - start] = i;
  602. }
  603. return arr;
  604. }
  605.  
  606. const FetchForumPosts = async () => {
  607. const pageSize = 100;
  608. const MakePageLink = pageNum => `/api/article?page=${pageNum}&pageSize=${pageSize}&category=General`;
  609. const MakeTopicLink = topicId => `/api/article/${topicId}`;
  610. const MakeReplyLink = (pageNum, topicId) => `/api/comment?site=article-${topicId}&pageSize=${pageSize}&page=${pageNum}`;
  611. const firstPageReq = await Fetch(MakePageLink(0));
  612. const firstPage = await firstPageReq.json();
  613. const pageCount = firstPage.pageNumber;
  614. const topics = [firstPage.items];
  615. topics.push(...(await Promise.all(Range(1, pageCount).map(async index => {
  616. const req = await Fetch(MakePageLink(index));
  617. const res = await req.json();
  618. return res.items;
  619. }))).flat());
  620. console.log(topics);
  621. const replies = (await Promise.all(topics.map(async topic => {
  622. try{
  623. const req = await Fetch(MakeTopicLink(topic.id));
  624. const res = await req.json();
  625. const repreq = await Fetch(MakeReplyLink(0, topic.id));
  626. const rep = await repreq.json();
  627. return [res, ...rep.items, ...rep.items.flatMap(item => item.replies)];
  628. }catch(e){
  629. return [];
  630. }
  631. }))).flat();
  632. window.localStorage.setItem("result", JSON.stringify([topics, replies]));
  633. return [topics, replies];
  634. }
  635. ujsConsole.GetItem = (key) => JSON.parse(window.localStorage.getItem(key));
  636. ujsConsole.SetItem = (key, value) => window.localStorage.setItem(key, JSON.stringify(value));
  637. ujsConsole = {
  638. ...ujsConsole,
  639. GetSakuraWorkspace: GetSakuraWorkspace,
  640. SetSakuraWorkspace: SetSakuraWorkspace,
  641. }