JustMoeComments

萌娘百科看Lih的镜像站的评论,同时集成了作品讨论的评论

目前为 2024-04-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JustMoeComments
  3. // @namespace https://github.com/gui-ying233/JustMoeComments
  4. // @version 2.12.0
  5. // @description 萌娘百科看Lih的镜像站的评论,同时集成了作品讨论的评论
  6. // @author 鬼影233
  7. // @license MIT
  8. // @match zh.moegirl.org.cn/*
  9. // @match mzh.moegirl.org.cn/*
  10. // @match mobile.moegirl.org.cn/*
  11. // @icon https://moegirl.uk/images/a/a2/%E7%B2%89%E8%89%B2%E5%A4%A7%E7%8C%9B%E5%AD%97.png
  12. // @supportURL https://github.com/gui-ying233/JustMoeComments/issues
  13. // ==/UserScript==
  14.  
  15. (async () => {
  16. "use strict";
  17. if (new URLSearchParams(window.location.search).get("safemode")) return;
  18. await new Promise(resolve => {
  19. const intervId = setInterval(() => {
  20. if (typeof mw !== "undefined" && typeof wgULS !== "undefined") {
  21. clearInterval(intervId);
  22. resolve();
  23. }
  24. }, 50);
  25. });
  26. if (
  27. mw.config.get("wgAction") !== "view" ||
  28. ![0, 2, 4, 12, 274].includes(mw.config.get("wgNamespaceNumber"))
  29. )
  30. return;
  31. const api = new mw.Api();
  32. const generatePost = post => {
  33. let timestamp;
  34. if (typeof post.timestamp === "number") {
  35. const diff = Date.now() - post.timestamp * 1000;
  36. if (diff > 0 && diff < 86400000) {
  37. timestamp = moment(post.timestamp * 1000)
  38. .locale(mw.config.get("wgUserLanguage"))
  39. .fromNow();
  40. } else {
  41. timestamp = moment(post.timestamp * 1000)
  42. .locale(mw.config.get("wgUserLanguage"))
  43. .format("LL, HH:mm:ss");
  44. }
  45. } else {
  46. timestamp = post.timestamp;
  47. }
  48. const postDiv = document.createElement("div");
  49. postDiv.className = "comment-thread";
  50. postDiv.innerHTML = `<div class="comment-post"><div class="comment-avatar"><a href="//moegirl.uk/U:${
  51. post.username
  52. }"><img src="//moegirl.uk/extensions/Avatar/avatar.php?user=${
  53. post.username
  54. }" decofing="async" loading="lazy" fetchpriority="low"></a></div><div class="comment-body"><div class="comment-user"><a href="//moegirl.uk/U:${
  55. post.username
  56. }">${post.username}</a></div><div class="comment-text">${
  57. post.text
  58. }</div><div class="comment-footer"><span class="comment-time">${timestamp}</span>${
  59. post.like ? `<span class="comment-like">赞 ${post.like}</span>` : ""
  60. }</div></div></div>`;
  61. postDiv.querySelector(".comment-avatar > a > img").onerror =
  62. function () {
  63. if (new URL(this.src).host === "moegirl.uk")
  64. this.src = `//commons.moegirl.org.cn/extensions/Avatar/avatar.php?user=${post.username}`;
  65. };
  66. [...postDiv.querySelectorAll("img[src^='/images/']")].forEach(i => {
  67. i.src = `//img.moegirl.org.cn/common/${new URL(
  68. i.src
  69. ).pathname.slice(8)}`;
  70. i.srcset = i.srcset.replaceAll(
  71. "/images/",
  72. "//img.moegirl.org.cn/common/"
  73. );
  74. });
  75. [
  76. ...postDiv.querySelectorAll(
  77. 'img[src*="thumb"][src$=".svg.png"], img[src*="thumb"][data-lazy-src$=".svg.png"], img[src*="thumb"][src$=".gif"], img[data-lazy-src*="thumb"][data-lazy-src$=".gif"]'
  78. ),
  79. ].forEach(i => {
  80. try {
  81. const _i = i.cloneNode();
  82. if (
  83. new mw.Uri(_i.src || _i.dataset.lazySrc).host ===
  84. "img.moegirl.org.cn"
  85. ) {
  86. _i.src = _i.src
  87. .replace("/thumb/", "/")
  88. .replace(/\.svg\/[^/]+\.svg\.png$/, ".svg")
  89. .replace(/\.gif\/[^/]+\.gif$/, ".gif");
  90. _i.removeAttribute("srcset");
  91. _i.removeAttribute("data-lazy-src");
  92. _i.removeAttribute("data-lazy-srcset");
  93. _i.removeAttribute("data-lazy-state");
  94. _i.classList.remove("lazyload");
  95. _i.onload = function () {
  96. i.replaceWith(_i);
  97. };
  98. }
  99. } catch {}
  100. });
  101. [
  102. ...postDiv.querySelectorAll(
  103. "a.extiw[title^='moe:'], a.extiw[title^='zhmoe:']"
  104. ),
  105. ].forEach(a => {
  106. a.classList.remove("extiw");
  107. api.get({
  108. action: "query",
  109. format: "json",
  110. titles: decodeURI(a.pathname.slice(1)),
  111. utf8: 1,
  112. formatversion: 2,
  113. }).done(b => {
  114. if (b.query.pages[0].missing) {
  115. a.href = `/index.php?title=${a.pathname.slice(
  116. 1
  117. )}&action=edit&redlink=1`;
  118. a.title += wgULS(" (页面不存在)", "(頁面不存在)");
  119. a.classList += "new";
  120. } else if (
  121. b.query.pages[0].pageid === mw.config.get("wgArticleId")
  122. ) {
  123. a.href = "";
  124. a.title = "";
  125. a.classList += "mw-selflink selflink";
  126. } else {
  127. a.href = a.pathname;
  128. a.title = a.title.replace(/moe:|zhmoe:/, "");
  129. }
  130. });
  131. });
  132. [...postDiv.getElementsByTagName("script")].forEach(s => {
  133. const _s = document.createElement("script");
  134. _s.innerHTML = s.innerHTML;
  135. [...s.attributes].forEach(a => {
  136. _s.setAttribute(a.name, a.value);
  137. });
  138. s.parentNode.replaceChild(_s, s);
  139. });
  140. return postDiv;
  141. };
  142. mw.loader.using(["moment"]).done(() => {
  143. const commentCSS = document.createElement("style");
  144. commentCSS.innerHTML =
  145. "#flowthread{clear:both;padding:1.5em}body.skin-moeskin #flowthread{background-color:var(--theme-background-color)}.comment-container-top:not(:empty){border:1px #ccc solid;border-radius:5px}body.skin-vector .comment-container-top{background-color:rgb(191 234 181 / 20%)}body.skin-moeskin .comment-container-top{background-color:var(--theme-card-background-color)}.comment-container-top>div:first-child{height:24px;line-height:24px;text-indent:1em;font-size:small;border-radius:5px 5px 0 0;font-weight:bold}body.skin-vector .comment-container-top>div:first-child{background-color:rgb(18 152 34 / 47%);color:#fff}body.skin-moeskin .comment-container-top>div:first-child{background-color:var(--theme-accent-color);color:var(--theme-accent-link-color)}.comment-thread{border-top:1px solid rgba(0,0,0,0.13)}.comment-thread .comment-thread{margin-left:40px}.comment-post{padding:10px}.comment-avatar{float:left}.comment-avatar img{width:50px;height:50px}.comment-body{padding-left:60px}.comment-thread>div:not(:first-of-type) .comment-avatar img{width:30px;height:30px}.comment-thread>div:not(:first-of-type) .comment-body{padding-left:40px}.comment-user,.comment-user a{color:#777;font-size:13px;margin-right:8px}.post-content .comment-text{position:static}.comment-text{font-size:13px;line-height:1.5em;margin:.5em 0;word-wrap:break-word;position:relative;overflow:hidden;min-height:1em}.comment-footer{font-size:12px;margin-right:8px;color:#999}.comment-like{margin-left:5px}";
  146. document.head.appendChild(commentCSS);
  147. const containerTop = document.createElement("div");
  148. containerTop.className = "comment-container-top";
  149. const container = document.createElement("div");
  150. container.className = "comment-container";
  151. const postContent = document.createElement("div");
  152. postContent.id = "flowthread";
  153. postContent.className = "post-content";
  154. postContent.appendChild(containerTop);
  155. postContent.appendChild(container);
  156. switch (mw.config.get("skin")) {
  157. case "vector":
  158. document.getElementById("footer").appendChild(postContent);
  159. break;
  160. case "moeskin":
  161. default:
  162. document
  163. .getElementById("moe-global-footer")
  164. .appendChild(postContent);
  165. break;
  166. }
  167. fetch(
  168. `https://moegirl.uk/api.php?${new URLSearchParams({
  169. action: "query",
  170. format: "json",
  171. prop: "pageprops",
  172. titles: mw.config.get("wgPageName"),
  173. utf8: 1,
  174. formatversion: 2,
  175. origin: "*",
  176. })}`
  177. )
  178. .then(a => a.json())
  179. .then(a =>
  180. (function getComment(offset) {
  181. fetch(
  182. `https://moegirl.uk/api.php?${new URLSearchParams({
  183. action: "flowthread",
  184. format: "json",
  185. type: "list",
  186. pageid: a.query.pages[0].pageid,
  187. limit: 15,
  188. offset,
  189. utf8: 1,
  190. formatversion: 2,
  191. origin: "*",
  192. })}`
  193. )
  194. .then(b => b.json())
  195. .then(b => {
  196. if (b.flowthread.popular.length) {
  197. document.body.getElementsByClassName(
  198. "comment-container-top"
  199. )[0].innerHTML = "<div>热门评论</div>";
  200. for (const post of b.flowthread.popular) {
  201. const _post = generatePost(post);
  202. _post.classList.add("comment-popular");
  203. document.body
  204. .getElementsByClassName(
  205. "comment-container-top"
  206. )[0]
  207. .appendChild(_post);
  208. }
  209. }
  210. for (const post of b.flowthread.posts) {
  211. const _post = generatePost(post);
  212. _post.id = `comment-${post.id}`;
  213. if (post.parentid) {
  214. document
  215. .getElementById(
  216. `comment-${post.parentid}`
  217. )
  218. .appendChild(_post);
  219. } else {
  220. document
  221. .getElementsByClassName(
  222. "comment-container"
  223. )[0]
  224. .appendChild(_post);
  225. }
  226. }
  227. if (b.flowthread.count > offset + 15) {
  228. new IntersectionObserver(
  229. (entries, observer) => {
  230. entries.forEach(entry => {
  231. if (entry.isIntersecting) {
  232. getComment(offset + 15);
  233. observer.unobserve(
  234. entry.target
  235. );
  236. }
  237. });
  238. }
  239. ).observe(
  240. document.querySelector(
  241. ".comment-container > div.comment-thread:last-of-type"
  242. )
  243. );
  244. }
  245. });
  246. })(0)
  247. )
  248. .catch(() => {
  249. fetch(
  250. "https://testingcf.jsdelivr.net/gh/gui-ying233/JustMoeComments/flowthread.json"
  251. )
  252. .then(a => a.json())
  253. .then(a => {
  254. let f = 0;
  255. for (const t of a) {
  256. if (
  257. t.title !==
  258. mw.config.get("wgPageName").replace("_", " ")
  259. )
  260. continue;
  261. a = t.posts;
  262. f = 1;
  263. break;
  264. }
  265. if (!f) return;
  266. for (const b of a) {
  267. if (+b.status) continue;
  268. const _post = generatePost({
  269. username: b.username,
  270. text: b.text,
  271. timestamp: "",
  272. });
  273. _post.id = `comment-${b.id}`;
  274. if (b.parentid) {
  275. document
  276. .getElementById(`comment-${b.parentid}`)
  277. .appendChild(_post);
  278. } else {
  279. document
  280. .getElementsByClassName(
  281. "comment-container"
  282. )[0]
  283. .appendChild(_post);
  284. }
  285. }
  286. });
  287. });
  288. if (mw.config.get("skin") !== "moeskin") return;
  289. setTimeout(() => {
  290. if (
  291. ![
  292. (mw.config.get("wgPageName"),
  293. mw.config.get("wgPageName").replace(/^(.+)\(.+?\)$/, "$1"),
  294. document.getElementById("firstHeading").innerText),
  295. ].includes(
  296. document.body.getElementsByClassName("artwork-title")[0]
  297. ?.innerText
  298. )
  299. )
  300. return;
  301. document.body.getElementsByClassName("comment-item").forEach(c => {
  302. if (
  303. !c.getElementsByClassName("comment-title")[0].innerText &&
  304. !c.getElementsByClassName("comment-content")[0].innerText
  305. )
  306. return;
  307. const post = {
  308. like: +c
  309. .getElementsByClassName("n-button-group")[0]
  310. .innerText.split("\n")[0],
  311. username:
  312. c.getElementsByClassName("comment-author")[0].innerText,
  313. text:
  314. c.getElementsByClassName("comment-title")[0].innerText +
  315. (c.getElementsByClassName("comment-title")[0]
  316. .innerText &&
  317. c.getElementsByClassName("comment-content")[0].innerText
  318. ? document.createElement("br").outerHTML
  319. : "") +
  320. c.getElementsByClassName("comment-content")[0]
  321. .innerText,
  322. timestamp:
  323. c.getElementsByClassName("comment-time")[0].innerText,
  324. };
  325. document
  326. .getElementsByClassName("comment-container")[0]
  327. .appendChild(generatePost(post));
  328. });
  329. }, 5000);
  330. });
  331. })();