CC98 Tools - TreeView

View posts in tree view

  1. // ==UserScript==
  2. // @name CC98 Tools - TreeView
  3. // @version 0.0.1
  4. // @description View posts in tree view
  5. // @icon https://www.cc98.org/static/98icon.ico
  6.  
  7. // @author ml98
  8. // @namespace https://www.cc98.org/user/name/ml98
  9. // @license MIT
  10.  
  11. // @match https://www.cc98.org/*
  12. // @match http://www-cc98-org-s.webvpn.zju.edu.cn:8001/*
  13. // @match https://www-cc98-org-s.webvpn.zju.edu.cn:8001/*
  14. // @run-at document-start
  15. // @grant none
  16. // ==/UserScript==
  17.  
  18. const _all_posts = [];
  19. const resolve = (url, data) => {
  20. if (/\/Topic\/\d+\/post/.test(url)) {
  21. const topic_id = url.match(/\/Topic\/(\d+)/)[1];
  22. (_all_posts[topic_id] ??= []).push(...data);
  23. const map = new Map(_all_posts[topic_id].map(i => [i.id, i]));
  24. _all_posts[topic_id] = [...map.values()];
  25. data = _all_posts[topic_id].sort((i,j)=>i.id-j.id);
  26. }
  27. return data;
  28. };
  29.  
  30. const json = Response.prototype.json;
  31. Response.prototype.json = function () {
  32. return json.call(this).then((data) => resolve(this.url, data));
  33. };
  34.  
  35. const style = document.createElement('style');
  36. document.head.append(style);
  37.  
  38. waitForElement(".center > .center", function(e) {
  39. const m = location.href.match(/\/topic\/(\d+)(?:\/(\d+)(?:#(\d+))?)?/);
  40. if(!m) {
  41. return;
  42. }
  43. const topic_id = m[1];
  44. const floor = ((m[2] || 1) - 1) * 10 + (m[3] || 1) % 10;
  45. const posts = _all_posts[topic_id];
  46.  
  47. const children = {}, parent = {}, level = {}, order = {};
  48. posts.forEach(i => { parent[i.id] = i.parentId });
  49. posts.forEach(i => {
  50. const p = parent[i.parentId] >= 0 ? i.parentId : 0;
  51. (children[p] ??= []).push(i.id);
  52. });
  53. let index = 0;
  54. (function traverse(id, depth) {
  55. level[id] = depth;
  56. order[id] = index++;
  57. children[id]?.forEach(id => traverse(id, depth+1));
  58. })(0, -1);
  59.  
  60. style.textContent = `
  61. .center > .center { ${posts.map(i => `
  62. --indent-${i.id}: calc(${level[i.id]} * var(--indent));
  63. --order-${i.id}: ${order[i.id]}; `).join('')}
  64. --indent: 4em;
  65. }
  66. .center > .center > .reply article > div[style^="background-color: rgb(245, 250, 255)"]:first-child {
  67. /* display: none; */
  68. }`;
  69.  
  70. setTimeout(() => {
  71. [...e.children].find(i => React(i.querySelector('.reply-content')).props.floor >= floor)
  72. ?.scrollIntoView({block: 'nearest', behavior: 'smooth'});
  73. }, 1000);
  74. });
  75.  
  76. waitForElement(".center > .center > .reply", function(e) {
  77. const id = React(e.querySelector('.reply-content')).props.postId;
  78. e.style = `
  79. margin-left: var(--indent-${id}, 0);
  80. width: calc(100% - var(--indent-${id}, 0)) !important;
  81. order: var(--order-${id});
  82. `;
  83. });
  84.  
  85. /* helper functions */
  86. function React(dom) {
  87. const key = Object.keys(dom).find((key) => key.startsWith("__reactInternalInstance$"));
  88. const instance = dom[key];
  89. return (
  90. instance?._debugOwner?.stateNode ||
  91. instance?.return?.stateNode ||
  92. instance?._currentElement?._owner?._instance ||
  93. null
  94. );
  95. }
  96.  
  97. function waitForElement(selector, callback, startNode = document) {
  98. const uid = "_" + Math.random().toString().slice(2);
  99. selector = `:is(${selector}):not([${uid}])`;
  100. const observer = new MutationObserver(mutations => {
  101. for (const mutation of mutations) {
  102. for (const node of mutation.addedNodes) {
  103. if (node.nodeType == 1) {
  104. if (node.matches(selector)) {
  105. node.setAttribute(uid, "");
  106. callback(node);
  107. }
  108. node.querySelectorAll(selector).forEach(child => {
  109. child.setAttribute(uid, "");
  110. callback(child);
  111. });
  112. }
  113. }
  114. }
  115. });
  116. observer.observe(startNode, {
  117. attributes: true,
  118. childList: true,
  119. subtree: true,
  120. });
  121. }