Greasy Fork is available in English.

match data importer

imports match data from compatible exports

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         match data importer
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  imports match data from compatible exports
// @author       Hendrik Steinmetz
// @match        https://homecourt.vcc.heimspiel.de/01_admin/spiele/*
// @match        https://homecourt.vcc.heimspiel.de/01_admin/spiele/spiel_aktionen.php
// @icon         https://www.google.com/s2/favicons?sz=64&domain=heimspiel.de
// @grant        GM_addStyle
// @license      GPLv3
// ==/UserScript==

let btn = document.createElement("button");
btn.id = "floatingButton";
btn.textContent = "Import";
btn.onclick = () => {
  document.getElementById("popup-body").value = "";
  document.querySelector("#customPopup").style.display = "block";
};

function triggerImport() {
  document.querySelector("#loadingSpinner").style.display = "block";
  const json = document.getElementById("popup-body").value;
  if (json) {
    const report = JSON.parse(json);
    if (!report) {
      document.querySelector("#loading-spinner").style.display = "none";
    }

    const homeTable = document.querySelectorAll("table")[0];
    const awayTable = document.querySelectorAll("table")[1];
    fillTable(
      homeTable,
      report.players?.filter((p) => p.home) ?? [],
      report.events.substitutions?.filter((sub) => sub.home) ?? [],
      report.events.cards?.filter((card) => card.home) ?? [],
      report.source
    );
    fillTable(
      awayTable,
      report.players?.filter((p) => !p.home) ?? [],
      report.events.substitutions?.filter((sub) => !sub.home) ?? [],
      report.events.cards?.filter((card) => !card.home) ?? [],
      report.source
    );
  }
  document.querySelector("#loadingSpinner").style.display = "none";
}

/**
 * @param table {HTMLTableElement}
 */
function fillTable(table, playerData, subData, cardData, source) {
  const mappedPlayers = [];
  const rowArray = Array.from(table.rows);
  for (let i = 3; i < table.rows.length - 1; i++) {
    const currentRow = rowArray[i];
    const cells = Array.from(currentRow.cells);

    const numberCell = cells[0];
    const nameShortCell = cells[1];
    const fullNameCell = cells[2];
    const startingCell = cells[4];
    const benchCell = cells[6];

    const subOnCell = cells[8];
    const subOnMinuteCell = cells[9];
    const subOnAddedCell = cells[10];

    const subOffCell = cells[12];
    const subOffMinuteCell = cells[13];
    const subOffAddedCell = cells[14];

    const yellowCell = cells[16];
    const yellowMinuteCell = cells[17];
    const yellowAddedCell = cells[18];

    const yellowRedCell = cells[20];
    const yellowRedMinuteCell = cells[21];
    const yellowRedAddedCell = cells[22];

    const redCell = cells[24];
    const redMinuteCell = cells[25];
    const redAddedCell = cells[26];

    const checkCell = (cell) => (cell.querySelector("input").checked = true);
    const uncheckCell = (cell) => (cell.querySelector("input").checked = false);
    const setCellValue = (cell, val) =>
      (cell.querySelector("input").value = val);

    const numberInput = numberCell.querySelector("input");
    if (!numberInput) {
      console.log("Number input null for", currentRow);
      continue;
    }

    let searchName = fullNameCell.innerText;
    if (source && source === "soccerway" && nameShortCell.innerText.includes(" ") && nameShortCell.innerText.includes(",")) {
      searchName =
        nameShortCell.innerText.split(" ")[1].charAt(0) + ". " + nameShortCell.innerText.split(" ")[0];
      searchName = searchName.replaceAll(",", "");
    }
    const player = findPlayer(
      playerData,
      numberInput.value,
      searchName
    );
    if (!player) {
      uncheckCell(startingCell);
      uncheckCell(benchCell);
      continue;
    }

    let playerId = player.id;

    if (!player.sub) {
      checkCell(startingCell);
      uncheckCell(benchCell);
    } else if (player.sub) {
      uncheckCell(startingCell);
      checkCell(benchCell);
    }

    if (playerWasSubbedOn(playerId, subData)) {
      const substitution = subData.find((sub) => sub.on === playerId);
      checkCell(subOnCell);
      checkCell(benchCell);
      setCellValue(subOnMinuteCell, substitution.minute);
      setCellValue(subOnAddedCell, substitution.added ?? "");
    }

    if (playerWasSubbedOff(playerId, subData)) {
      const substitution = subData.find((sub) => sub.off === playerId);
      checkCell(subOffCell);
      setCellValue(subOffMinuteCell, substitution.minute);
      setCellValue(subOffAddedCell, substitution.added ?? "");
      if (!playerWasSubbedOn(playerId, subData)) {
        checkCell(startingCell);
      }
    }

    const cardEvents = cardData.filter((p) => p.id === playerId);
    if (cardEvents.length > 0) {
      cardEvents.forEach((event) => {
        if (event.card === "yellow") {
          checkCell(yellowCell);
          setCellValue(yellowMinuteCell, event.minute);
          setCellValue(yellowAddedCell, event.added ?? "");
        } else if (event.card === "yellow-red") {
          checkCell(yellowRedCell);
          setCellValue(yellowRedMinuteCell, event.minute);
          setCellValue(yellowRedAddedCell, event.added ?? "");
        } else if (event.card === "red") {
          checkCell(redCell);
          setCellValue(redMinuteCell, event.minute);
          setCellValue(redAddedCell, event.added ?? "");
        }
      });
    }

    mappedPlayers.push(playerId);
    playerData = playerData.filter((p) => p.id !== playerId);
  }
  const unmapped = playerData.filter((p) => !mappedPlayers.includes(p.id));
  const list = document.createElement("ul");

  let container = document.getElementById("unmappedPlayersContainer");
  if (!container) {
    container = document.createElement("div", {
      id: "unmappedPlayersContainer",
    });
    container.style.marginTop = "10px";
    const caption = document.createElement("strong");
    caption.innerText =
      "The following players from the match report could not be found in the table:";
    container.appendChild(caption);
  }

  unmapped.forEach((p) => {
    const item = document.createElement("li");
    item.innerText = `${p.number} - ${p.name}`;
    list.appendChild(item);
  });
  container.appendChild(list);

  table.insertAdjacentElement("afterend", container);
}

