您需要先安装一个扩展,例如 篡改猴、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.5 // @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) { var 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; _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, "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 }))); // 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 (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.uid = CommentParser.getUID(); } } _defineProperty(CommentParser, "getUID", () => document.querySelector("#headerNeue2 > div > div.idBadgerNeue > a").href.split("user/")[1]); ;// 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() { 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")); // initialize this.a.id = "tab_bsbc"; this.applyState(this.states.loading); this.li.appendChild(this.a); document.getElementById('timelineTabs').appendChild(this.li); } applyState(state) { this.a.text = state.text; this.a.style.cursor = state.cursor; } loaded() { this.applyState(this.states.done); } } ;// CONCATENATED MODULE: ./src/index.ts async function main() { const tabItem = new TabItem(); const cp = new CommentParser(GM_getValue("maxpages"), GM_getValue("maxresults")); cp.fetchComments().then(data => { tabItem.loaded(); // TODO: pagination? // FIXME: ugly access tabItem.a.onclick = function () { ["tab_all", "tab_say", "tab_subject", "tab_progress", "tab_blog", tabItem.a.id].forEach(id => { document.getElementById(id).classList.remove("focus"); }); tabItem.a.classList.add("focus"); document.getElementById("timeline").replaceChildren(cp.commentDataToTLList(data)); }; }); const dialog = htmlToElement(`<dialog id="dialog"><form id="dialog-form" method="dialog"><p>提交后请刷新以生效设置</p><div><label for="maxpages">获取最近读过的前多少页条目的评论:</label> <input id="maxpages" name="maxpages" type="number" value="${GM_getValue("maxpages") || cp.MAX_PAGES}"/></div><div><label for="maxresults">最多显示评论的数目:</label> <input id="maxresults" name="maxresults" type="number" value="${GM_getValue("maxresults") || cp.MAX_RESULTS}"/></div><div class="buttons-wrapper"> <button type="submit">Submit</button> <button type="reset">Reset</button> </form></dialog>`); dialog.firstElementChild.addEventListener("submit", function (e) { e.preventDefault(); const data = new FormData(e.target); [...data.entries()].forEach(kv => { GM_setValue(kv[0], kv[1]); }); dialog.close(); }); document.body.appendChild(dialog); GM_registerMenuCommand("设置", () => { dialog.showModal(); }); } main().catch(e => { console.log(e); }); /******/ })() ;
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址