kbin enhancement script

Few small changes to the kbin UI while they still develop some features

目前為 2023-06-21 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name kbin enhancement script
  3. // @description Few small changes to the kbin UI while they still develop some features
  4. // @namespace com.sirpsychomantis
  5. // @license MIT
  6. // @version 1.8
  7. // @grant none
  8. // @run-at document-end
  9. // @match https://fedia.io/*
  10. // @match https://kbin.social/*
  11. // ==/UserScript==
  12.  
  13.  
  14. (function(){
  15. const version = "1.8";
  16. const style = document.createElement('style');
  17. style.textContent = `
  18. .entry figure {overflow: hidden}
  19. #kes-version-dialog {position: fixed; width: 100vw; height: 100vh; top: 0; left: 0; display: flex; align-items: center; justify-content: center; background-color: rgba(0,0,0,.3); z-index: 9999999}
  20. .kes-version-dialog-content {background: #ddd; color: #444; position: relative; padding: 40px}
  21. .kes-expand {grid-gap: 0; padding-left:55px}
  22. .kes-blur {filter: blur(4px); transition-duration: 0.5s}
  23. .kes-blur-large {filter: blur(15px)}
  24. .kes-blur:hover {filter: none}
  25. `;
  26. document.head.appendChild(style);
  27. const allSettings = [
  28. {name: "Show domains", value:"show-domains"},
  29. {name: "Show collapse comment", value:"show-collapse"},
  30. {name: "Show collapse replies", value:"show-collapse-replies"},
  31. {name: "Replies start collapsed", value:"start-collapse-replies", default: "false"},
  32. {name: "Move comment box to top", value:"comment-box-top"},
  33. {name: "Reply box improvements", value:"comment-cancel"},
  34. {name: "Hide known NSFW domains", value:"nsfw-hide"},
  35. {name: "Blur known NSFW domains", value:"nsfw-blur"},
  36. {name: "Hide random sidebar posts", value:"hide-random"}
  37. ];
  38. allSettings.forEach(setting => {
  39. if (setting.default === "false" && localStorage.getItem("setting-" + setting.value) === null) {
  40. localStorage.setItem("setting-" + setting.value, "false");
  41. }
  42. });
  43. function getSetting(setting) {
  44. let value = localStorage.getItem("setting-" + setting);
  45. if (value === null)
  46. value = "true";
  47. return value === "true";
  48. }
  49. function setSetting(setting, value) {
  50. localStorage.setItem("setting-" + setting, value);
  51. location.reload();
  52. }
  53. function addDomain(link) {
  54. const parts = link.title.split("@");
  55. if (parts[2] !== location.hostname && !link.innerText.includes("@" + parts[2])) {
  56. const linkText = link.childNodes[link.childNodes.length-1];
  57. linkText.nodeValue += "@" + parts[2];
  58. }
  59. }
  60. function addDomains() {
  61. document.querySelectorAll(".magazine-inline, .user-inline").forEach(link => {
  62. addDomain(link);
  63. });
  64. const config = { childList: true };
  65. const callback = (mutationList, observer) => {
  66. for (const mutation of mutationList) {
  67. mutation.addedNodes.forEach(container => {
  68. container.querySelectorAll(".magazine-inline, .user-inline").forEach(link => {
  69. addDomain(link);
  70. });
  71. });
  72. }
  73. };
  74.  
  75. const observer = new MutationObserver(callback);
  76. observer.observe(document.querySelector("div#content > div"), config);
  77. }
  78. function getComments(comment, allComments) {
  79. const id = comment.id.split('-')[2];
  80. allComments.push(comment);
  81. const subComments = comment.parentElement.querySelectorAll('blockquote[data-subject-parent-value="'+id+'"]');
  82. subComments.forEach(blockquote => { getComments(blockquote, allComments); });
  83. }
  84. function getCollapsos(comment, allCollapsos) {
  85. const id = comment.id.split('-')[2];
  86. if (comment.classList.contains('kes-expand'))
  87. allCollapsos.push(comment);
  88. const subComments = comment.parentElement.querySelectorAll('blockquote[data-subject-parent-value="'+id+'"]');
  89. subComments.forEach(blockquote => { getCollapsos(blockquote, allCollapsos); });
  90. }
  91. function removeAllCollapsos(blockquote) {
  92. // Just remove all these for now, don't want to figure out how to do this cleanly right now.
  93. const allCollapsos = [];
  94. getCollapsos(blockquote, allCollapsos);
  95. allCollapsos.forEach(comment => { comment.remove() });
  96. }
  97. function expandComment(blockquote) {
  98. const allComments = [];
  99. getComments(blockquote, allComments);
  100. allComments.forEach(comment => { comment.style.display="" });
  101. removeAllCollapsos(blockquote);
  102. }
  103. function collapseComment(blockquote) {
  104. const id = blockquote.id.split('-')[2];
  105. let commentLevel = "1";
  106. blockquote.classList.forEach(classItem => {
  107. if (classItem.includes("comment-level"))
  108. commentLevel = classItem.split("--")[1];
  109. });
  110. const allComments = [];
  111. getComments(blockquote, allComments);
  112. allComments.forEach(comment => { comment.style.display="none" });
  113.  
  114. const username = blockquote.querySelector("header a").innerText;
  115. const time = blockquote.querySelector("header time").innerText;
  116.  
  117. const newBlockquote = document.createElement('blockquote');
  118. newBlockquote.className = 'kes-expand section comment entry-comment comment-level--' + commentLevel;
  119. newBlockquote.dataset.subjectParentValue = id;
  120. newBlockquote.innerHTML = '<header><a href="javascript:;">' + username + ', ' + time + ' [+]</a></header>';
  121. newBlockquote.querySelector('a').addEventListener("click", () => {expandComment(blockquote)});
  122. blockquote.parentNode.insertBefore(newBlockquote, blockquote);
  123. }
  124. function getTotalReplyCount(blockquote) {
  125. const allComments = [];
  126. getComments(blockquote, allComments);
  127. return allComments.length - 1;
  128. }
  129. function addCollapseLinks() {
  130. if (location.pathname.startsWith('/m')) {
  131. const comments = document.querySelectorAll("blockquote.comment");
  132. comments.forEach(blockquote => {
  133. const menu = blockquote.querySelector("header");
  134. if (!menu.innerText.includes('[-]')) {
  135. const newA = document.createElement('a');
  136. newA.href = "javascript:;";
  137. newA.className = "kes-collapse";
  138. newA.innerHTML = '[-]';
  139. menu.appendChild(newA);
  140. }
  141. });
  142. document.querySelectorAll(".kes-collapse").forEach(link => {link.addEventListener("click", () => {
  143. const blockquote = link.closest("blockquote.comment");
  144. collapseComment(blockquote);
  145. })});
  146. }
  147. }
  148. function getAllReplies(blockquote) {
  149. const allComments = [];
  150. getComments(blockquote, allComments);
  151. allComments.splice(allComments.indexOf(blockquote), 1);
  152. return allComments;
  153. }
  154. function toggleReplies(blockquote, display) {
  155. const id = blockquote.id.split('-')[2];
  156. const allReplies = getAllReplies(blockquote);
  157. let anyHidden = false;
  158. allReplies.forEach(reply => {
  159. if (reply.style.display == 'none')
  160. anyHidden = true;
  161. });
  162. allReplies.forEach(comment => { comment.style.display = anyHidden ? '' : 'none' });
  163. removeAllCollapsos(blockquote);
  164. }
  165. function addCollapseRepliesLinks() {
  166. if (location.pathname.startsWith('/m')) {
  167. const comments = document.querySelectorAll("blockquote.comment-level--1");
  168. comments.forEach(blockquote => {
  169. const id = blockquote.id.split('-')[2];
  170. const subComments = blockquote.parentElement.querySelectorAll('blockquote[data-subject-parent-value="'+id+'"]');
  171. if (subComments.length > 0) {
  172. const menu = blockquote.querySelector("footer menu");
  173. const newLi = document.createElement('li');
  174. newLi.innerHTML = '<a href="javascript:;" class="kes-collapse-replies">toggle replies ('+getTotalReplyCount(blockquote)+')</a>';
  175. menu.appendChild(newLi);
  176. }
  177. });
  178. document.querySelectorAll(".kes-collapse-replies").forEach(link => {link.addEventListener("click", () => {
  179. const blockquote = link.closest("blockquote.comment");
  180. toggleReplies(blockquote);
  181. })});
  182. }
  183. }
  184. function collapseAllReplies() {
  185. const comments = document.querySelectorAll("blockquote.comment-level--2");
  186. comments.forEach(blockquote => {
  187. collapseComment(blockquote);
  188. });
  189. }
  190. function moveCommentBox() {
  191. const commentAdd = document.querySelector('#comment-add');
  192. if (commentAdd)
  193. commentAdd.parentNode.insertBefore(commentAdd, document.querySelector('#comments'));
  194. }
  195. function removeReplyBox(container) {
  196. container.innerHTML = '';
  197. container.style = '';
  198. }
  199. function addCommentCancelButton(container) {
  200. const list = container.querySelector('div.actions ul');
  201. const newLi = document.createElement('li');
  202. newLi.innerHTML = '<div><button class="btn btn__primary">Cancel</button></div>';
  203. list.appendChild(newLi);
  204. newLi.querySelector('button').addEventListener("click", () => { removeReplyBox(container) });
  205. }
  206. function fixMarkdownButtons(form) {
  207. const formActionSplit = form.action.split('/');
  208. const newId = 'entry_comment_body_' + formActionSplit[formActionSplit.length-1];
  209. form.querySelector('#entry_comment_body').id = newId;
  210. form.querySelector('markdown-toolbar').setAttribute('for', newId);
  211. }
  212. function observeReplyAdded() {
  213. const config = { childList: true };
  214. const callback = (mutationList, observer) => {
  215. for (const mutation of mutationList) {
  216. const container = mutation.target;
  217. const form = container.querySelector('form.comment-add');
  218. if (form !== null) {
  219. fixMarkdownButtons(form);
  220. addCommentCancelButton(container);
  221. }
  222. }
  223. };
  224.  
  225. const observer = new MutationObserver(callback);
  226. document.querySelectorAll('blockquote.comment footer div.js-container').forEach(container => { observer.observe(container, config) })
  227. }
  228. // Thanks u/le__el
  229. function addBlur(container) {
  230. container.querySelectorAll("img").forEach(el => {
  231. el.classList.add('kes-blur');
  232. });
  233. container.querySelectorAll("figure img").forEach(el => {
  234. el.classList.add('kes-blur-large');
  235. });
  236. }
  237. const nsfwDomains = [
  238. "lemmynsfw.com",
  239. "redgifs.com"
  240. ];
  241. function hideNSFW() {
  242. document.querySelectorAll("article").forEach(article => {
  243. if (article.querySelector("small.danger") !== null
  244. || nsfwDomains.includes(article.querySelector(".entry__domain a").innerText)
  245. || nsfwDomains.includes(article.querySelector(".magazine-inline").title.split('@')[2])) {
  246. if (getSetting("nsfw-hide")) {
  247. article.remove();
  248. } else {
  249. addBlur(article);
  250. }
  251. }
  252. });
  253. }
  254. function hideRandom() {
  255. document.querySelector('#sidebar section.posts').style = "display:none;"
  256. document.querySelector('#sidebar section.entries').style = "display:none;"
  257. }
  258. function generateSettingDiv(settingDisplay, setting) {
  259. const settingValue = getSetting(setting);
  260. const newDiv = document.createElement('div');
  261. newDiv.className = "row";
  262. newDiv.innerHTML = `<span>${settingDisplay}:</span>
  263. <div>
  264. <a class="kes-setting-yes link-muted ${settingValue ? 'active' : ''}" href="javascript:;" data-setting="${setting}">
  265. Yes
  266. </a>
  267. |
  268. <a class="kes-setting-no link-muted ${settingValue ? '' : 'active'}" href="javascript:;" data-setting="${setting}">
  269. No
  270. </a>
  271. </div>`;
  272. return newDiv;
  273. }
  274. function addHTMLSettings() {
  275. const settingsList = document.querySelector(".settings-list");
  276. const header = document.createElement('strong');
  277. header.textContent = "kbin enhancement script";
  278. settingsList.appendChild(header);
  279. allSettings.forEach(setting => { settingsList.appendChild(generateSettingDiv(setting.name, setting.value)) });
  280. document.querySelectorAll(".kes-setting-yes").forEach(link => { link.addEventListener("click", () => {setSetting(link.dataset.setting, true) })});
  281. document.querySelectorAll(".kes-setting-no").forEach(link => { link.addEventListener("click", () => {setSetting(link.dataset.setting, false) })});
  282. }
  283. addHTMLSettings();
  284. if (getSetting("show-domains"))
  285. addDomains();
  286. if (getSetting("show-collapse"))
  287. addCollapseLinks();
  288. if (getSetting("show-collapse-replies"))
  289. addCollapseRepliesLinks();
  290. if (getSetting("start-collapse-replies"))
  291. collapseAllReplies();
  292. if (getSetting("comment-box-top"))
  293. moveCommentBox();
  294. if (getSetting("comment-cancel"))
  295. observeReplyAdded();
  296. if (getSetting("nsfw-blur") || getSetting("nsfw-hide"))
  297. hideNSFW();
  298. if (getSetting("hide-random"))
  299. hideRandom();
  300. if (localStorage.getItem("setting-changelog-version") != version) {
  301. const message = `<strong>kbin enhancement script version: ${version}</strong><br>
  302. Thanks for downloading! You can always toggle on and off features in the kbin sidebar settings.<br>Recent changes:
  303. <ul>
  304. <li>Hide random sidebar</li>
  305. <li>Fixed infinite scroll not showing domains</li>
  306. <li>Additional NSFW protection</li>
  307. <li>Fixed markdown buttons and added "Cancel" when replying</li>
  308. </ul>
  309. `
  310. const versionDiv = document.createElement('div');
  311. versionDiv.id = 'kes-version-dialog';
  312. versionDiv.innerHTML = '<div class="kes-version-dialog-content">'+message+'<br><button>Close</button></div>';
  313. document.body.appendChild(versionDiv);
  314. document.querySelector('#kes-version-dialog button').addEventListener("click", () => {
  315. document.querySelector('#kes-version-dialog').remove();
  316. localStorage.setItem("setting-changelog-version", version);
  317. });
  318. }
  319. })();