kommu / GeoGuessr Liked Maps List Enhanced

// ==UserScript==
// @name GeoGuessr Liked Maps List Enhanced
// @namespace kommu
// @description Display all your liked maps in a simple table.
// @version 1.0.0
// @include https://www.geoguessr.com/*
// @run-at document-start
// @license MIT
// ==/UserScript==

/*jshint esversion: 6 */

var oldHref = document.location.href;

if (localStorage.gmle_dark_mode === undefined) {
  localStorage.gmle_dark_mode = "true";
}

if (localStorage.gmle_search_mode === undefined) {
  localStorage.gmle_search_mode = "classic";
}

window.addEventListener("load", (event) => {
  mapsBehavior();

  var bodyList = document.querySelector("body"),
    observer = new MutationObserver(function (mutations) {
      mutations.forEach(function (mutation) {
        if (oldHref != document.location.href) {
          oldHref = document.location.href;
          mapsBehavior();
        }
      });
    });

  var config = {
    childList: true,
    subtree: true
  };

  observer.observe(bodyList, config);
});

function mapsBehavior() {
  if (!location.pathname.startsWith("/me/likes") || location.pathname.endsWith("#")) {
    return false;
  }

  var style_general = document.createElement("style");
  document.head.appendChild(style_general);
  style_general.sheet.insertRule("table.custom-table {\n" +
    "    border: 1px solid transparent;\n" +
    "    width: 100%;\n" +
    "    border-collapse: collapse;\n" +
    "    margin-bottom: 20px;\n" +
    "    font-size: 12px;\n" +
    "    border-radius: 10px;\n" +
    "}")
  style_general.sheet.insertRule("table.custom-table td, table.custom-table th {\n" +
    "    border: 1px solid #00000026;\n" +
    "    background: #ffffff1a;\n" +
    "    padding: 10px;\n" +
    "}")
  style_general.sheet.insertRule("table.custom-table th {\n" +
    "    text-transform: uppercase;\n" +
    "    font-size: 10px;\n" +
    "}")
  style_general.sheet.insertRule("table.custom-table td:nth-child(3) {\n" +
    "    font-size: 10px;\n" +
    "    width: 300px!important;\n" +
    "    max-width: 500px;\n" +
    "    word-break: break-word;\n" +
    "}")
  style_general.sheet.insertRule(".table-filter input {\n" +
    "    width: 100%;\n" +
    "}")
  style_general.sheet.insertRule(".map-name.visible {\n" +
    "    transition-duration: 0.2s;\n" +
    "    display: table-row;\n" +
    "    background: transparent;\n" +
    "}")
  style_general.sheet.insertRule(".map-name.visible:hover {\n" +
    "    background: #1e913c47;\n" +
    "    box-shadow: 0 0 20px 7px #095e202b;\n" +
    "}")
  style_general.sheet.insertRule(".map-name {\n" +
    "    display: none;\n" +
    "}")
  style_general.sheet.insertRule("table.custom-table td:nth-child(1),\n" +
    "table.custom-table td:nth-child(4),\n" +
    "table.custom-table td:nth-child(5),\n" +
    "table.custom-table td:nth-child(6),\n" +
    "table.custom-table td:nth-child(7) {\n" +
    "    text-align: center;\n" +
    "}")

  style_general.sheet.insertRule("div[class^='maps_backButton'] {\n" +
    "    display: none;\n" +
    "}")

  style_general.sheet.insertRule("a.delete--button.button.button--small.button--secondary {\n" +
    "   background: #f44336;\n" +
    "   color: white;\n" +
    "}")

  style_general.sheet.insertRule("a.publish--button.button.button--small.button--primary {\n" +
    "   background: transparent;\n" +
    "   color: #568209;\n" +
    "   border: 1px solid #568209;\n" +
    "}")

  style_general.sheet.insertRule("a.unpublish--button.button.button--small.button--secondary {\n" +
    "   background: transparent;\n" +
    "   color: #e95656;\n" +
    "   border: 1px solid #e95656;\n" +
    "}")

  style_general.sheet.insertRule("td.table-row-actions {\n" +
    "   display: flex;\n" +
    "   justify-content: center;\n" +
    "   gap: 10px;\n" +
    "   border: none;\n" +
    "}")

  var style_light_mode = document.createElement("style");
  style_light_mode.id = "gmle_style_light_mode"
  document.head.appendChild(style_light_mode);
  style_light_mode.sheet.insertRule("table.custom-table {\n" +
    "    background: #ffffff;\n" +
    "    color: black!important;\n" +
    "}")
  style_light_mode.disabled = localStorage.gmle_dark_mode === "true"

  var divParent = document.createElement('div');
  divParent.id = "gmle";
  var elements = document.querySelector("[class^=container_content]");
  elements.parentNode.insertBefore(divParent, elements);

  var header = "<div class=\"table-filter\" style=\"background: #0000001f;margin: 40px;padding: 20px 40px;border-radius: 10px;\"><p><a target=\"_blank\" class=\"button button--small button--primary\" href=\"/map-maker/\">Create a new map</a> <a class=\"button button--small button--secondary\" id=\"gmle_button_switch_light_dark_mode\">Switch Light/Dark mode</a> <a class=\"button button--small button--secondary\" id=\"button_switch_search_mode\">Switch search mode : Classic</a><span style=\"float: right;\" id=\"gmle_count\"></span></p><p><input id=\"filter-map-name\" type=\"text\" placeholder=\"Filter on the map name or description\"></p></div>"
  var div = document.createElement('div');
  div.id = "gmle_header";
  div.innerHTML = header
  divParent.appendChild(div)

  document.getElementById("gmle_button_switch_light_dark_mode").addEventListener("click", function () {
    document.getElementById("gmle_style_light_mode").disabled = !document.getElementById("gmle_style_light_mode").disabled
    localStorage.gmle_dark_mode = document.getElementById("gmle_style_light_mode").disabled
  })

  document.getElementById("button_switch_search_mode").addEventListener("click", function () {
    if (localStorage.gmle_search_mode === "classic") {
      localStorage.gmle_search_mode = "exact"
    } else if (localStorage.gmle_search_mode === "exact") {
      localStorage.gmle_search_mode = "regex"
    } else {
      localStorage.gmle_search_mode = "classic"
    }
    document.getElementById("button_switch_search_mode").innerHTML = { "classic": "Switch search mode : Classic", "exact": "Switch search mode : Exact", "regex": "Switch search mode : Regex" }[localStorage.gmle_search_mode]
    document.getElementById("filter-map-name").dispatchEvent(new Event("input"))
  })

  document.getElementById("button_switch_search_mode").innerHTML = { "classic": "Switch search mode : Classic", "exact": "Switch search mode : Exact", "regex": "Switch search mode : Regex" }[localStorage.gmle_search_mode]

  div = document.createElement('div');
  div.id = "gmle_table";
  div.innerHTML = "<div class=\"table-filter\" style=\"background: #0000001f;margin: 40px;padding: 20px 40px;border-radius: 10px;\">Loading maps ...</div>"
  divParent.appendChild(div)

  function getMaps(sendResponse, maps = null, page = 0, n = 100) {
    fetch("https://www.geoguessr.com/api/v3/likes?page=" + page + "&count=" + n, { method: 'GET', async: true, contentType: 'json'})
      .then((response) => response.json())
      .then((response) => {
        if (maps === null) {
          maps = response
        } else {
          maps = maps.concat(response)
        }
        if (response.length == 0) {
          sendResponse(maps);
        } else {
          getMaps(sendResponse, maps, page + 1, n)
        }
      })
      .catch(function (err) {
        document.getElementById("gmle_table").innerHTML = "An error occured, while getting maps.";
      });
  }

  getMaps(async function (maps) {
    if (!location.pathname.startsWith("/me/likes") || location.pathname.endsWith("#")) {
      return false;
    }

    document.getElementById("gmle_count").innerHTML = maps.length + " maps loaded"

    maps.sort(function (a, b) {
      return a.name.localeCompare(b.name);
    });

    var table = "<div style=\"background: #0000001f;margin: 40px;border-radius: 10px;\"><table class=\"custom-table\"><tr><th>Updated Recently</th><th>Name</th><th>Creator</th><th>Created At</th><th>Updated At</th><th>Description</th><th>Nb coordinates</th><th>Likes</th><th>Nb games</th><th>Actions</th></tr></thead><tbody>";
    table += getHtml(maps);
    table += "</tbody></table></div>";

    document.getElementById("gmle_table").innerHTML = table
    var rows = document.getElementsByClassName("map-name");
    var filter_map_name = document.getElementById("filter-map-name");
    filter_map_name.addEventListener('input', function (evt) {
      var filter_value = filter_map_name.value;
      if (filter_value != "" && filter_value.length > 0) {
        let visible = 0;
        let search_mode = localStorage.gmle_search_mode
        for (var row_index = 0; row_index < rows.length; row_index++) {
          if (typeof rows[row_index] != 'undefined') {
            if (search_mode === "classic" && (rows[row_index].getAttribute('mapname').replace(/[^a-z\d\s]+/gi, "").toLowerCase().includes(filter_value.toLowerCase())
              || rows[row_index].getAttribute('mapdescription').replace(/[^a-z\d\s]+/gi, "").toLowerCase().includes(filter_value.toLowerCase()))) {
              rows[row_index].classList.add("visible");
              visible++;
            } else if (search_mode === "exact" && (rows[row_index].getAttribute('mapname').includes(filter_value) || rows[row_index].getAttribute('mapdescription').includes(filter_value))) {
              rows[row_index].classList.add("visible");
              visible++;
            } else if (search_mode === "regex" && (rows[row_index].getAttribute('mapname').match(filter_value) || rows[row_index].getAttribute('mapdescription').match(filter_value))) {
              rows[row_index].classList.add("visible");
              visible++;
            } else {
              rows[row_index].classList.remove("visible");
            }
          }
        }
        document.getElementById("gmle_count").innerHTML = visible + "/" + rows.length
      } else {
        for (var row_index_2 = 0; row_index_2 < rows.length; row_index_2++) {
          if (typeof rows[row_index_2] != 'undefined') {
            rows[row_index_2].classList.add("visible");
          }
        }
        document.getElementById("gmle_count").innerHTML = rows.length + "/" + rows.length
      }
    });
  })
}

