├── .deepsource.toml ├── .github └── workflows │ └── 导出FlowThread评论.yaml ├── LICENSE ├── flowthread.json ├── main.css ├── main.user.js ├── package-lock.json ├── package.json └── scripts └── 导出FlowThread评论.js /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | 6 | [analyzers.meta] 7 | environment = ["browser"] -------------------------------------------------------------------------------- /.github/workflows/导出FlowThread评论.yaml: -------------------------------------------------------------------------------- 1 | name: 导出 FlowThread 评论 2 | on: 3 | schedule: 4 | - cron: "*/5 * * * *" 5 | workflow_dispatch: 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 签出 11 | uses: actions/checkout@v4 12 | - name: 设置 node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | cache: npm 17 | - name: 安装 npm 依赖 18 | run: npm install 19 | - name: 配置 git 20 | run: | 21 | git config user.name "github-actions[bot]" 22 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 23 | git pull --rebase 24 | - name: 导出 FlowThread 评论 25 | env: 26 | MOEGIRL_UK_MGPUSERID: ${{ vars.MOEGIRL_UK_MGPUSERID }} 27 | MOEGIRL_UK_MGPTOKEN: ${{ secrets.MOEGIRL_UK_MGPTOKEN }} 28 | run: node scripts/导出FlowThread评论 29 | - name: 更新 flowthread.json 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | run: | 33 | git add -f flowthread.json 34 | if ! git diff-index --quiet HEAD --; then 35 | git commit -m "Update threeds" 36 | git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/gui-ying233/JustMoeComments.git 37 | else 38 | echo "评论无更新" 39 | fi 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 鬼影233 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | #flowthread { 2 | clear: both; 3 | padding: 1.5em 4 | } 5 | body.skin-moeskin #flowthread { 6 | background-color: var(--theme-background-color) 7 | } 8 | .comment-container-top:not(:empty) { 9 | border: 1px #ccc solid; 10 | border-radius: 5px 11 | } 12 | body.skin-vector .comment-container-top { 13 | background-color: rgb(191 234 181 / 20%) 14 | } 15 | body.skin-moeskin .comment-container-top { 16 | background-color: var(--theme-card-background-color) 17 | } 18 | .comment-container-top>div:first-child { 19 | height: 24px; 20 | line-height: 24px; 21 | text-indent: 1em; 22 | font-size: small; 23 | border-radius: 5px 5px 0 0; 24 | font-weight: bold 25 | } 26 | body.skin-vector .comment-container-top>div:first-child { 27 | background-color: rgb(18 152 34 / 47%); 28 | color: #fff 29 | } 30 | body.skin-moeskin .comment-container-top>div:first-child { 31 | background-color: var(--theme-accent-color); 32 | color: var(--theme-accent-link-color) 33 | } 34 | .comment-thread { 35 | border-top: 1px solid rgba(0,0,0,0.13) 36 | } 37 | .comment-thread .comment-thread { 38 | margin-left: 40px 39 | } 40 | .comment-post { 41 | padding: 10px 42 | } 43 | .comment-avatar { 44 | float: left 45 | } 46 | .comment-avatar img { 47 | width: 50px; 48 | height: 50px 49 | } 50 | .comment-body { 51 | padding-left: 60px 52 | } 53 | .comment-thread>div:not(:first-of-type) .comment-avatar img { 54 | width: 30px; 55 | height: 30px 56 | } 57 | .comment-thread>div:not(:first-of-type) .comment-body { 58 | padding-left: 40px 59 | } 60 | .comment-user,.comment-user a { 61 | color: #777; 62 | font-size: 13px; 63 | margin-right: 8px 64 | } 65 | .post-content .comment-text { 66 | position: static 67 | } 68 | .comment-text { 69 | font-size: 13px; 70 | line-height: 1.5em; 71 | margin: .5em 0; 72 | word-wrap: break-word; 73 | position: relative; 74 | overflow: hidden; 75 | min-height: 1em 76 | } 77 | .comment-footer { 78 | font-size: 12px; 79 | margin-right: 8px; 80 | color: #999 81 | } 82 | .comment-like { 83 | margin-left: 5px 84 | } 85 | -------------------------------------------------------------------------------- /main.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JustMoeComments 3 | // @namespace https://github.com/gui-ying233/JustMoeComments 4 | // @version 2.16.2 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 | // @match moegirl.icu/* 12 | // @match cube.moegirl.icu/* 13 | // @match zh.moegirl.tw/* 14 | // @match zh.moegirl.tw/* 15 | // @match mzh.moegirl.tw/* 16 | // @match mobile.moegirl.tw/* 17 | // @icon https://moegirl.uk/images/a/a2/%E7%B2%89%E8%89%B2%E5%A4%A7%E7%8C%9B%E5%AD%97.png 18 | // @supportURL https://github.com/gui-ying233/JustMoeComments/issues 19 | // @grant none 20 | // ==/UserScript== 21 | 22 | (async () => { 23 | "use strict"; 24 | if (new URLSearchParams(window.location.search).get("safemode")) return; 25 | await new Promise(resolve => { 26 | const intervId = setInterval( 27 | () => window?.mw?.Api && (clearInterval(intervId), resolve()), 28 | 50 29 | ); 30 | }); 31 | window.wgULS ??= (hans, hant, cn, tw, hk, sg, zh, mo, my) => 32 | ({ 33 | zh: zh || hans || hant || cn || tw || hk || sg || mo || my, 34 | "zh-hans": hans || cn || sg || my, 35 | "zh-hant": hant || tw || hk || mo, 36 | "zh-cn": cn || hans || sg || my, 37 | "zh-sg": sg || hans || cn || my, 38 | "zh-tw": tw || hant || hk || mo, 39 | "zh-hk": hk || hant || mo || tw, 40 | "zh-mo": mo || hant || hk || tw, 41 | }[mw.config.get("wgUserLanguage")] || 42 | zh || 43 | hans || 44 | hant || 45 | cn || 46 | tw || 47 | hk || 48 | sg || 49 | mo || 50 | my); 51 | if ( 52 | mw.config.get("wgAction") !== "view" || 53 | ![0, 2, 4, 12, 274].includes(mw.config.get("wgNamespaceNumber")) 54 | ) 55 | return; 56 | const api = new mw.Api(); 57 | const generatePost = ({ username, text, timestamp, like }) => { 58 | let _timestamp; 59 | if (typeof timestamp === "number") { 60 | const diff = Date.now() - timestamp * 1000; 61 | if (diff > 0 && diff < 86400000) { 62 | _timestamp = moment(timestamp * 1000) 63 | .locale(mw.config.get("wgUserLanguage")) 64 | .fromNow(); 65 | } else { 66 | _timestamp = moment(timestamp * 1000) 67 | .locale(mw.config.get("wgUserLanguage")) 68 | .format("LL, HH:mm:ss"); 69 | } 70 | } else { 71 | _timestamp = timestamp; 72 | } 73 | const postDiv = document.createElement("div"); 74 | postDiv.className = "comment-thread"; 75 | postDiv.innerHTML = `
${text}
`; 78 | postDiv.querySelector(".comment-avatar > a > img").onerror = 79 | function () { 80 | if (new URL(this.src).host === "moegirl.uk") 81 | this.src = `//commons.moegirl.org.cn/extensions/Avatar/avatar.php?user=${username}`; 82 | }; 83 | [...postDiv.querySelectorAll("img[src^='/images/']")].forEach(i => { 84 | i.src = `//img.moegirl.org.cn/common/${new URL( 85 | i.src 86 | ).pathname.slice(8)}`; 87 | i.srcset = i.srcset.replaceAll( 88 | "/images/", 89 | "//img.moegirl.org.cn/common/" 90 | ); 91 | }); 92 | [ 93 | ...postDiv.querySelectorAll( 94 | '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"]' 95 | ), 96 | ].forEach(i => { 97 | try { 98 | const _i = i.cloneNode(); 99 | if ( 100 | new mw.Uri(_i.src || _i.dataset.lazySrc).host === 101 | "img.moegirl.org.cn" 102 | ) { 103 | _i.src = _i.src 104 | .replace("/thumb/", "/") 105 | .replace(/\.svg\/[^/]+\.svg\.png$/, ".svg") 106 | .replace(/\.gif\/[^/]+\.gif$/, ".gif"); 107 | _i.removeAttribute("srcset"); 108 | _i.removeAttribute("data-lazy-src"); 109 | _i.removeAttribute("data-lazy-srcset"); 110 | _i.removeAttribute("data-lazy-state"); 111 | _i.classList.remove("lazyload"); 112 | _i.onload = function () { 113 | i.replaceWith(_i); 114 | }; 115 | } 116 | } catch {} 117 | }); 118 | [ 119 | ...postDiv.querySelectorAll( 120 | "a.extiw[title^='moe:'], a.extiw[title^='zhmoe:']" 121 | ), 122 | ].forEach(a => { 123 | a.classList.remove("extiw"); 124 | api.get({ 125 | action: "query", 126 | format: "json", 127 | titles: decodeURI(a.pathname.slice(1)), 128 | utf8: 1, 129 | formatversion: 2, 130 | }).done(b => { 131 | if (b.query.pages[0].missing) { 132 | a.href = `/index.php?title=${a.pathname.slice( 133 | 1 134 | )}&action=edit&redlink=1`; 135 | a.title += wgULS(" (页面不存在)", "(頁面不存在)"); 136 | a.classList += "new"; 137 | } else if ( 138 | b.query.pages[0].pageid === mw.config.get("wgArticleId") 139 | ) { 140 | a.href = ""; 141 | a.title = ""; 142 | a.classList += "mw-selflink selflink"; 143 | } else { 144 | a.href = a.pathname; 145 | a.title = a.title.replace(/moe:|zhmoe:/, ""); 146 | } 147 | }); 148 | }); 149 | [...postDiv.getElementsByTagName("script")].forEach(s => { 150 | const _s = document.createElement("script"); 151 | _s.innerHTML = s.innerHTML; 152 | [...s.attributes].forEach(a => { 153 | _s.setAttribute(a.name, a.value); 154 | }); 155 | s.parentNode.replaceChild(_s, s); 156 | }); 157 | return postDiv; 158 | }; 159 | mw.loader.using(["moment"]).done(() => { 160 | const commentCSS = document.createElement("style"); 161 | commentCSS.innerHTML = 162 | "#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}"; 163 | document.head.appendChild(commentCSS); 164 | const containerTop = document.createElement("div"); 165 | containerTop.className = "comment-container-top"; 166 | const container = document.createElement("div"); 167 | container.className = "comment-container"; 168 | const postContent = document.createElement("div"); 169 | postContent.id = "flowthread"; 170 | postContent.className = "post-content"; 171 | postContent.appendChild(containerTop); 172 | postContent.appendChild(container); 173 | document 174 | .getElementById( 175 | mw.config.get("skin") === "vector" 176 | ? "footer" 177 | : "moe-global-footer" 178 | ) 179 | .appendChild(postContent); 180 | fetch( 181 | `https://moegirl.uk/api.php?${new URLSearchParams({ 182 | action: "query", 183 | format: "json", 184 | prop: "pageprops", 185 | titles: mw.config.get("wgPageName"), 186 | utf8: 1, 187 | formatversion: 2, 188 | origin: "*", 189 | })}` 190 | ) 191 | .then(a => a.json()) 192 | .then(a => { 193 | if (a.query.pages[0].missing) return; 194 | (function getComment(offset) { 195 | fetch( 196 | `https://moegirl.uk/api.php?${new URLSearchParams({ 197 | action: "flowthread", 198 | format: "json", 199 | type: "list", 200 | pageid: a.query.pages[0].pageid, 201 | limit: 15, 202 | offset, 203 | utf8: 1, 204 | formatversion: 2, 205 | origin: "*", 206 | })}` 207 | ) 208 | .then(b => b.json()) 209 | .then(b => { 210 | if (b.flowthread.popular.length) { 211 | document.body.getElementsByClassName( 212 | "comment-container-top" 213 | )[0].innerHTML = "
热门评论
"; 214 | for (const post of b.flowthread.popular) { 215 | const _post = generatePost(post); 216 | _post.classList.add("comment-popular"); 217 | document.body 218 | .getElementsByClassName( 219 | "comment-container-top" 220 | )[0] 221 | .appendChild(_post); 222 | } 223 | } 224 | for (const post of b.flowthread.posts) { 225 | const _post = generatePost(post); 226 | _post.id = `comment-${post.id}`; 227 | if (post.parentid) { 228 | document 229 | .getElementById( 230 | `comment-${post.parentid}` 231 | ) 232 | .appendChild(_post); 233 | } else { 234 | document 235 | .getElementsByClassName( 236 | "comment-container" 237 | )[0] 238 | .appendChild(_post); 239 | } 240 | } 241 | if (b.flowthread.count > offset + 15) { 242 | new IntersectionObserver( 243 | (entries, observer) => { 244 | entries.forEach(entry => { 245 | if (entry.isIntersecting) { 246 | getComment(offset + 15); 247 | observer.unobserve( 248 | entry.target 249 | ); 250 | } 251 | }); 252 | } 253 | ).observe( 254 | document.querySelector( 255 | ".comment-container > div.comment-thread:last-of-type" 256 | ) 257 | ); 258 | } 259 | }); 260 | })(0); 261 | }) 262 | .catch(() => { 263 | fetch( 264 | "https://testingcf.jsdelivr.net/gh/gui-ying233/JustMoeComments/flowthread.json" 265 | ) 266 | .then(a => a.json()) 267 | .then(a => { 268 | let f = 0; 269 | for (const t of a) { 270 | if ( 271 | t.title !== 272 | mw.config.get("wgPageName").replace("_", " ") 273 | ) 274 | continue; 275 | a = t.posts; 276 | f = 1; 277 | break; 278 | } 279 | if (!f) return; 280 | for (const b of a) { 281 | if (+b.status) continue; 282 | const _post = generatePost({ 283 | username: b.username, 284 | text: b.text, 285 | timestamp: "", 286 | }); 287 | _post.id = `comment-${b.id}`; 288 | (b.parentid 289 | ? document.getElementById( 290 | `comment-${b.parentid}` 291 | ) 292 | : document.getElementsByClassName( 293 | "comment-container" 294 | )[0] 295 | ).appendChild(_post); 296 | } 297 | }); 298 | }); 299 | if (mw.config.get("skin") !== "moeskin") return; 300 | setTimeout(() => { 301 | if ( 302 | ![ 303 | (mw.config.get("wgPageName"), 304 | mw.config.get("wgPageName").replace(/^(.+)\(.+?\)$/, "$1"), 305 | document.getElementById("firstHeading").innerText), 306 | ].includes( 307 | document.body.getElementsByClassName("artwork-title")[0] 308 | ?.innerText 309 | ) 310 | ) 311 | return; 312 | document.body.getElementsByClassName("comment-item").forEach(c => { 313 | if ( 314 | !c.getElementsByClassName("comment-title")[0].innerText && 315 | !c.getElementsByClassName("comment-content")[0].innerText 316 | ) 317 | return; 318 | const post = { 319 | like: +c 320 | .getElementsByClassName("n-button-group")[0] 321 | .innerText.split("\n")[0], 322 | username: 323 | c.getElementsByClassName("comment-author")[0].innerText, 324 | text: 325 | c.getElementsByClassName("comment-title")[0].innerText + 326 | (c.getElementsByClassName("comment-title")[0] 327 | .innerText && 328 | c.getElementsByClassName("comment-content")[0].innerText 329 | ? document.createElement("br").outerHTML 330 | : "") + 331 | c.getElementsByClassName("comment-content")[0] 332 | .innerText, 333 | timestamp: 334 | c.getElementsByClassName("comment-time")[0].innerText, 335 | }; 336 | document 337 | .getElementsByClassName("comment-container")[0] 338 | .appendChild(generatePost(post)); 339 | }); 340 | }, 5000); 341 | }); 342 | })(); 343 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justmoecomments", 3 | "version": "2.15.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "justmoecomments", 9 | "version": "2.15.1", 10 | "license": "MIT" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justmoecomments", 3 | "version": "2.15.1", 4 | "description": "萌娘百科看Lih的镜像站的评论,同时集成了作品讨论的评论", 5 | "scripts": { 6 | "export": "node scripts/导出FlowThread评论" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/gui-ying233/JustMoeComments.git" 11 | }, 12 | "keywords": [ 13 | "tampermonkey", 14 | "comments", 15 | "moegirlpedia" 16 | ], 17 | "author": "鬼影233", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/gui-ying233/JustMoeComments/issues" 21 | }, 22 | "homepage": "https://github.com/gui-ying233/JustMoeComments" 23 | } 24 | -------------------------------------------------------------------------------- /scripts/导出FlowThread评论.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { 3 | env: { MOEGIRL_UK_MGPUSERID, MOEGIRL_UK_MGPTOKEN }, 4 | } = require("process"); 5 | const { writeFileSync } = require("fs"); 6 | fetch( 7 | "https://moegirl.uk/Special:%E5%AF%BC%E5%87%BAFlowThread%E8%AF%84%E8%AE%BA", 8 | { 9 | method: "POST", 10 | headers: { 11 | cookie: `mgpUserID=${MOEGIRL_UK_MGPUSERID}; mgpToken=${MOEGIRL_UK_MGPTOKEN}`, 12 | }, 13 | } 14 | ) 15 | .then(res => res.json()) 16 | .then(d => 17 | writeFileSync( 18 | "flowthread.json", 19 | JSON.stringify( 20 | d.map(({ title, posts }) => { 21 | return { 22 | title, 23 | posts: posts.filter(({ status }) => !status), 24 | }; 25 | }) 26 | ) 27 | ) 28 | ); 29 | --------------------------------------------------------------------------------