View posts in tree view
// ==UserScript==
// @name CC98 Tools - TreeView
// @version 0.0.1
// @description View posts in tree view
// @icon https://www.cc98.org/static/98icon.ico
// @author ml98
// @namespace https://www.cc98.org/user/name/ml98
// @license MIT
// @match https://www.cc98.org/*
// @match http://www-cc98-org-s.webvpn.zju.edu.cn:8001/*
// @match https://www-cc98-org-s.webvpn.zju.edu.cn:8001/*
// @run-at document-start
// @grant none
// ==/UserScript==
const _all_posts = [];
const resolve = (url, data) => {
if (/\/Topic\/\d+\/post/.test(url)) {
const topic_id = url.match(/\/Topic\/(\d+)/)[1];
(_all_posts[topic_id] ??= []).push(...data);
const map = new Map(_all_posts[topic_id].map(i => [i.id, i]));
_all_posts[topic_id] = [...map.values()];
data = _all_posts[topic_id].sort((i,j)=>i.id-j.id);
}
return data;
};
const json = Response.prototype.json;
Response.prototype.json = function () {
return json.call(this).then((data) => resolve(this.url, data));
};
const style = document.createElement('style');
document.head.append(style);
waitForElement(".center > .center", function(e) {
const m = location.href.match(/\/topic\/(\d+)(?:\/(\d+)(?:#(\d+))?)?/);
if(!m) {
return;
}
const topic_id = m[1];
const floor = ((m[2] || 1) - 1) * 10 + (m[3] || 1) % 10;
const posts = _all_posts[topic_id];
const children = {}, parent = {}, level = {}, order = {};
posts.forEach(i => { parent[i.id] = i.parentId });
posts.forEach(i => {
const p = parent[i.parentId] >= 0 ? i.parentId : 0;
(children[p] ??= []).push(i.id);
});
let index = 0;
(function traverse(id, depth) {
level[id] = depth;
order[id] = index++;
children[id]?.forEach(id => traverse(id, depth+1));
})(0, -1);
style.textContent = `
.center > .center { ${posts.map(i => `
--indent-${i.id}: calc(${level[i.id]} * var(--indent));
--order-${i.id}: ${order[i.id]}; `).join('')}
--indent: 4em;
}
.center > .center > .reply article > div[style^="background-color: rgb(245, 250, 255)"]:first-child {
/* display: none; */
}`;
setTimeout(() => {
[...e.children].find(i => React(i.querySelector('.reply-content')).props.floor >= floor)
?.scrollIntoView({block: 'nearest', behavior: 'smooth'});
}, 1000);
});
waitForElement(".center > .center > .reply", function(e) {
const id = React(e.querySelector('.reply-content')).props.postId;
e.style = `
margin-left: var(--indent-${id}, 0);
width: calc(100% - var(--indent-${id}, 0)) !important;
order: var(--order-${id});
`;
});
/* helper functions */
function React(dom) {
const key = Object.keys(dom).find((key) => key.startsWith("__reactInternalInstance$"));
const instance = dom[key];
return (
instance?._debugOwner?.stateNode ||
instance?.return?.stateNode ||
instance?._currentElement?._owner?._instance ||
null
);
}
function waitForElement(selector, callback, startNode = document) {
const uid = "_" + Math.random().toString().slice(2);
selector = `:is(${selector}):not([${uid}])`;
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType == 1) {
if (node.matches(selector)) {
node.setAttribute(uid, "");
callback(node);
}
node.querySelectorAll(selector).forEach(child => {
child.setAttribute(uid, "");
callback(child);
});
}
}
}
});
observer.observe(startNode, {
attributes: true,
childList: true,
subtree: true,
});
}