function getHtml(data) {
  var output = '';
  for (let obj in data) {
    var map = data[obj];
    let recent_update = false;
    let day_ago = '';
    let updatedAt = '';
    if (map.updatedAt !== null) {
      let updated = Date.parse(map.updatedAt);
      if ((Date.now() - 2592000000) < updated) {
        recent_update = true;
        day_ago = timeSince(updated);
      }
      updatedAt = new Date(Date.parse(map.updatedAt)).toLocaleString();
    }
    let created = new Date(Date.parse(map.createdAt)).toLocaleString();

    output += '<tr class="map-name visible" mapname="' + map.name + '" mapdescription="' + ((map.description !== null) ? map.description : '') + ' ' + ((map.creator !== null) ? map.creator.nick.replace(/[^a-z\d\s]+/gi, "").toLowerCase() : '') + '">' +
      '<td>' + ((recent_update) ? '✅<br>' + day_ago + ' ago' : day_ago) + '</td>' +
      '<td>' + map.name + '</td>' +
      '<td>' + ((map.creator == null) ? 'GeoGuessr' : map.creator.nick) + '</td>' +
      '<td>' + created + '</td>' +
      '<td>' + updatedAt + '</td>' +
      '<td style="width: 200px; font-size: 10px;">' + map.description + '</td>' +
      '<td>' + map.coordinateCount + '</td>' +
      '<td>' + map.likes + '</td>' +
      '<td>' + map.numFinishedGames + '</td>' +
      '<td style="min-width: 160px;">' +
      '&nbsp;&nbsp;&nbsp;&nbsp;<a target="_blank" class="button button--small button--secondary" href="' + map.url + '"><span class="button__animation"></span><span class="button__label">Open</span></a>' +
      '&nbsp;&nbsp;&nbsp;&nbsp;<a target="_blank" class="button button--small button--primary" href="' + map.playUrl + '"><span class="button__animation"></span><span class="button__label">Play</span></a>' +
      '</td></tr>';
  }
  return output;
}

function timeSince(date) {
  let seconds = Math.floor((new Date() - date) / 1000);
  let interval = seconds / 31536000;
  if (interval > 1) {
    return Math.floor(interval) + " years";
  }
  interval = seconds / 2592000;
  if (interval > 1) {
    return Math.floor(interval) + " months";
  }
  interval = seconds / 86400;
  if (interval > 1) {
    return Math.floor(interval) + " days";
  }
  interval = seconds / 3600;
  if (interval > 1) {
    return Math.floor(interval) + " hours";
  }
  interval = seconds / 60;
  if (interval > 1) {
    return Math.floor(interval) + " minutes";
  }
  return Math.floor(seconds) + " seconds";
}