Greasy Fork is available in English.

kbin enhancement script

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

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