/**
 * Finds players based on number and name:
 * - if no player with number is found: name is used
 * - if multiple with same number: use most similar name
 */
// TODO clean up and make more efficient
// ONLY ONE similarity computation per filter
// -> add as key and sort by
//
// one list filtered by number and one by name
// if only one in number list: return if sim > 0.5
// else if many in number list: sort by sim return first if sim > 0.5
// else if no number or none in number list: return first of name search if sim > 0.5
// return null
function findPlayer(players, number, nameFull) {
  console.log("Searching player:", nameFull, number)
  const playersByNumber = players.filter(
    (player) => parseInt(player.number) === parseInt(number)
  );
  nameFull = nameFull.toLowerCase();
  const firstName = nameFull.split(" ")[0];

  const similiarityThreshold = 0.6;

  // ONLY ONE PLAYER FOUND OR NUMBER WAS SUPPLIED
  if (playersByNumber.length === 1 || !number) {
    if (
      number &&
      similarity(
        playersByNumber[0].name,
        //playersByNumber[0].name.split(" ")[0].toLowerCase(),
        nameFull
      ) > similiarityThreshold
    ) {
      return playersByNumber[0];
    }

    const playersByName = players.filter((p) =>
      similarity(p.name.toLowerCase(), nameFull)
    );
    if (playersByName.length === 0) return null;

    playersByName.sort(
      (a, b) =>
        similarity(b.name.toLowerCase(), nameFull) -
        similarity(a.name.toLowerCase(), nameFull)
    );
    const firstScore = similarity(
      playersByName[0].name.toLowerCase(),
      nameFull
    );

    if (firstScore > similiarityThreshold) {
      return playersByName[0];
    }
  }

  // MULTIPLE PLAYERS WITH SAME NUMBER FOUND => return highest similarity
  if (playersByNumber.length > 1) {
    playersByNumber.sort(
      (a, b) =>
        similarity(b.name.toLowerCase(), nameFull) -
        similarity(a.name.toLowerCase(), nameFull)
    );
    const firstScore = similarity(
      playersByNumber[0].name.toLowerCase(),
      nameFull
    );

    if (firstScore > similiarityThreshold) {
      return playersByNumber[0];
    }
  }

  // NO PLAYER FOUND WITH NUMBER => search by name
  if (playersByNumber.length === 0) {
    console.log(players, number, nameFull);
    const nameSearch = players.filter(
      (p) => similarity(p.name.toLowerCase(), nameFull) > similiarityThreshold
    );
    nameSearch.sort(
      (a, b) =>
        similarity(b.name.toLowerCase(), nameFull) -
        similarity(a.name.toLowerCase(), nameFull)
    );

    console.log("similarity search result", nameSearch);

    if (nameSearch.length === 0) {
      console.log("PLAYER NOT FOUND", nameFull);
      return null;
    }

    const firstScore = similarity(nameSearch[0].name.toLowerCase(), nameFull);

    return firstScore > similiarityThreshold ? nameSearch[0] : null;
  }

  return null;
}

