GreasyFork Moderator Actions Log Viewer

to view GreasyFork Moderator Actions Log Table

  1. // ==UserScript==
  2. // @name GreasyFork Moderator Actions Log Viewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.5
  5. // @description to view GreasyFork Moderator Actions Log Table
  6. // @author CY Fung
  7. // @match https://greasyfork.org/*/moderator_actions*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
  9. // @grant none
  10. // @run-at document-idle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. function formatDateToCustomFormat(date) {
  18. var year = date.getFullYear();
  19. var month = padZero(date.getMonth() + 1);
  20. var day = padZero(date.getDate());
  21. var hours = padZero(date.getHours());
  22. var minutes = padZero(date.getMinutes());
  23. var timeZoneOffset = getTimeZoneOffsetString();
  24.  
  25. return year + '.' + month + '.' + day + ' ' + hours + ':' + minutes + ' (GMT' + timeZoneOffset + ')';
  26. }
  27.  
  28. function padZero(value) {
  29. return value.toString().padStart(2, '0');
  30. }
  31.  
  32. function getTimeZoneOffsetString() {
  33. var offsetMinutes = new Date().getTimezoneOffset();
  34. var sign = offsetMinutes > 0 ? '-' : '+';
  35. var offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
  36.  
  37. return sign + offsetHours;
  38. }
  39.  
  40. function removePrevTextNode(e, k) {
  41. let tn = e.previousSibling;
  42. if (tn && tn.nodeType === Node.TEXT_NODE) {
  43. if (tn.textContent.trim() === k) tn.remove();
  44. }
  45. }
  46.  
  47.  
  48. function setupTableContent() {
  49.  
  50. for (const s of document.querySelectorAll('.log-table td:nth-child(1) relative-time:not(.jsm)')) {
  51.  
  52. s.classList.add('jsm')
  53.  
  54. let date = s.date;
  55. if (date) {
  56.  
  57. let e = document.createElement('div');
  58. let q = formatDateToCustomFormat(date);
  59. q = q.split(' ');
  60. // e.textContent = formatDateToCustomFormat(date);
  61. e.className = 'date-entry';
  62. s.classList.add('jsm-hidden')
  63. s.after(e)
  64.  
  65. e.appendChild(Object.assign(document.createElement('span'), {
  66. className: 'date-entry-date',
  67.  
  68. textContent: q[0]
  69. }));
  70.  
  71. e.appendChild(Object.assign(document.createElement('span'), {
  72. className: 'date-entry-time',
  73.  
  74. textContent: q[1]
  75. }));
  76.  
  77. e.appendChild(Object.assign(document.createElement('span'), {
  78. className: 'date-entry-gmt',
  79. textContent: q[2]
  80. }));
  81. }
  82.  
  83.  
  84. }
  85.  
  86.  
  87. for (const s of document.querySelectorAll('.log-table td:nth-child(3)')) {
  88.  
  89. if(s.querySelector('a')) continue;
  90.  
  91. let t = s.textContent;
  92. let m;
  93. t=t.replace(/O script (\d+) foi removido/g,(_,d)=>`Deleted script ${d}`);
  94. if(m=/([\s\S]*)\b(Deleted user)\s?(\d+)\b([\s\S]*)/i.exec(t)){
  95. m[1]=m[1].trim();
  96. m[4]=m[4].trim();
  97. s.innerHTML=`<a class="user-link deleted" href="/${document.documentElement.lang}/users/${m[3]}">${"Deleted User: "+m[3]}</a>`;
  98. if(m[1]) s.insertBefore(document.createTextNode(m[1]), s.firstChild);
  99. if(m[4]) s.appendChild(document.createTextNode(m[4]));
  100. }else if(m=/([\s\S]*)(Slettet skriptet|Skrip terhapus|ลบสคริปต์|Đã xoá script|סקריפט מחוק|Gelöschtes Skript|삭제된 스크립트|Script supprimé|Silinmiş script|Удалённый скрипт|刪除腳本|Verwijderd script|Изтрит скрипт|已删除的脚本|削除されたスクリプト|Διαγραμμένος κώδικας|Șterge script-ul|Script borrado|Deleted script|Script eliminato|Odstránený skript|Usunięto skrypt|أحذف السكربت)\s?(\d+)\b([\s\S]*)/i.exec(t)){
  101. m[1]=m[1].trim();
  102.  
  103. m[4]=m[4].trim();
  104. s.innerHTML=`<a class="deleted" href="/${document.documentElement.lang}/scripts/${m[3]}">${"Deleted Script: "+m[3]}</a>`;
  105. if(m[1]) s.insertBefore(document.createTextNode(m[1]), s.firstChild);
  106. if(m[4]) s.appendChild(document.createTextNode(m[3]));
  107. }
  108.  
  109.  
  110.  
  111. }
  112.  
  113.  
  114.  
  115. for (const s of document.querySelectorAll('.log-table td:nth-child(3) a[href*="/scripts/"]:not(.jsm)')) {
  116.  
  117.  
  118.  
  119. s.classList.add('jsm')
  120. let m = /\/scripts\/(\d+)/.exec(s.href);
  121. if (m) {
  122. let e = document.createElement('div');
  123. e.className = 'script-entry';
  124. s.replaceWith(e);
  125. e.appendChild(s);
  126.  
  127. let span = document.createElement('span');
  128. span.className = 'entry-rid';
  129. span.textContent = m[1]
  130. e.prepend(span)
  131. removePrevTextNode(e, 'Script:');
  132. }
  133.  
  134.  
  135. }
  136.  
  137. for (const s of document.querySelectorAll('.log-table td:nth-child(3) a[href*="/users/"]:not(.jsm)')) {
  138.  
  139.  
  140.  
  141. s.classList.add('jsm')
  142. let m = /\/users\/(\d+)/.exec(s.href);
  143. if (m) {
  144. let e = document.createElement('div');
  145. e.className = 'user-entry';
  146. s.replaceWith(e);
  147. e.appendChild(s);
  148.  
  149. let span = document.createElement('span');
  150. span.className = 'entry-rid';
  151. span.textContent = m[1]
  152. e.prepend(span)
  153. removePrevTextNode(e, 'User:');
  154. }
  155.  
  156.  
  157. }
  158.  
  159.  
  160. for (const s of document.querySelectorAll('.log-table td:nth-child(4)')) {
  161.  
  162. convertToBadges(s);
  163.  
  164.  
  165. }
  166.  
  167.  
  168. for (const s of document.querySelectorAll('.log-table td:nth-child(5)')) {
  169.  
  170. convertHyperlinks(s);
  171.  
  172.  
  173. }
  174.  
  175. }
  176. function convertHyperlinks(elm) {
  177. var walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT, null, false);
  178.  
  179. while (walker.nextNode()) {
  180. var textNode = walker.currentNode;
  181. var parentNode = textNode.parentNode;
  182.  
  183. var text = textNode.nodeValue.trim();
  184. if (text.length > 0 && parentNode.tagName !== 'A') {
  185. var match = text.match(/(https?:\/\/[^\s]+)/);
  186.  
  187. if (match) {
  188. var link = document.createElement('a');
  189. link.href = match[0];
  190. link.textContent = match[0].replace('https://greasyfork.org/scripts/', 'scripts/');
  191.  
  192. var before = document.createTextNode(text.substring(0, match.index));
  193. var after = document.createTextNode(text.substring(match.index + match[0].length));
  194.  
  195. parentNode.insertBefore(before, textNode);
  196. parentNode.insertBefore(link, textNode);
  197. parentNode.insertBefore(after, textNode);
  198.  
  199. parentNode.removeChild(textNode);
  200. }
  201. }
  202. }
  203. }
  204.  
  205. function makeTextableBadge(tag, message, color) {
  206.  
  207. let div = Object.assign(document.createElement('div'), {
  208. className: 'textable-div',
  209. })
  210. let img = Object.assign(document.createElement('img'), {
  211. src: `https://img.shields.io/badge/${tag}-${message}-${color}`
  212. });
  213. div.appendChild(img)
  214. div.appendChild(Object.assign(document.createElement('span'), {
  215. className: 'textable-span',
  216. textContent: `${tag}: ${message}`
  217. }))
  218. return div;
  219.  
  220. }
  221.  
  222. function convertToBadges(elm) {
  223.  
  224. const converts = {
  225. 'Ban': () => makeTextableBadge('action', 'ban', 'FF5C5C'),
  226. 'Delete and lock': () => makeTextableBadge('action', 'delete', 'FF9933'),
  227. 'Undelete': () => makeTextableBadge('action', 'undelete', '66CC66'),
  228.  
  229. }
  230.  
  231. var walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT, null, false);
  232.  
  233. while (walker.nextNode()) {
  234. var textNode = walker.currentNode;
  235. var parentNode = textNode.parentNode;
  236.  
  237. var text = textNode.nodeValue.trim();
  238. if (text.length > 0 && parentNode.tagName !== 'A' && parentNode.tagName !== 'IMG') {
  239. let t = text.trim();
  240. if (converts[t]) textNode.replaceWith(converts[t]());
  241. }
  242. }
  243. }
  244.  
  245.  
  246.  
  247. function convertToAdvancedTable(tableSelector) {
  248.  
  249. setupTableContent();
  250. // Get the table element
  251. var table = document.querySelector(tableSelector);
  252.  
  253. // Add classes to the table and its components
  254. table.classList.add('advanced-table');
  255. table.tHead.classList.add('advanced-table-head');
  256. table.tBodies[0].classList.add('advanced-table-body');
  257.  
  258. // Get the table headers
  259. var headers = Array.from(table.tHead.rows[0].cells);
  260.  
  261. var sortOrder = []; // Track sort order for each column
  262.  
  263. // Add classes and event listeners to enable sorting
  264. headers.forEach(function (header, index) {
  265. header.classList.add('sortable');
  266. header.addEventListener('click', function (event) {
  267. if (!event.target.classList.contains('search-input')) {
  268. sortTable(table, index, sortOrder);
  269. sortOrder[index] = !sortOrder[index]; // Toggle sort order
  270. }
  271. });
  272.  
  273. // Create search input element
  274. var searchInput = document.createElement('input');
  275. searchInput.setAttribute('type', 'text');
  276. searchInput.setAttribute('placeholder', 'Search');
  277. searchInput.classList.add('search-input');
  278. searchInput.addEventListener('input', function () {
  279. filterTable(table, index);
  280. });
  281. header.appendChild(searchInput);
  282.  
  283. // Create sort icon element
  284. var sortIcon = document.createElement('span');
  285. sortIcon.classList.add('sort-icon');
  286. header.appendChild(sortIcon);
  287. });
  288. }
  289.  
  290. // Function to sort the table by column index
  291. function sortTable(table, columnIndex, sortOrder) {
  292. var rows = Array.from(table.tBodies[0].rows);
  293.  
  294. rows.sort(function (a, b) {
  295. var cellA = a.cells[columnIndex].textContent.toLowerCase();
  296. var cellB = b.cells[columnIndex].textContent.toLowerCase();
  297.  
  298. if (sortOrder[columnIndex]) {
  299. // Sort in descending order
  300. if (cellA < cellB) return 1;
  301. if (cellA > cellB) return -1;
  302. return 0;
  303. } else {
  304. // Sort in ascending order
  305. if (cellA < cellB) return -1;
  306. if (cellA > cellB) return 1;
  307. return 0;
  308. }
  309. });
  310.  
  311. table.tBodies[0].innerHTML = '';
  312. rows.forEach(function (row) {
  313. table.tBodies[0].appendChild(row);
  314. });
  315. }
  316.  
  317. // Function to filter the table by column index
  318. function filterTable(table, columnIndex) {
  319. var filterValue = table.tHead.rows[0].cells[columnIndex].querySelector('.search-input').value.toLowerCase();
  320. var rows = Array.from(table.tBodies[0].rows);
  321.  
  322. rows.forEach(function (row) {
  323. var cellValue = row.cells[columnIndex].textContent.toLowerCase();
  324. row.style.display = cellValue.includes(filterValue) ? '' : 'none';
  325. });
  326. }
  327.  
  328.  
  329. const colsize = (idx) => `.log-table th:nth-child(${idx}), .log-table td:nth-child(${idx}){width:${colsizes[idx - 1]}; max-width:${0};}`
  330.  
  331. let colsizes = [28, 34, 120, 32, 82];
  332. let colsizeSum = colsizes.reduce((a, b) => a + b, 0);
  333. colsizes = colsizes.map(t => (t / colsizeSum * 100).toFixed(2) + '%');
  334.  
  335. document.head.appendChild(document.createElement('style')).textContent = `
  336. .log-table.advanced-table {
  337. border-collapse:separate;
  338. border-spacing: 0 1em;
  339. }
  340. .log-table.advanced-table td img{
  341. display:block;
  342. }
  343.  
  344. .advanced-table-head th {
  345. position: relative;
  346. padding: 2px 4px;
  347. }
  348.  
  349. .sortable {
  350. cursor: pointer;
  351. }
  352.  
  353. .sort-icon {
  354. position: absolute;
  355. top: 50%;
  356. right: 8px;
  357. transform: translateY(-50%);
  358. width: 8px;
  359. height: 8px;
  360. border-left: 4px solid transparent;
  361. border-right: 4px solid transparent;
  362. transition: transform 0.2s ease;
  363. }
  364.  
  365. .sortable.asc .sort-icon {
  366. border-bottom: 4px solid #000;
  367. }
  368.  
  369. .sortable.desc .sort-icon {
  370. border-top: 4px solid #000;
  371. }
  372.  
  373. .search-input {
  374. width: 100%;
  375. box-sizing: border-box;
  376. padding: 4px;
  377. border: 1px solid #ccc;
  378. border-radius: 4px;
  379. }
  380.  
  381.  
  382.  
  383. ${colsize(1)}
  384. ${colsize(2)}
  385. ${colsize(3)}
  386. ${colsize(4)}
  387. ${colsize(5)}
  388.  
  389. .entry-rid{
  390. font-size:80%;
  391. font-family: 'Open Sans',sans-serif,"Segoe UI Emoji";
  392. }
  393. .date-entry{
  394. font-family: 'Open Sans',sans-serif,"Segoe UI Emoji";
  395.  
  396. }
  397.  
  398. .user-entry, .script-entry{
  399. display: flex;
  400. column-gap: 4px;
  401. place-items: center;
  402. }
  403.  
  404. .script-entry a[href]{
  405. overflow: hidden;
  406. white-space: nowrap;
  407. max-width: 24em;
  408. text-overflow: ellipsis;
  409. }
  410.  
  411.  
  412. /* Shared styles for both ".user-entry > .entry-rid" and ".script-entry > .entry-rid" */
  413. .user-entry > .entry-rid,
  414. .script-entry > .entry-rid {
  415.  
  416. display: inline-flex;
  417. place-content: center;
  418. padding: 4px 8px;
  419. color: #fff; /* Set an appropriate white text color */
  420. border-radius: 8px; /* Set the desired border radius */
  421. transition: background-color 0.3s; /* Add transition effect */
  422. min-width: 4em;
  423. }
  424.  
  425. /* Styles for ".user-entry > .entry-rid" */
  426. .user-entry > .entry-rid {
  427. background-color: #4A90E2; /* Set your desired background color */
  428. }
  429.  
  430. .user-entry > .entry-rid:hover {
  431. background-color: #77B5FF; /* Set your desired hover background color */
  432. }
  433.  
  434. /* Styles for ".script-entry > .entry-rid" */
  435. .script-entry > .entry-rid {
  436. background-color: #B146C2; /* Set your desired background color */
  437. }
  438.  
  439. .script-entry > .entry-rid:hover {
  440. background-color: #D27BFF; /* Set your desired hover background color */
  441. }
  442.  
  443. relative-time.jsm-hidden {
  444. display:none;
  445. }
  446.  
  447. .date-entry-date{
  448.  
  449. display: inline-block;
  450. padding: 4px 8px;
  451. color: #fff; /* Set an appropriate white text color */
  452. border-radius: 8px; /* Set the desired border radius */
  453. transition: background-color 0.3s; /* Add transition effect */
  454. font-size:70%;
  455. background-color: #336699;
  456. }
  457.  
  458.  
  459. .date-entry-time{
  460.  
  461. display: inline-block;
  462. padding: 4px 8px;
  463. color: #fff; /* Set an appropriate white text color */
  464. border-radius: 8px; /* Set the desired border radius */
  465. transition: background-color 0.3s; /* Add transition effect */
  466. font-size:70%;
  467. background-color: #663366;
  468. }
  469.  
  470. .date-entry-gmt{
  471.  
  472. display: inline-block;
  473. padding: 4px 8px;
  474. color: #fff; /* Set an appropriate white text color */
  475. border-radius: 8px; /* Set the desired border radius */
  476. transition: background-color 0.3s; /* Add transition effect */
  477. font-size:40%;
  478. background-color: #336633;
  479.  
  480. }
  481.  
  482.  
  483. /*
  484. .date-entry-gmt{
  485.  
  486. padding: 2px 4px;
  487. border-radius:4px;
  488. }*/
  489.  
  490. .date-entry{
  491. display: flex;
  492. flex-wrap:wrap;
  493. row-gap:2px;
  494. column-gap:2px;
  495. place-items: end;
  496. }
  497.  
  498.  
  499. .textable-div {
  500. display: inline-block;
  501. }
  502. .textable-span {
  503. position: fixed;
  504. left: -100vw;
  505. top: -100vh;
  506. transform: scale(0.001);
  507. font-size: 1pt;
  508. user-select: none !important;
  509. pointer-events: none !important;
  510. transform-origin: 0 0;
  511. z-index: -1;
  512. }
  513.  
  514. .log-table .deleted {
  515. background: #aaa;
  516. color: #222;
  517. text-decoration: line-through underline;
  518.  
  519. }
  520.  
  521. `
  522.  
  523. setInterval(() => {
  524.  
  525. let table = document.querySelector('table.log-table:not(.advanced-table)')
  526. if (table) {
  527. requestAnimationFrame(() => {
  528. if (table.classList.contains('advanced-table')) return;
  529. table = null;
  530. convertToAdvancedTable('table.log-table')
  531. });
  532. }
  533.  
  534. }, 100);
  535.  
  536.  
  537.  
  538. // Your code here...
  539. })();