kommu / GeoGuessr Maps List Enhanced

// ==UserScript==
// @name GeoGuessr Maps List Enhanced
// @namespace kommu
// @description Display all your maps in a simple table. Thx to Nicolas for recent updates.
// @version 1.0.1.0
// @match 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/maps") || 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 = 30) {
    fetch("https://www.geoguessr.com/api/v4/user-maps/maps?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.";
      });
  }

  async function getDrafts() {
    const response = await fetch('https://www.geoguessr.com/api/v4/user-maps/dangling-drafts', { method: 'GET', async: false, contentType: 'json'});
    const json = await response.json();
    return json;
  }

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

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

    console.log('test');
    let drafts = await getDrafts();
    if (drafts !== null && drafts.length > 0) {
      maps = maps.concat(drafts);
    }

    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\"><thead><tr><th>published</th><th>Name</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

    document.addEventListener('click', function (event) {
      if (event.target.matches('.publish--button')) {
        event.preventDefault();
        let slug = event.target.getAttribute('data-target')
        const requestOptions = {method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({})};
        fetch('https://www.geoguessr.com/api/v4/user-maps/drafts/' + slug + '/publish', requestOptions).then(response => response.json()).then(data => window.location.reload());
      }

      if (event.target.matches('.unpublish--button')) {
        event.preventDefault();
        let slug = event.target.getAttribute('data-target')
        const requestOptions = {method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({published: false})};
        fetch('https://www.geoguessr.com/api/v3/profiles/maps/' + slug, requestOptions).then(response => response.json()).then(data => window.location.reload());
      }

      if (event.target.matches('.delete--button')) {
        event.preventDefault();
        var confirmation = confirm('Are you sure you want to delete this map?');
        if (!confirmation) {
          event.stopPropagation();
        }
        let slug = event.target.getAttribute('data-target')
        const requestOptions = {method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({})};
        fetch('https://www.geoguessr.com/api/v4/user-maps/' + slug, requestOptions).then(response => response.json()).then(data => window.location.reload());
      }
    }, false);

    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 = '';
  var already_added = [];
  for (let obj in data) {
    var map = data[obj];
    if (already_added.includes((map.hasOwnProperty('id')) ? map.id : map.slug)) {
        continue;
    }
    already_added.push((map.hasOwnProperty('id')) ? map.id : map.slug);
    output += '<tr class="map-name visible" mapname="' + map.name + '" mapdescription="' + ((map.description !== null) ? map.description : '') + '">' +
      '<td>' + ((!map.hasOwnProperty('published')) ? '🟥' : ((map.published) ? '✅' : '')) + '</td>' +
      '<td>' + map.name + '</td>' +
      '<td style="width: 200px">' + ((map.description !== null) ? map.description : '') + '</td>' +
      '<td>' + ((map.hasOwnProperty('coordinateCount')) ? map.coordinateCount : '0') + '</td>' +
      '<td>' + ((map.hasOwnProperty('likes')) ? map.likes : '0') + '</td>' +
      '<td>' + ((map.hasOwnProperty('numFinishedGames')) ? map.numFinishedGames : '0') + '</td>' +
      '<td class="table-row-actions">' +
      '<a target="_blank" class="button button--small button--secondary" href="/map-maker/' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '"><span class="button__animation"></span><span class="button__label">Edit</span></a>' +
      ((map.hasOwnProperty('published')) ? '<a target="_blank" class="button button--small button--primary" href="/maps/' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '"><span class="button__animation"></span><span class="button__label">Open</span></a>' : '') +
      ((!map.hasOwnProperty('id') || (map.hasOwnProperty('published') && !map.published)) ? '<a class="publish--button button button--small button--primary" data-target="' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '"><span data-target="' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '" class="publish--button button__label">Publish</span></a>' : '') +
      ((map.hasOwnProperty('published') && map.published) ? '<a class="unpublish--button button button--small button--secondary" data-target="' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '"><span data-target="' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '" class="unpublish--button button__label">Unpublish</span></a>' : '') +
      '<a class="delete--button button button--small button--secondary" data-target="' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '"><span data-target="' + ((map.hasOwnProperty('id')) ? map.id : map.slug) + '" class="delete--button button__label">Delete</span></a>' +
      '</td>' +
      '</tr>';
  }
  return output;
}