function playerWasSubbedOn(playerId, subData) {
  return subData.filter((sub) => sub.on === playerId).length > 0;
}
function playerWasSubbedOff(playerId, subData) {
  return subData.filter((sub) => sub.off === playerId).length > 0;
}

let popup = document.createElement("div");
popup.id = "customPopup";
popup.innerHTML = `
<div class="popup-content">
  <h2 id="popup-title">Import</h2>
  <textarea id="popup-body"></textarea>
  <div id="loadingSpinner" class="spinner"></div>
  <button id="closePopup">Close</button>
  <button id="importBtn">Start import</button>
</div>
`;

document.addEventListener("click", function (event) {
  if (event.target.id === "closePopup") {
    document.getElementById("customPopup").style.display = "none";
  }
  if (event.target.id === "importBtn") {
    triggerImport();
  }
});

GM_addStyle(`
        #floatingButton {
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 10px 20px;
            background-color: #007BFF;
            color: white;
            font-size: 16px;
            font-weight: bold;
            border: none;
            border-radius: 10px;
            box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
            cursor: pointer;
            z-index: 9999;
            transition: background-color 0.3s ease, transform 0.2s ease;
        }
        #floatingButton:hover {
            background-color: #0056b3;
            transform: scale(1.1);
        }
        #customPopup {
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            width: 50%;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            border-radius: 10px;
            z-index: 10000;
        }
        #popup-title {
            text-align: center;
        }
        #popup-body {
            border: 1px solid #ccc;
            width: 100%;
            height: 120px;
            padding: 5px;
            font-size: 14px;
            border: none;
            resize: none;
            background: #f8f8f8;
            outline: none;
        }
        #closePopup {
            margin-top: 10px;
            padding: 8px 16px;
            background-color: #dc3545;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        #closePopup:hover {
            background-color: #c82333;
        }
        #importBtn {
            margin-top: 10px;
            margin-left: 10px;
            padding: 8px 16px;
            background-color: #007BFF;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        #importBtn:hover {
            background-color: #0056b3;
        }
        .spinner {
            display: none;
            margin: 10px auto;
            width: 40px;
            height: 40px;
            border: 4px solid rgba(0, 0, 0, 0.2);
            border-top: 4px solid #007BFF;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    `);
document.body.appendChild(btn);
document.body.appendChild(popup);

function similarity(s1, s2) {
  var longer = s1;
  var shorter = s2;
  if (s1.length < s2.length) {
    longer = s2;
    shorter = s1;
  }
  var longerLength = longer.length;
  if (longerLength == 0) {
    return 1.0;
  }
  return (
    (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength)
  );
}

function editDistance(s1, s2) {
  s1 = s1.toLowerCase();
  s2 = s2.toLowerCase();

  var costs = new Array();
  for (var i = 0; i <= s1.length; i++) {
    var lastValue = i;
    for (var j = 0; j <= s2.length; j++) {
      if (i == 0) costs[j] = j;
      else {
        if (j > 0) {
          var newValue = costs[j - 1];
          if (s1.charAt(i - 1) != s2.charAt(j - 1))
            newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
          costs[j - 1] = lastValue;
          lastValue = newValue;
        }
      }
    }
    if (i > 0) costs[s2.length] = lastValue;
  }
  return costs[s2.length];
}