GreasyFork Moderator Actions Log Viewer

to view GreasyFork Moderator Actions Log Table

目前为 2023-05-24 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name GreasyFork Moderator Actions Log Viewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.2
  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) a[href*="/scripts/"]:not(.jsm)')) {
  88.  
  89.  
  90.  
  91. s.classList.add('jsm')
  92. let m = /\/scripts\/(\d+)/.exec(s.href);
  93. if (m) {
  94. let e = document.createElement('div');
  95. e.className = 'script-entry';
  96. s.replaceWith(e);
  97. e.appendChild(s);
  98.  
  99. let span = document.createElement('span');
  100. span.className = 'entry-rid';
  101. span.textContent = m[1]
  102. e.prepend(span)
  103. removePrevTextNode(e, 'Script:');
  104. }
  105.  
  106.  
  107. }
  108.  
  109.  
  110. for (const s of document.querySelectorAll('.log-table td:nth-child(3) a[href*="/users/"]:not(.jsm)')) {
  111.  
  112.  
  113.  
  114. s.classList.add('jsm')
  115. let m = /\/users\/(\d+)/.exec(s.href);
  116. if (m) {
  117. let e = document.createElement('div');
  118. e.className = 'user-entry';
  119. s.replaceWith(e);
  120. e.appendChild(s);
  121.  
  122. let span = document.createElement('span');
  123. span.className = 'entry-rid';
  124. span.textContent = m[1]
  125. e.prepend(span)
  126. removePrevTextNode(e, 'User:');
  127. }
  128.  
  129.  
  130. }
  131.  
  132.  
  133. for (const s of document.querySelectorAll('.log-table td:nth-child(4)')) {
  134.  
  135. convertToBadges(s);
  136.  
  137.  
  138. }
  139.  
  140.  
  141. for (const s of document.querySelectorAll('.log-table td:nth-child(5)')) {
  142.  
  143. convertHyperlinks(s);
  144.  
  145.  
  146. }
  147.  
  148. }
  149. function convertHyperlinks(elm) {
  150. var walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT, null, false);
  151.  
  152. while (walker.nextNode()) {
  153. var textNode = walker.currentNode;
  154. var parentNode = textNode.parentNode;
  155.  
  156. var text = textNode.nodeValue.trim();
  157. if (text.length > 0 && parentNode.tagName !== 'A') {
  158. var match = text.match(/(https?:\/\/[^\s]+)/);
  159.  
  160. if (match) {
  161. var link = document.createElement('a');
  162. link.href = match[0];
  163. link.textContent = match[0].replace('https://greasyfork.org/scripts/', 'scripts/');
  164.  
  165. var before = document.createTextNode(text.substring(0, match.index));
  166. var after = document.createTextNode(text.substring(match.index + match[0].length));
  167.  
  168. parentNode.insertBefore(before, textNode);
  169. parentNode.insertBefore(link, textNode);
  170. parentNode.insertBefore(after, textNode);
  171.  
  172. parentNode.removeChild(textNode);
  173. }
  174. }
  175. }
  176. }
  177.  
  178. function makeTextableBadge(tag, message, color) {
  179.  
  180. let div = Object.assign(document.createElement('div'), {
  181. className: 'textable-div',
  182. })
  183. let img = Object.assign(document.createElement('img'), {
  184. src: `https://img.shields.io/badge/${tag}-${message}-${color}`
  185. });
  186. div.appendChild(img)
  187. div.appendChild(Object.assign(document.createElement('span'), {
  188. className: 'textable-span',
  189. textContent: `${tag}: ${message}`
  190. }))
  191. return div;
  192.  
  193. }
  194.  
  195. function convertToBadges(elm) {
  196.  
  197. const converts = {
  198. 'Ban': () => makeTextableBadge('action', 'ban', 'FF5C5C'),
  199. 'Delete and lock': () => makeTextableBadge('action', 'delete', 'FF9933'),
  200. 'Undelete': () => makeTextableBadge('action', 'undelete', '66CC66'),
  201.  
  202. }
  203.  
  204. var walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT, null, false);
  205.  
  206. while (walker.nextNode()) {
  207. var textNode = walker.currentNode;
  208. var parentNode = textNode.parentNode;
  209.  
  210. var text = textNode.nodeValue.trim();
  211. if (text.length > 0 && parentNode.tagName !== 'A' && parentNode.tagName !== 'IMG') {
  212. let t = text.trim();
  213. if (converts[t]) textNode.replaceWith(converts[t]());
  214. }
  215. }
  216. }
  217.  
  218.  
  219.  
  220. function convertToAdvancedTable(tableSelector) {
  221.  
  222. setupTableContent();
  223. // Get the table element
  224. var table = document.querySelector(tableSelector);
  225.  
  226. // Add classes to the table and its components
  227. table.classList.add('advanced-table');
  228. table.tHead.classList.add('advanced-table-head');
  229. table.tBodies[0].classList.add('advanced-table-body');
  230.  
  231. // Get the table headers
  232. var headers = Array.from(table.tHead.rows[0].cells);
  233.  
  234. var sortOrder = []; // Track sort order for each column
  235.  
  236. // Add classes and event listeners to enable sorting
  237. headers.forEach(function (header, index) {
  238. header.classList.add('sortable');
  239. header.addEventListener('click', function (event) {
  240. if (!event.target.classList.contains('search-input')) {
  241. sortTable(table, index, sortOrder);
  242. sortOrder[index] = !sortOrder[index]; // Toggle sort order
  243. }
  244. });
  245.  
  246. // Create search input element
  247. var searchInput = document.createElement('input');
  248. searchInput.setAttribute('type', 'text');
  249. searchInput.setAttribute('placeholder', 'Search');
  250. searchInput.classList.add('search-input');
  251. searchInput.addEventListener('input', function () {
  252. filterTable(table, index);
  253. });
  254. header.appendChild(searchInput);
  255.  
  256. // Create sort icon element
  257. var sortIcon = document.createElement('span');
  258. sortIcon.classList.add('sort-icon');
  259. header.appendChild(sortIcon);
  260. });
  261. }
  262.  
  263. // Function to sort the table by column index
  264. function sortTable(table, columnIndex, sortOrder) {
  265. var rows = Array.from(table.tBodies[0].rows);
  266.  
  267. rows.sort(function (a, b) {
  268. var cellA = a.cells[columnIndex].textContent.toLowerCase();
  269. var cellB = b.cells[columnIndex].textContent.toLowerCase();
  270.  
  271. if (sortOrder[columnIndex]) {
  272. // Sort in descending order
  273. if (cellA < cellB) return 1;
  274. if (cellA > cellB) return -1;
  275. return 0;
  276. } else {
  277. // Sort in ascending order
  278. if (cellA < cellB) return -1;
  279. if (cellA > cellB) return 1;
  280. return 0;
  281. }
  282. });
  283.  
  284. table.tBodies[0].innerHTML = '';
  285. rows.forEach(function (row) {
  286. table.tBodies[0].appendChild(row);
  287. });
  288. }
  289.  
  290. // Function to filter the table by column index
  291. function filterTable(table, columnIndex) {
  292. var filterValue = table.tHead.rows[0].cells[columnIndex].querySelector('.search-input').value.toLowerCase();
  293. var rows = Array.from(table.tBodies[0].rows);
  294.  
  295. rows.forEach(function (row) {
  296. var cellValue = row.cells[columnIndex].textContent.toLowerCase();
  297. row.style.display = cellValue.includes(filterValue) ? '' : 'none';
  298. });
  299. }
  300.  
  301.  
  302. const colsize = (idx) => `.log-table th:nth-child(${idx}), .log-table td:nth-child(${idx}){width:${colsizes[idx - 1]}; max-width:${colsizes[idx - 1]};}`
  303.  
  304. let colsizes = [28, 34, 120, 32, 82];
  305. let colsizeSum = colsizes.reduce((a, b) => a + b, 0);
  306. colsizes = colsizes.map(t => (t / colsizeSum * 100).toFixed(2) + '%');
  307.  
  308. document.head.appendChild(document.createElement('style')).textContent = `
  309. .log-table.advanced-table {
  310. border-collapse:separate;
  311. border-spacing: 0 1em;
  312. }
  313. .log-table.advanced-table td img{
  314. display:block;
  315. }
  316.  
  317. .advanced-table-head th {
  318. position: relative;
  319. padding: 8px;
  320. }
  321.  
  322. .sortable {
  323. cursor: pointer;
  324. }
  325.  
  326. .sort-icon {
  327. position: absolute;
  328. top: 50%;
  329. right: 8px;
  330. transform: translateY(-50%);
  331. width: 8px;
  332. height: 8px;
  333. border-left: 4px solid transparent;
  334. border-right: 4px solid transparent;
  335. transition: transform 0.2s ease;
  336. }
  337.  
  338. .sortable.asc .sort-icon {
  339. border-bottom: 4px solid #000;
  340. }
  341.  
  342. .sortable.desc .sort-icon {
  343. border-top: 4px solid #000;
  344. }
  345.  
  346. .search-input {
  347. width: 100%;
  348. box-sizing: border-box;
  349. padding: 4px;
  350. border: 1px solid #ccc;
  351. border-radius: 4px;
  352. }
  353.  
  354.  
  355.  
  356. ${colsize(1)}
  357. ${colsize(2)}
  358. ${colsize(3)}
  359. ${colsize(4)}
  360. ${colsize(5)}
  361.  
  362. .entry-rid{
  363. font-size:80%;
  364. font-family: 'Open Sans',sans-serif,"Segoe UI Emoji";
  365. }
  366. .date-entry{
  367. font-family: 'Open Sans',sans-serif,"Segoe UI Emoji";
  368.  
  369. }
  370.  
  371. .user-entry, .script-entry{
  372. display: flex;
  373. column-gap: 4px;
  374. place-items: center;
  375. }
  376.  
  377. .script-entry a[href]{
  378. overflow: hidden;
  379. white-space: nowrap;
  380. width: 24em;
  381. text-overflow: ellipsis;
  382. }
  383.  
  384.  
  385. /* Shared styles for both ".user-entry > .entry-rid" and ".script-entry > .entry-rid" */
  386. .user-entry > .entry-rid,
  387. .script-entry > .entry-rid {
  388.  
  389. display: inline-flex;
  390. place-content: center;
  391. padding: 4px 8px;
  392. color: #fff; /* Set an appropriate white text color */
  393. border-radius: 8px; /* Set the desired border radius */
  394. transition: background-color 0.3s; /* Add transition effect */
  395. min-width: 4em;
  396. }
  397.  
  398. /* Styles for ".user-entry > .entry-rid" */
  399. .user-entry > .entry-rid {
  400. background-color: #4A90E2; /* Set your desired background color */
  401. }
  402.  
  403. .user-entry > .entry-rid:hover {
  404. background-color: #77B5FF; /* Set your desired hover background color */
  405. }
  406.  
  407. /* Styles for ".script-entry > .entry-rid" */
  408. .script-entry > .entry-rid {
  409. background-color: #B146C2; /* Set your desired background color */
  410. }
  411.  
  412. .script-entry > .entry-rid:hover {
  413. background-color: #D27BFF; /* Set your desired hover background color */
  414. }
  415.  
  416. relative-time.jsm-hidden {
  417. display:none;
  418. }
  419.  
  420. .date-entry-date{
  421.  
  422. display: inline-block;
  423. padding: 4px 8px;
  424. color: #fff; /* Set an appropriate white text color */
  425. border-radius: 8px; /* Set the desired border radius */
  426. transition: background-color 0.3s; /* Add transition effect */
  427. font-size:70%;
  428. background-color: #336699;
  429. }
  430.  
  431.  
  432. .date-entry-time{
  433.  
  434. display: inline-block;
  435. padding: 4px 8px;
  436. color: #fff; /* Set an appropriate white text color */
  437. border-radius: 8px; /* Set the desired border radius */
  438. transition: background-color 0.3s; /* Add transition effect */
  439. font-size:70%;
  440. background-color: #663366;
  441. }
  442.  
  443. .date-entry-gmt{
  444.  
  445. display: inline-block;
  446. padding: 4px 8px;
  447. color: #fff; /* Set an appropriate white text color */
  448. border-radius: 8px; /* Set the desired border radius */
  449. transition: background-color 0.3s; /* Add transition effect */
  450. font-size:40%;
  451. background-color: #336633;
  452.  
  453. }
  454.  
  455.  
  456. /*
  457. .date-entry-gmt{
  458.  
  459. padding: 2px 4px;
  460. border-radius:4px;
  461. }*/
  462.  
  463. .date-entry{
  464. display: flex;
  465. flex-wrap:wrap;
  466. row-gap:2px;
  467. column-gap:2px;
  468. place-items: end;
  469. }
  470.  
  471.  
  472. .textable-div {
  473. display: inline-block;
  474. }
  475. .textable-span {
  476. position: fixed;
  477. left: -100vw;
  478. top: -100vh;
  479. transform: scale(0.001);
  480. font-size: 1pt;
  481. user-select: none !important;
  482. pointer-events: none !important;
  483. transform-origin: 0 0;
  484. z-index: -1;
  485. }
  486.  
  487. `
  488.  
  489. setInterval(() => {
  490.  
  491. let table = document.querySelector('table.log-table:not(.advanced-table)')
  492. if (table) {
  493. requestAnimationFrame(() => {
  494. if (table.classList.contains('advanced-table')) return;
  495. table = null;
  496. convertToAdvancedTable('table.log-table')
  497. });
  498. }
  499.  
  500. }, 100);
  501.  
  502.  
  503.  
  504. // Your code here...
  505. })();