您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
共读 @ Bangumi。Ref: https://github.com/bangumi/scripts/tree/b0113743743dba35accb28e9b7b9da8cbbea6952/yonjar#%E7%94%A8%E6%88%B7%E8%AF%A6%E6%83%85%E7%88%AC%E5%8F%96
当前为
// ==UserScript== // @name Bangumi shared book collections // @namespace http://tampermonkey.net/ // @version 1.0.9 // @author txfs19260817 // @source https://github.com/txfs19260817/bangumi-shared-book-collections // @license WTFPL // @icon https://bangumi.tv/img/favicon.ico // @match http*://*.bangumi.tv/ // @match http*://*.bgm.tv/ // @match http*://*.chii.in/ // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @description 共读 @ Bangumi。Ref: https://github.com/bangumi/scripts/tree/b0113743743dba35accb28e9b7b9da8cbbea6952/yonjar#%E7%94%A8%E6%88%B7%E8%AF%A6%E6%83%85%E7%88%AC%E5%8F%96 // ==/UserScript== /******/ (() => { // webpackBootstrap /******/ "use strict"; var __webpack_exports__ = {}; ;// CONCATENATED MODULE: ./src/utils.ts const parseTimestamp = s => { var _s$match, _s$match2, _s$match3; if (!s.includes("ago")) { return new Date(s); } const now = new Date(); const d = ((_s$match = s.match(/(\d+)d/i)) === null || _s$match === void 0 ? void 0 : _s$match[1]) || "0"; const h = ((_s$match2 = s.match(/(\d+)h/i)) === null || _s$match2 === void 0 ? void 0 : _s$match2[1]) || "0"; const m = ((_s$match3 = s.match(/(\d+)m/i)) === null || _s$match3 === void 0 ? void 0 : _s$match3[1]) || "0"; now.setDate(now.getDate() - +d); now.setHours(now.getHours() - +h); now.setMinutes(now.getMinutes() - +m); return now; }; const fetchHTMLDocument = function (url) { let fetchMethod = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "GET"; return fetch(url, { method: fetchMethod, credentials: "include" }).then(r => r.text(), err => Promise.reject(err)).then(t => { const parser = new DOMParser(); return parser.parseFromString(t, "text/html"); }); }; function htmlToElement(html) { const template = document.createElement('template'); html = html.trim(); // Never return a text node of whitespace as the result template.innerHTML = html; return template.content.firstChild; } ;// CONCATENATED MODULE: ./src/CommentParser.ts function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } class CommentParser { constructor() { let max_pages = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 5; let max_results = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100; let show_stars = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; let watchlist = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; _defineProperty(this, "SHOW_STARS", true); _defineProperty(this, "WATCHLIST", []); _defineProperty(this, "defaultAvatarElem", userUrl => { const avatar = document.createElement("span"); const userAvatarAnchor = document.createElement("a"); const userAvatarSpan = document.createElement("span"); userAvatarSpan.classList.add("avatarNeue", "avatarReSize40", "ll"); userAvatarSpan.style.backgroundImage = 'url("//lain.bgm.tv/pic/user/l/icon.jpg")'; userAvatarAnchor.href = userUrl; userAvatarAnchor.appendChild(userAvatarSpan); avatar.classList.add("avatar"); avatar.appendChild(userAvatarAnchor); return avatar; }); _defineProperty(this, "sids2subjects", async sids => { const DOMs = await Promise.all(sids.map(s => fetchHTMLDocument(`${location.origin}/subject/${s}/comments`))); return DOMs.map((d, i) => ({ url: `${location.origin}/subject/${sids[i]}/comments`, title: d.querySelector("#headerSubject > h1 > a").text, cover: d.querySelector("#subject_inner_info > a > img").src })); }); _defineProperty(this, "fetchComments", async () => { // URL to the list of read books const readCollectionURL = `${location.origin}/book/list/${this.uid}/collect`; // The first page of the list of read books const firstPage = await fetchHTMLDocument(readCollectionURL); // Get the total page number, yet limit it to MAX_PAGES const maxPageNum = Math.min(this.MAX_PAGES, Math.max(...Array.from(document.getElementsByClassName('page_inner')[0].childNodes) // get paginator elements .filter(e => e instanceof HTMLAnchorElement && e.href.length > 0) // keep anchors w/ href .map(e => +(e.href.match(/[0-9]+$/)[0] || 1) // parse page numbers )) // get the maximum of the array ); // Get DOMs by URLs const readCollectionURLs = Array.from({ length: maxPageNum }, (_, i) => i + 1).map(i => `${readCollectionURL}?page=${i}`); // [1, ..., maxPageNum] readCollectionURLs.shift(); // [2, ..., maxPageNum] const pages = await Promise.all(readCollectionURLs.map(u => fetchHTMLDocument(u))); pages.unshift(firstPage); // all DOMs here // Get all subject URLs from the list, along with title and cover attributes const subjects = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => ({ url: c.firstElementChild.href + "/comments", title: c.getElementsByTagName('h3')[0].textContent.trim(), cover: c.getElementsByTagName('img')[0].src }))); // fetch watchlist subjects which are not shown in what we've already obtained const sidSet = new Set(subjects.map(s => s.url.slice(0, -9).match(/\d+$/)[0])); const filteredWatchlist = this.WATCHLIST.map(i => i.trim()).filter(i => !sidSet.has(i)); subjects.push(...(await this.sids2subjects(filteredWatchlist))); // Get DOMs by URLs const commentPageDOMs = await Promise.all(subjects.map(s => fetchHTMLDocument(s.url))); // Extract comment elements const commentElements = commentPageDOMs.map(p => Array.from(p.getElementsByClassName("text"))); // build an UID to Avatar element Map const avatarElements = commentPageDOMs.flatMap(p => Array.from(p.querySelectorAll('#comment_box > div > .avatar'))); const uid2avatar = new Map(); avatarElements.forEach(e => { // adjust avatar span class e.firstElementChild.classList.replace("rr", "ll"); e.firstElementChild.classList.replace("avatarSize32", "avatarReSize40"); e.firstElementChild.style.marginLeft = '6px'; // parse username from href const username = e.href.split('/').at(-1); uid2avatar.set(username, e); }); const data = commentElements.flatMap((cs, i) => { return cs.map(c => { var _c$getElementsByClass, _c$getElementsByClass2; const userAnchor = c.firstElementChild; return { subjectUrl: subjects[i].url, subjectTitle: subjects[i].title, subjectCover: subjects[i].cover, userAvatarElement: uid2avatar.get(userAnchor.href.split('/').at(-1)) ?? this.defaultAvatarElem(userAnchor.href), userUrl: userAnchor.href, username: userAnchor.text, date: parseTimestamp(c.getElementsByTagName('small')[0].textContent.slice(2)), comment: c.getElementsByTagName('p')[0].textContent, stars: ((_c$getElementsByClass = c.getElementsByClassName("starlight")[0]) === null || _c$getElementsByClass === void 0 ? void 0 : (_c$getElementsByClass2 = _c$getElementsByClass.classList.value.match(/\d+/)) === null || _c$getElementsByClass2 === void 0 ? void 0 : _c$getElementsByClass2[0]) ?? 0 }; }).filter(c => !c.userUrl.includes(this.uid)); // exclude users themselves }); data.sort((a, b) => +b.date - +a.date); return data.slice(0, this.MAX_RESULTS); }); _defineProperty(this, "commentDataToTLList", data => { const ul = document.createElement("ul"); const lis = data.map(d => { const li = document.createElement("li"); li.classList.add("clearit", "tml_item"); // avatar li.appendChild(d.userAvatarElement); // info const info = document.createElement("span"); info.classList.add("clearit", "info"); // info - cover const coverAnchor = document.createElement("a"); const coverImg = document.createElement("img"); coverImg.classList.add("rr"); coverImg.src = d.subjectCover; coverImg.height = 48; coverImg.width = 48; coverAnchor.appendChild(coverImg); info.appendChild(coverAnchor); // info - username const userAnchor = document.createElement("a"); userAnchor.href = d.userUrl; userAnchor.textContent = d.username; userAnchor.classList.add("l"); info.appendChild(userAnchor); const connector = document.createElement("span"); connector.textContent = " 读过 "; info.appendChild(connector); // info - subject const subjectAnchor = document.createElement("a"); subjectAnchor.href = d.subjectUrl; subjectAnchor.textContent = d.subjectTitle; subjectAnchor.classList.add("l"); info.appendChild(subjectAnchor); // info - comment const collectInfo = document.createElement("div"); collectInfo.classList.add("collectInfo"); const quoteDiv = document.createElement("div"); quoteDiv.classList.add("quote"); const quoteQ = document.createElement("q"); quoteQ.textContent = d.comment; quoteDiv.appendChild(quoteQ); if (this.SHOW_STARS && d.stars > 0) { const starSpan = document.createElement("span"); starSpan.classList.add("starstop-s"); const starlightSpan = document.createElement("span"); starlightSpan.classList.add("starlight", `stars${d.stars}`); starSpan.appendChild(starlightSpan); collectInfo.appendChild(starSpan); } collectInfo.appendChild(quoteDiv); info.appendChild(collectInfo); // info - date const dateP = document.createElement("p"); dateP.classList.add("date"); dateP.textContent = d.date.toLocaleString(); info.appendChild(dateP); // info done li.appendChild(info); return li; }); lis.forEach(l => { ul.appendChild(l); }); return ul; }); this.MAX_PAGES = max_pages; this.MAX_RESULTS = max_results; this.SHOW_STARS = show_stars; this.WATCHLIST = watchlist; this.uid = CommentParser.getUID(); } } _defineProperty(CommentParser, "getUID", () => document.querySelector("#headerNeue2 > div > div.idBadgerNeue > a").href.split("user/")[1]); ;// CONCATENATED MODULE: ./src/Dialog.ts const createSettingsDialog = () => { const dialog = htmlToElement(` <dialog id="dialog"> <form id="dialog-form" method="dialog"> <h2>共读设置</h2> <h3>提交后请刷新以生效改动</h3> <div> <label for="maxpages">获取最近读过的前多少页条目的评论:</label> <input id="maxpages" name="maxpages" type="number" value="${GM_getValue("maxpages") || cp.MAX_PAGES}" min="1" /> </div> <div> <label for="maxresults">最多显示评论的数目:</label> <input id="maxresults" name="maxresults" type="number" value="${GM_getValue("maxresults") || cp.MAX_RESULTS}" min="1" /> </div> <div> <label for="showstars">显示评分:</label> <input type="hidden" name="showstars" value="false" /> <input id="showstars" name="showstars" type="checkbox" value="true" ${GM_getValue("showstars") ? "checked" : ""} /> </div> <div> <label for="disablesettings">不在首页显示设置按钮:</label> <input type="hidden" name="disablesettings" value="false" /> <input id="disablesettings" name="disablesettings" type="checkbox" value="true" ${GM_getValue("disablesettings") ? "checked" : ""} /> <p style="color: gray;">(控制设置按钮在首页的可见性,选中后仍可在Tampermonkey类插件中设置)</p> </div> <div> <label for="watchlist">关注列表(每行一个条目数字id,列表中的条目的最新评论一定会被收集):</label> <br /> <textarea id="watchlist" name="watchlist" class="quick" rows="6" cols="10" placeholder="例:\n326125\n329803">${GM_getValue("watchlist").map(s => s.trim()).join("\n")}</textarea> </div> <div> <button type="submit">Submit</button> <button type="reset">Reset</button> <button type="button" onclick="document.getElementById('dialog').close()">Close</button> </div> </form> </dialog>`); dialog.firstElementChild.addEventListener("submit", function (e) { e.preventDefault(); const data = new FormData(e.target); [...data.entries()].forEach(kv => { const k = kv[0]; let v = kv[1]; if (k === "watchlist") { v = kv[1].split("\n").filter(n => Number.isInteger(Number(n)) && Number(n) > 0); } else if (k === "showstars" || k === "disablesettings") { v = v === "true"; } GM_setValue(k, v); }); dialog.close(); }); // dialog style dialog.style.borderRadius = "12px"; dialog.style.borderColor = "#F09199"; dialog.style.boxShadow = "0 0 #0000, 0 0 #0000, 0 25px 50px -12px rgba(0, 0, 0, 0.25)"; // inject dialog element document.body.appendChild(dialog); // userscript menu GM_registerMenuCommand("设置", () => { dialog.showModal(); }); }; ;// CONCATENATED MODULE: ./src/TabItem.ts function TabItem_defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } class TabItem { constructor() { let disable_settings = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; TabItem_defineProperty(this, "states", { loading: { text: "⏳", cursor: "wait" }, done: { text: "共读", cursor: "pointer" } }); TabItem_defineProperty(this, "li", document.createElement("li")); TabItem_defineProperty(this, "a", document.createElement("a")); this.DISABLE_SETTINGS = disable_settings; // initialize this.a.id = "tab_bsbc"; this.applyState(this.states.loading); this.li.appendChild(this.a); document.getElementById('timelineTabs').appendChild(this.li); } settingAnchor() { const a = document.createElement("a"); a.text = "⚙️设置"; a.style.cursor = "pointer"; a.onclick = function () { document.getElementById("dialog").showModal(); }; const li = document.createElement("li"); li.appendChild(a); return li; } applyState(state) { this.a.text = state.text; this.a.style.cursor = state.cursor; } loaded() { for (var _len = arguments.length, nodes = new Array(_len), _key = 0; _key < _len; _key++) { nodes[_key] = arguments[_key]; } this.applyState(this.states.done); // add onclick handler const a = this.a; this.a.onclick = function () { if (a.classList.contains("focus")) return; ["tab_all", "tab_say", "tab_subject", "tab_progress", "tab_blog"].forEach(id => { document.getElementById(id).classList.remove("focus"); }); a.classList.add("focus"); document.getElementById("timeline").replaceChildren(...nodes); }; if (!this.DISABLE_SETTINGS) { // add settings button document.getElementById('timelineTabs').appendChild(this.settingAnchor()); } } } ;// CONCATENATED MODULE: ./src/index.ts async function main() { const tabItem = new TabItem(!!GM_getValue("disablesettings")); const cp = new CommentParser(GM_getValue("maxpages"), GM_getValue("maxresults"), !!GM_getValue("showstars"), GM_getValue("watchlist")); cp.fetchComments().then(data => { createSettingsDialog(); tabItem.loaded(cp.commentDataToTLList(data)); // TODO: pagination? }); } main().catch(e => { console.error(e); }); /******/ })() ;
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址