// ==UserScript==
// @name Bangumi shared book collections
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @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.xmlHttpRequest
// @run-at document-end
// @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/TabItem.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 TabItem {
constructor() {
_defineProperty(this, "states", {
loading: {
text: "⏳",
cursor: "wait"
},
done: {
text: "共读",
cursor: "pointer"
}
});
_defineProperty(this, "li", document.createElement("li"));
_defineProperty(this, "a", document.createElement("a"));
// initialize
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/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 getUID = () => {
return document.querySelector("#headerNeue2 > div > div.idBadgerNeue > a").href.split("user/")[1];
};
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");
});
};
const 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;
};
const MAX_PAGES = 5;
const fetchComments = async uid => {
const url = `${location.origin}/book/list/${uid}/collect`;
const firstPage = await fetchHTMLDocument(url); // TODO: latest pages
const maxPageNum = Math.min(MAX_PAGES, firstPage.getElementsByClassName('page_inner')[0].childElementCount - 1);
const pageURLs = Array.from({
length: maxPageNum
}, (_, i) => i + 1).map(i => `${url}?page=${i}`);
pageURLs.shift(); // [2, ..., maxPageNum]
const pages = await Promise.all(pageURLs.map(u => fetchHTMLDocument(u)));
pages.unshift(firstPage);
const subjectUrls = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => c.firstElementChild.href + "/comments"));
const subjectCovers = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => c.getElementsByTagName('img')[0].src));
const commentPageDOMs = await Promise.all(subjectUrls.map(u => fetchHTMLDocument(u)));
const commentElements = commentPageDOMs.map(p => Array.from(p.getElementsByClassName("text")));
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 subjectTitles = pages.flatMap(page => Array.from(page.getElementById("browserItemList").children).map(c => c.getElementsByTagName('h3')[0].textContent.trim()));
const data = commentElements.flatMap((cs, i) => {
return cs.map(c => {
const userAnchor = c.firstElementChild;
return {
subjectUrl: subjectUrls[i],
subjectTitle: subjectTitles[i],
subjectCover: subjectCovers[i],
userAvatarElement: uid2avatar.get(userAnchor.href.split('/').at(-1)) ?? defaultAvatarElem(userAnchor.href),
userUrl: userAnchor.href,
username: userAnchor.text,
date: parseTimestamp(c.getElementsByTagName('small')[0].textContent.slice(2)),
comment: c.getElementsByTagName('p')[0].textContent
};
}).filter(c => !c.userUrl.includes(uid));
});
data.sort((a, b) => +b.date - +a.date);
return data;
};
const 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);
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;
};
;// CONCATENATED MODULE: ./src/index.ts
async function main() {
const uid = getUID();
const tabItem = new TabItem();
fetchComments(uid).then(r => {
tabItem.loaded();
const tl = document.getElementById("timeline"); // TODO: pagination?
// FIXME: ugly access
tabItem.a.onclick = function () {
tabItem.a.classList.add("focus");
tl.replaceChildren(commentDataToTLList(r.slice(0, 100)));
};
});
}
main().catch(e => {
console.log(e);
});
/******/ })()
;