GreasyFork Moderator Actions Log Viewer

to view GreasyFork Moderator Actions Log Table

// ==UserScript==
// @name          GreasyFork Moderator Actions Log Viewer
// @namespace     http://tampermonkey.net/
// @version       0.2.5
// @description   to view GreasyFork Moderator Actions Log Table
// @author        CY Fung
// @match         https://greasyfork.org/*/moderator_actions*
// @icon          https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
// @grant         none
// @run-at        document-idle
// @license       MIT
// ==/UserScript==

(function () {
  'use strict';

  function formatDateToCustomFormat(date) {
    var year = date.getFullYear();
    var month = padZero(date.getMonth() + 1);
    var day = padZero(date.getDate());
    var hours = padZero(date.getHours());
    var minutes = padZero(date.getMinutes());
    var timeZoneOffset = getTimeZoneOffsetString();

    return year + '.' + month + '.' + day + ' ' + hours + ':' + minutes + ' (GMT' + timeZoneOffset + ')';
  }

  function padZero(value) {
    return value.toString().padStart(2, '0');
  }

  function getTimeZoneOffsetString() {
    var offsetMinutes = new Date().getTimezoneOffset();
    var sign = offsetMinutes > 0 ? '-' : '+';
    var offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);

    return sign + offsetHours;
  }

  function removePrevTextNode(e, k) {
    let tn = e.previousSibling;
    if (tn && tn.nodeType === Node.TEXT_NODE) {
      if (tn.textContent.trim() === k) tn.remove();
    }
  }


  function setupTableContent() {

    for (const s of document.querySelectorAll('.log-table td:nth-child(1) relative-time:not(.jsm)')) {

      s.classList.add('jsm')

      let date = s.date;
      if (date) {

        let e = document.createElement('div');
        let q = formatDateToCustomFormat(date);
        q = q.split(' ');
        // e.textContent = formatDateToCustomFormat(date);
        e.className = 'date-entry';
        s.classList.add('jsm-hidden')
        s.after(e)

        e.appendChild(Object.assign(document.createElement('span'), {
          className: 'date-entry-date',

          textContent: q[0]
        }));

        e.appendChild(Object.assign(document.createElement('span'), {
          className: 'date-entry-time',

          textContent: q[1]
        }));

        e.appendChild(Object.assign(document.createElement('span'), {
          className: 'date-entry-gmt',
          textContent: q[2]
        }));
      }


    }


    for (const s of document.querySelectorAll('.log-table td:nth-child(3)')) {

        if(s.querySelector('a')) continue;

        let t = s.textContent;
        let m;
        t=t.replace(/O script (\d+) foi removido/g,(_,d)=>`Deleted script ${d}`);
        if(m=/([\s\S]*)\b(Deleted user)\s?(\d+)\b([\s\S]*)/i.exec(t)){
            m[1]=m[1].trim();
            m[4]=m[4].trim();
            s.innerHTML=`<a class="user-link deleted" href="/${document.documentElement.lang}/users/${m[3]}">${"Deleted User: "+m[3]}</a>`;
            if(m[1]) s.insertBefore(document.createTextNode(m[1]), s.firstChild);
            if(m[4]) s.appendChild(document.createTextNode(m[4]));
        }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)){
            m[1]=m[1].trim();

            m[4]=m[4].trim();
            s.innerHTML=`<a class="deleted" href="/${document.documentElement.lang}/scripts/${m[3]}">${"Deleted Script: "+m[3]}</a>`;
            if(m[1]) s.insertBefore(document.createTextNode(m[1]), s.firstChild);
            if(m[4]) s.appendChild(document.createTextNode(m[3]));
        }



    }



    for (const s of document.querySelectorAll('.log-table td:nth-child(3) a[href*="/scripts/"]:not(.jsm)')) {



      s.classList.add('jsm')
      let m = /\/scripts\/(\d+)/.exec(s.href);
      if (m) {
        let e = document.createElement('div');
        e.className = 'script-entry';
        s.replaceWith(e);
        e.appendChild(s);

        let span = document.createElement('span');
        span.className = 'entry-rid';
        span.textContent = m[1]
        e.prepend(span)
        removePrevTextNode(e, 'Script:');
      }


    }

    for (const s of document.querySelectorAll('.log-table td:nth-child(3) a[href*="/users/"]:not(.jsm)')) {



      s.classList.add('jsm')
      let m = /\/users\/(\d+)/.exec(s.href);
      if (m) {
        let e = document.createElement('div');
        e.className = 'user-entry';
        s.replaceWith(e);
        e.appendChild(s);

        let span = document.createElement('span');
        span.className = 'entry-rid';
        span.textContent = m[1]
        e.prepend(span)
        removePrevTextNode(e, 'User:');
      }


    }


    for (const s of document.querySelectorAll('.log-table td:nth-child(4)')) {

      convertToBadges(s);


    }


    for (const s of document.querySelectorAll('.log-table td:nth-child(5)')) {

      convertHyperlinks(s);


    }

  }
  function convertHyperlinks(elm) {
    var walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT, null, false);

    while (walker.nextNode()) {
      var textNode = walker.currentNode;
      var parentNode = textNode.parentNode;

      var text = textNode.nodeValue.trim();
      if (text.length > 0 && parentNode.tagName !== 'A') {
        var match = text.match(/(https?:\/\/[^\s]+)/);

        if (match) {
          var link = document.createElement('a');
          link.href = match[0];
          link.textContent = match[0].replace('https://greasyfork.org/scripts/', 'scripts/');

          var before = document.createTextNode(text.substring(0, match.index));
          var after = document.createTextNode(text.substring(match.index + match[0].length));

          parentNode.insertBefore(before, textNode);
          parentNode.insertBefore(link, textNode);
          parentNode.insertBefore(after, textNode);

          parentNode.removeChild(textNode);
        }
      }
    }
  }

  function makeTextableBadge(tag, message, color) {

    let div = Object.assign(document.createElement('div'), {
      className: 'textable-div',
    })
    let img = Object.assign(document.createElement('img'), {
      src: `https://img.shields.io/badge/${tag}-${message}-${color}`
    });
    div.appendChild(img)
    div.appendChild(Object.assign(document.createElement('span'), {
      className: 'textable-span',
      textContent: `${tag}: ${message}`
    }))
    return div;

  }

  function convertToBadges(elm) {

    const converts = {
      'Ban': () => makeTextableBadge('action', 'ban', 'FF5C5C'),
      'Delete and lock': () => makeTextableBadge('action', 'delete', 'FF9933'),
      'Undelete': () => makeTextableBadge('action', 'undelete', '66CC66'),

    }

    var walker = document.createTreeWalker(elm, NodeFilter.SHOW_TEXT, null, false);

    while (walker.nextNode()) {
      var textNode = walker.currentNode;
      var parentNode = textNode.parentNode;

      var text = textNode.nodeValue.trim();
      if (text.length > 0 && parentNode.tagName !== 'A' && parentNode.tagName !== 'IMG') {
        let t = text.trim();
        if (converts[t]) textNode.replaceWith(converts[t]());
      }
    }
  }



  function convertToAdvancedTable(tableSelector) {

    setupTableContent();
    // Get the table element
    var table = document.querySelector(tableSelector);

    // Add classes to the table and its components
    table.classList.add('advanced-table');
    table.tHead.classList.add('advanced-table-head');
    table.tBodies[0].classList.add('advanced-table-body');

    // Get the table headers
    var headers = Array.from(table.tHead.rows[0].cells);

    var sortOrder = []; // Track sort order for each column

    // Add classes and event listeners to enable sorting
    headers.forEach(function (header, index) {
      header.classList.add('sortable');
      header.addEventListener('click', function (event) {
        if (!event.target.classList.contains('search-input')) {
          sortTable(table, index, sortOrder);
          sortOrder[index] = !sortOrder[index]; // Toggle sort order
        }
      });

      // Create search input element
      var searchInput = document.createElement('input');
      searchInput.setAttribute('type', 'text');
      searchInput.setAttribute('placeholder', 'Search');
      searchInput.classList.add('search-input');
      searchInput.addEventListener('input', function () {
        filterTable(table, index);
      });
      header.appendChild(searchInput);

      // Create sort icon element
      var sortIcon = document.createElement('span');
      sortIcon.classList.add('sort-icon');
      header.appendChild(sortIcon);
    });
  }

  // Function to sort the table by column index
  function sortTable(table, columnIndex, sortOrder) {
    var rows = Array.from(table.tBodies[0].rows);

    rows.sort(function (a, b) {
      var cellA = a.cells[columnIndex].textContent.toLowerCase();
      var cellB = b.cells[columnIndex].textContent.toLowerCase();

      if (sortOrder[columnIndex]) {
        // Sort in descending order
        if (cellA < cellB) return 1;
        if (cellA > cellB) return -1;
        return 0;
      } else {
        // Sort in ascending order
        if (cellA < cellB) return -1;
        if (cellA > cellB) return 1;
        return 0;
      }
    });

    table.tBodies[0].innerHTML = '';
    rows.forEach(function (row) {
      table.tBodies[0].appendChild(row);
    });
  }

  // Function to filter the table by column index
  function filterTable(table, columnIndex) {
    var filterValue = table.tHead.rows[0].cells[columnIndex].querySelector('.search-input').value.toLowerCase();
    var rows = Array.from(table.tBodies[0].rows);

    rows.forEach(function (row) {
      var cellValue = row.cells[columnIndex].textContent.toLowerCase();
      row.style.display = cellValue.includes(filterValue) ? '' : 'none';
    });
  }


  const colsize = (idx) => `.log-table th:nth-child(${idx}), .log-table td:nth-child(${idx}){width:${colsizes[idx - 1]}; max-width:${0};}`

  let colsizes = [28, 34, 120, 32, 82];
  let colsizeSum = colsizes.reduce((a, b) => a + b, 0);
  colsizes = colsizes.map(t => (t / colsizeSum * 100).toFixed(2) + '%');

  document.head.appendChild(document.createElement('style')).textContent = `
  .log-table.advanced-table {
    border-collapse:separate;
    border-spacing: 0 1em;
  }
  .log-table.advanced-table td img{
    display:block;
  }

.advanced-table-head th {
  position: relative;
  padding: 2px 4px;
}

.sortable {
  cursor: pointer;
}

.sort-icon {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  width: 8px;
  height: 8px;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  transition: transform 0.2s ease;
}

.sortable.asc .sort-icon {
  border-bottom: 4px solid #000;
}

.sortable.desc .sort-icon {
  border-top: 4px solid #000;
}

.search-input {
  width: 100%;
  box-sizing: border-box;
  padding: 4px;
  border: 1px solid #ccc;
  border-radius: 4px;
}



		${colsize(1)}
		${colsize(2)}
		${colsize(3)}
		${colsize(4)}
		${colsize(5)}

    .entry-rid{
      font-size:80%;
      font-family: 'Open Sans',sans-serif,"Segoe UI Emoji";
    }
    .date-entry{
      font-family: 'Open Sans',sans-serif,"Segoe UI Emoji";

    }

        .user-entry, .script-entry{
        display: flex;
        column-gap: 4px;
            place-items: center;
        }

        .script-entry a[href]{
          overflow: hidden;
          white-space: nowrap;
          max-width: 24em;
          text-overflow: ellipsis;
        }


/* Shared styles for both ".user-entry > .entry-rid" and ".script-entry > .entry-rid" */
.user-entry > .entry-rid,
.script-entry > .entry-rid {

    display: inline-flex;
    place-content: center;
  padding: 4px 8px;
  color: #fff; /* Set an appropriate white text color */
  border-radius: 8px; /* Set the desired border radius */
  transition: background-color 0.3s; /* Add transition effect */
  min-width: 4em;
}

/* Styles for ".user-entry > .entry-rid" */
.user-entry > .entry-rid {
  background-color: #4A90E2; /* Set your desired background color */
}

.user-entry > .entry-rid:hover {
  background-color: #77B5FF; /* Set your desired hover background color */
}

/* Styles for ".script-entry > .entry-rid" */
.script-entry > .entry-rid {
  background-color: #B146C2; /* Set your desired background color */
}

.script-entry > .entry-rid:hover {
  background-color: #D27BFF; /* Set your desired hover background color */
}

relative-time.jsm-hidden {
display:none;
}

.date-entry-date{

  display: inline-block;
  padding: 4px 8px;
  color: #fff; /* Set an appropriate white text color */
  border-radius: 8px; /* Set the desired border radius */
  transition: background-color 0.3s; /* Add transition effect */
  font-size:70%;
  background-color: #336699;
}


.date-entry-time{

  display: inline-block;
  padding: 4px 8px;
  color: #fff; /* Set an appropriate white text color */
  border-radius: 8px; /* Set the desired border radius */
  transition: background-color 0.3s; /* Add transition effect */
  font-size:70%;
  background-color: #663366;
}

.date-entry-gmt{

  display: inline-block;
  padding: 4px 8px;
  color: #fff; /* Set an appropriate white text color */
  border-radius: 8px; /* Set the desired border radius */
  transition: background-color 0.3s; /* Add transition effect */
  font-size:40%;
  background-color: #336633;

}


/*
.date-entry-gmt{

  padding: 2px 4px;
  border-radius:4px;
  }*/

  .date-entry{
  display: flex;
  flex-wrap:wrap;
  row-gap:2px;
  column-gap:2px;
      place-items: end;
  }


  .textable-div {
    display: inline-block;
  }
  .textable-span {
    position: fixed;
    left: -100vw;
    top: -100vh;
    transform: scale(0.001);
    font-size: 1pt;
    user-select: none !important;
    pointer-events: none !important;
    transform-origin: 0 0;
    z-index: -1;
  }

  .log-table .deleted {
    background: #aaa;
    color: #222;
    text-decoration: line-through underline;

  }

`

  setInterval(() => {

    let table = document.querySelector('table.log-table:not(.advanced-table)')
    if (table) {
      requestAnimationFrame(() => {
        if (table.classList.contains('advanced-table')) return;
        table = null;
        convertToAdvancedTable('table.log-table')
      });
    }

  }, 100);



  // Your code here...
})();