// ==UserScript==
// @name Mazyar
// @namespace http://tampermonkey.net/
// @version 2.43
// @description Swiss Army knife for managerzone.com
// @copyright z7z from managerzone.com
// @author z7z from managerzone.com
// @license MIT
// @run-at document-idle
// @noframes
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect self
// @require https://unpkg.com/dexie@4.0.8/dist/dexie.min.js
// @match https://www.managerzone.com/*
// @match https://test.managerzone.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @supportURL https://github.com/mz-ir/mazyar
// ==/UserScript==
(async function () {
"use strict";
const DEADLINE_INTERVAL_SECONDS = 30; // in seconds
let mazyar = null;
/* *********************** Changelogs ********************************** */
const currentVersion = GM_info.script.version;
const changelogs = {
"2.43": ["<b>[new]</b> Manager Ranking: add value and average top players for each team. You can sort them by this two columns too."],
"2.42": ["<b>[improve]</b> Squad Summary: add share and market icon for your own players too."],
"2.41": ["<b>[new]</b> Notebook: add a note icon to MZY Toolbar to open/hide a notebook. It stores your note and you can stick it to a corner to be always be available and visible."],
"2.40": ["<b>[fix]</b> Transfer: change Fee font color to blue."],
"2.39": ["<b>[new]</b> Transfer: for non one-club players, the price that current club paid for the player is added next to 'Days at this club'."],
"2.38": ["<b>[fix]</b> Transfer Filters: delete icon was missing in 'MZY Transfer Filters' modal."],
"2.37": ["<b>[new]</b> support Managerzone Test Site (test.managerzone.com). It is not fully tested. Please report any issues you encounter in Test site too."],
"2.36": [
"<b>[new]</b> Deadline Alert: add 'Timeout' option in <b>MZY Settings</b> to set deadline timeout. Its value must be between 1 and 360 minutes.",
"<b>[new]</b> Deadline Alert: add 'Sound Notification' option to play a bell sound when deadline of at least one of monitored players is less than timeout.",
"<b>[fix]</b> Deadline Alert: trash icon was missing in 'MZY Transfer Deadlines' modal when 'van.mz.playerAdvanced' script is enabled.",
"<b>[fix]</b> MZY Settings: 'Mark maxed skills' option was missing."
],
"2.35": ["<b>[new]</b> Days at this club: add to player profiles in training report. It is optional and disabled by default. You can enable it from MZY Settings."],
"2.34": ["<b>[new]</b> <b>(Experimental)</b> Transfer: add deadline alert."],
"2.33": ["<b>[fix]</b> Federation Front Page: add top players when current federation is changed."],
"2.32": ["<b>[improve]</b> Federation: first team member sort"],
"2.31": ["<b>[new]</b> Clash: add average age of top players and teams senior league for each team. this feature is not supported in mobile view."],
"2.30": ["<b>[fix]</b> Transfer Filters: reset selected H & L checkboxes when Transfer filter is not enabled."],
"2.29": ["<b>[fix]</b> Hide Players: fixed an issue about hide icon when transfer scout filters are used."],
"2.28": ["<b>[fix]</b> Days at this club: after v2.27, it was broken in players page."],
"2.27": ["<b>[new]</b> Transfer Market: it adds a trash icon next to player ID in search result. click on the icon to <b>hide the player. To remove players from hide list, use 'MZY Hide' button."],
"2.26": [
"<b>[improve]</b> Days at this club: it is optional. It is disabled by default. You can enable it from MZY Settings.",
"<b>[improve]</b> Player Profile: it stores player profiles in local database to reduce number of requests.",
"<b>[improve]</b> Local Database: it deletes outdated local data to reduce the size of database.",
"<b>[improve]</b> Transfer: it uses less ajax requests now.",
],
"2.25": ["<b>[new]</b> Training Report: click on player's camp package icon to open its camp report."],
"2.24": ["<b>[fix]</b> Player Profile: fix Days at this club for injured or suspended players."],
"2.23": [
"<b>[new]</b> Squad Profile: add 'days at this club' to each player profile.",
"<b>[fix]</b> Player Comment: show comment icon for players when selected tab changes.",
"<b>[fix]</b> Player Comment: change color of comment icon to lightskyblue when player has no comment. (previous color was the same as loyal players background)",
],
"2.22": ["<b>[new]</b> Hire Coaches: adds salary range of each coach. Thanks to <a href=\"https://www.managerzone.com/?p=profile&uid=8577497\">@douglaskampl</a> for suggesting the idea and sharing his implementation."],
"2.21": ["<b>[new]</b> Club Page: adds total trophy count."],
"2.20": ["<b>[new]</b> Player Profile: add 'Days at this club' counter."],
"2.19": ["<b>[new]</b> Squad Summary: it marks players whose skills are shared. click on share icon to see the player in place.",
"<b>[new]</b> Squad Summary: it marks players that are in transfer market. click on transfer icon to see the player in market."],
"2.18": ["<b>[new]</b> show changelog after script update.",
"<b>[improve]</b> change icon style of player's comment."],
"2.17": ["<b>[fix]</b> fixed total skill balls"],
}
/* *********************** Styles ********************************** */
const styles = `
#mazyar-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
}
#mazyar-modal-content {
width: fit-content;
background-color: beige;
border-radius: 5px;
max-height: 100%;
}
.mazyar-notebook-plain {
z-index: 9990;
position: absolute;
left: 0;
top: 0;
flex-wrap: nowrap;
}
.mazyar-notebook-modal {
position: fixed;
top: 0;
left: 0;
background-color: beige;
border-radius: 5px;
min-width: 200px;
min-height: 250px;
max-width: 95vw;
max-height: 99vh;
flex-wrap: nowrap;
}
.mazyar-notebook-textarea {
padding: 5px;
min-width: 180px;
resize: none;
width: 90%;
flex: 1;
}
.mazyar-scrollable-vertical {
overflow-x: clip;
overflow-y: auto;
}
.mazyar-resizable {
resize: both;
}
.mazyar-flex-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
max-height: 100%;
}
.mazyar-flex-container-row {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.mazyar-donut {
width: 1.7em;
height: 1.7em;
margin-right: 5px;
border-radius: 50%;
text-align: center;
font-size: 1.2em;
padding: 3px;
background-color: yellow;
color: yellow;
}
.mazyar-final-donut {
border: rgb(213, 232, 44) solid 2px;
color: inherit;
padding:0;
}
.mazyar-loading-donut {
border-bottom-color: rgb(213, 232, 44);
animation: 1.5s mazyar-donut-spin infinite linear;
}
@keyframes mazyar-donut-spin {
to {
transform: rotate(360deg);
}
}
table.mazyar-table {
border-spacing: 0;
width: auto;
}
table.mazyar-table tbody td, table.mazyar-table thead th {
padding: 0.3em 0.5em;
vertical-align: middle;
}
@media only screen and (max-device-width: 1020px) {
thead.responsive-show.mazyar-responsive-show {
display: table-header-group !important;
}
tbody tr.responsive-show.mazyar-responsive-show {
display: table-row !important;
}
}
a.mazyar-in-progress-result {
animation: mazyar-change-result-background 3s infinite;
}
@keyframes mazyar-change-result-background {
0% {background-color: inherit;}
50% {background-color: lightgreen;}
100% {background-color: inherit;}
}
div.mazyar-deadline-throb-lightgreen {
animation: mazyar-throb-deadline-icon 3s infinite;
}
@keyframes mazyar-throb-deadline-icon {
0% {color: yellow;}
50% {color: crimson;}
100% {color: yellow;}
}
span.mazyar-icon-delete {
display: inline-block;
cursor: pointer;
background-image: url("nocache-869/img/player/discard.png");
width: 21px;
height: 20px;
}
button.mazyar-button {
cursor: pointer;
}
div.mazyar-transfer-control-area {
width: 50%;
}
.mazyar-dim-50 {
opacity: 50%;
}
.mazyar-dim-60 {
opacity: 60%;
}
.mazyar-hide {
display: none;
}
.mazyar-scout-h {
font-weight: bold;
}
.mazyar-scout-1 {
color: red;
}
.mazyar-scout-2 {
color: darkgoldenrod;
}
.mazyar-scout-3 {
color: blue;
}
.mazyar-scout-4 {
color: fuchsia;
}
.mazyar-player-comment-icon-www {
vertical-align: top;
}
.mazyar-player-comment-icon-www span.player_icon_wrapper {
text-align: center;
}
.mazyar-player-comment-icon-inactive {
color: lightskyblue;
}
.mazyar-player-comment-icon-active {
color: blue;
}
.mazyar-player-comment-textarea {
margin: 0.5rem;
min-width: 220px;
min-height: 100px;
}
.mazyar-days-at-this-club {
margin: 5px;
padding: 3px;
background-color: azure;
border-radius: 5px;
border: 1px solid black;
text-align: center;
}
.mazyar-days-at-this-club-entire {
margin: 5px;
padding: 3px;
background-color: greenyellow;
border-radius: 5px;
border: 1px solid black;
text-align: center;
}
`;
/* ********************** Constants ******************************** */
const TRANSFER_INTERVALS = {
always: {
value: "0",
label: "always",
},
onceMinute: {
value: "1",
label: "once a minute",
},
onceHour: {
value: "2",
label: "once an hour",
},
onceDay: {
value: "3",
label: "once a day",
},
never: {
value: "4",
label: "never",
},
};
const deadlineAlertSound =
"data:audio/mpeg;base64,//OExAAAAAAAAAAAAFhpbmcAAAAPAAAAKAAADbAAAQEfHy4uLjMzOTk5Pz9CQkJISE9PT2ZmbW1tc"
+ "3N2dnZ6eoCAgImJlJSUo6OoqKiurrOzs7m5v7+/xMTKysrNzdHR0dXV2dnZ3Nzg4ODk5Ofn5+vr7+/v8/P09PT4+P7+/v//AAAAC"
+ "kxBTUUzLjEwMARIAAAAAAAAAAAVCCQCzCEAAZoAAA2wXtypYQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//MUxAAAAAP8AUAAAACAS"
+ "QCQRgCASLq7//PExAsjqwbDH4dpIB1CMEAD6dTAceIh3ZSwkBKkv/gDUEnGWPf6boppjjHOHIAuAsH0EGMjxmOcpE8OeRA5gmYKe"
+ "CRoflaB40QTHmHMOl8RgYoXMFb3/SOjwUaLL7HAtYywQgSgviYEoaEv/+ipSKZcVTMDRMe5THoShocHAPApjgHgX///Wp//5fNwu"
+ "g2EuseMh+mJdpVnl7fQKJA0AAMYlm5M3efLtmZ5pmpiPi+lPeGNDVo8maLthyIjQOnQUKcHBPHPC86JxSY1y6Sw0DATakZLSWQIZ"
+ "YrmIrUZwslwgRZJ4umKZVMiKmpDSdZIgIBAhGatZYC1Qad7A3THKNEBeA2MjtLpeOEy8bhJn18nSyH7D0ntaktEyNiZI4eiqY/0r"
+ "M9ev67OOoMGn//2MgAZP//KICzT1ZkAcAABn2BbgANQeBkFvylb//5MjaS/orSRpfqp7X5xHv//0SZJEBsJ1NH+y1f//rMP/Q9PS"
+ "/WuqqizrYvGxSFbDjHaBBgs//OExO8p2wbjH5iYJOJ4qtwAAAd9gqwA9A7N6hlx3f+r/3X3TQsZD0l/+v9Y6gWlEMNm/6///zpn/"
+ "6f////uxsMqBiwYUAn2Az3CwB8FkFDf/6cw4y2nY4bOvdP/6hugHgOmr/3dL//+r//////1qGuBiuQGmggAAcDXgDv+Bjf///+kg"
+ "MUn//+cCbu//t//+pf//////RckRrGsX2oAAe4DfcOQAHqcv9fkAZ/czPTqellTejNNGFToMr//UZgJ//M0xPoSaqa7H8WYAWMG/"
+ "+v//6lf/////+pZEAHgTqoCCS0j+oDQP7J9Z/WgggdSUibTXdaisMiHVLL5RFjRIJ5fJFCmuQia//M0xOsOAqqqXhAoNIjQjPrPZ"
+ "tFdxg1xmTp2p1L9loLFCAD1Fbn2/TVe3//rX//////9h1hQxQa/b/YDeAGgAbtBQEACJhTQ//M0xO4LYqKkXAKm9bUQ42ig/jRkF"
+ "GZrOMIGC5Uy5EcPBywwRQCBzKjyEcTDDAixkUZIQOi5NK44/C80mmu3+all2fQkIUl+//MkxPsJ4qa0ngCa1JahVCjRgAozJgDMA"
+ "DFqTAkDYJjBnxYEAm4GADplB4wwhBKn//M0xPYNEqKmXgHanFFkGsGPGjp43yQxM06JcvIrQ+ymjPH5fNiEEwM7kzJIfjzAGoI3p"
+ "Jx8CiROuscgABwRW9uTOUAYgIky//NExPwUwqaaXGsnKAZAVClUdJKAsGRoAo1NgctjDQLeMvpImtYyhIBIWl+y1A4UFfQwYSOBB"
+ "2wQhH1NY0OHIgtYEIlcnRD5aili8Qh1QNoLjuAo6DhsYZKimsAqNyKF//OkxPxF4r6SXtY1yOh5Ibl8ELEVQSAMJyWB0vRoAJmRa"
+ "djNIAg6X9rrqYbUqwACcACOKgICPYPFjrprgWaVimlY1hv/Q9/rak6Ojdf/Wyklo//9JLRBSDdH7KU6P0bfo+pKjrbUl/baLmzJc"
+ "dCUUxFiSk1Ke1W0ucxEp0lH4CBHgS2HCIAfDygAfu3m0N//+NDP/9BFT///rH4DXzA+3+rf//1f//////TLgucLoi3oKuAAAMH4t"
+ "wAcPx1Dt////d/WpPf//6A6wgwi3/V///U3/r////6KKRdJcJgEDNUAB4H41wA+MHfXtVs3/s6e79///TLgBEldf/Z///1t///+v"
+ "9/61GZIBjhxagAB/gAOB5QAdxon18ix/////fqT//NExPcWaqLW/kNZHXV///Y3AVROK+rfu3//W///////sfJYMjDEertpfgd8w"
+ "JQAm4tHEMMSAc3UyVhQDjgIr6DEEHtN76ziJmkpTE2keNzhVQLpsYLJsyQTL5F5MIUN//M0xPALiqa2niglJP7/0CYIIFYFMpk+n"
+ "/X//qdWgimhQ3Qp7skymNzA0mCBoX01WW3QroHSkSwFHjIn15mIeoCQeFVAB/4y//MkxPwLWqauPggmGLeAVAP5OT0hhleGHOcFi"
+ "RHPH/hue6ablgXIlIcVqxnx7UeG//MkxPEK2qasngCk8OjfX/4zZwQDHQS1a02UyClKYcwgh4gAyhU6ygHJAsb2Bvcc//M0xOgLo"
+ "qaqXgBiiMGYIoeJguglEKFN1K/GZSLyOvDAAnAQUEoFQToRQ0Of///oMpBkNi+bm+sjD//84Mgaf/dhYAbnil3e//NUxPQZ+qqeX"
+ "0OYAIu5vYnYhYa3/hgUkTQAAbuDis9Wrqkaa0nWrVZfTbyuBdVBAYa4thFy0MsiUiSFmg3QYjY+i1I0L1B6pRLBsYECGuXSDEWJ8"
+ "mVlhFjInjcZkbw7gxidcxJJAmtaisRtFFSY5A7kbpKDEY+k//NkxPciswbDHZiYALnQacNMNyVK5FzE1dFSJiYjicnnyRMhBUiJ4"
+ "umubGJkmXVHUCmVRzjL/qXW31LQe51ILsHK//qFv//zMFgbqpiAgAAAi2g8gAGDweFyK1e1f//izCfb6m7f9TPq3WWTZ1Kb6/+iT"
+ "IfqARgypBTX/t//9ZgNd2/rUr////1E0MsDdxEc//OExO8qiwbjH5iIBLXgADug8APN6h3CZv///0evsmeZbt//1JkwBR5JJ/q1f"
+ "//zhKf/////+s4NcEEyfUoADa8GwAfCYHif/r9+6PW93Z0BsoHGa739f1oEPADIiCv+p///zIa7f9v////RSNSBAIgQM1UADUYDb"
+ "8OQAEwxD8wNP1CnjSf9Jbf9TKSOo2QSSL5QJl1s3qfo/uaA8hPo/60av//pkx//////7JmAmZk6/G//E3AF6vW/xXAiV21P//M0x"
+ "PcRGqa3H8WQATzz36up5+nn0lSILaN16NRv5kAD88l/6///yse///6000EKqy+9mXb2UakuJCoAD4ULfcH8AGIQif4h//M0xO0L2"
+ "qap/BAmGAvjGjs/WtkUdlooUlIsnZ9svkk6ls7qd2R/6hugVhw8//b/+ikkk5iPD//r3U/UlMS6zJLWpJKroF8X//M0xPgNSqagf"
+ "gKk4CBsw8PVHAtgA+DeGjf///+hglki///0x0Adjk2/1UF///5gO7//////qWZD5CRy6pUQwH/AHzxCf/////M0xP0PKqaiXgBmb"
+ "P6mLX///SBHjD/mMp1//+UFn/////+k04lAOAoDqgwHA9wA+JQd////+gUC6h///ogjgm2+1f///UZf//M0xPsPEp6sfgKavP///"
+ "+3+mOgWUaUCBwBoAPkoPP////qW5SHf///0gFWXH////+o9//////+gvB1VABkBgPsAPjgAKd3///M0xPkTCqaqXgBibP//1NF3/"
+ "//KwLhNv1N///1mX//////2L5MdEOBLSB8Ihf////+oHjZ///6zAIG/9////O///////j5K//M0xOcK0qakHgKm8MeVIOAMyB8aD"
+ "P////6KEn///WAzlf////qMv//////zo7Ug4HoQHyYz////+g8GW///zAD1O/7f//9S//MkxPYJ2qK0PgRU+P//////+VOqCQcCw"
+ "ID5dHr///9/Uw1C7///0gGSMP9v///J//MkxPEJqqKsXgHk5P//////x2oAQDgCBAfEIFv///1N8qobf///QA4mf////9f///Mkx"
+ "O0JIqKwXgRU+P/////MagFwOBvIB8zGA/////Wohf//84FOh/p///1J//////MkxOsJcqK4ngHa8P//oCoDBwLQwPhQsf////xXO"
+ "///6gEF93/p///6ajDAD5MG//MkxOgIkqKsPgHg8H////6GCz///nASdX/////9dRwH8uDah////7IJic///8vg//MkxOgH8p6wP"
+ "gHa8MBn7/////9SAD0AoVepE/8XwqrChgzJftMv/NjGih/1PRZI//MkxOsICqK0PgHa8MieM/YCokdCCEqCkwFm4UeF46akNIKKG"
+ "HJb/b/IqYF0EhE3//MkxO0IYqKoXgSE+P//pkxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MkxO4IUp6sfgHa8Kqqq"
+ "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MkxO8IIp64fgNE+Kqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
+ "qqqqqqq//MkxPEGUW60XgCU4Kqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MUxPoFmW6oOAHa8Kqqqqqqqqqqq"
+ "qqq//MkxO4GKW6gGUBoAKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//M0xPgQMcqoeYCgAKqqqqqqqqqqqqqqq"
+ "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MUxPIAAAP8AYAAAKqqqqqqqqqqqqqq";
/* *********************** Utils ********************************** */
function parseMzDate(dateString) {
const [day, month, year] = dateString.split('-').map(Number);
return new Date(year, month - 1, day);
}
function parseMzDateTime(dateTimeString) {
const [date, time] = dateTimeString.split(' ');
const [day, month, year] = date.split('-').map(Number);
const [hours, minutes] = time.split(':').map(Number);
return new Date(year, month - 1, day, hours, minutes);
}
function generateUuidV4() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function filterHitsIsValid(hits) {
return typeof hits === "number" && hits >= 0;
}
function hasDuplicates(array) {
return new Set(array).size !== array.length;
}
function getSportType(doc = document) {
const zone = doc.querySelector("a#shortcut_link_thezone");
if (zone) {
return zone.href.indexOf("hockey") > -1 ? "hockey" : "soccer";
}
return "soccer";
}
function getClubCurrency(doc) {
const players = doc.getElementById("playerAltViewTable")?.querySelectorAll("tbody tr");
if (players && players.length > 0) {
const parts = players[0].querySelector("td:nth-child(3)")?.innerText.split(" ");
return parts[parts.length - 1].trim();
}
return "";
}
function getNationalCurrency(doc) {
// works for both domestic and foreign countries
const playerNode = doc.getElementById("thePlayers_0")?.querySelector("table tbody tr:nth-child(6)");
if (playerNode) {
const parts = playerNode.innerText.split(" ");
return parts[parts.length - 1].trim();
}
return "";
}
function extractTeamId(link) {
const regex = /tid=(\d+)/;
const match = regex.exec(link);
return match ? match[1] : null;
}
function extractPlayerID(link) {
const regex = /pid=(\d+)/;
const match = regex.exec(link);
return match ? match[1] : null;
}
function extractMatchID(link) {
const regex = /mid=(\d+)/;
const match = regex.exec(link);
return match ? match[1] : null;
}
function extractPlayerIDFromTransferMonitor(link) {
const regex = /u=(\d+)/;
const match = regex.exec(link);
return match ? match[1] : null;
}
function getPlayerIdFromContainer(player) {
return player?.querySelector("h2 span.player_id_span")?.innerText;
}
function isMatchInProgress(resultText) {
const scoreRegex = /\b(X|0|[1-9]\d*) - (X|0|[1-9]\d*)\b/;
return !scoreRegex.test(resultText);
}
function getSquadSummaryLink(tid) {
return `https://${location.hostname}/?p=players&sub=alt&tid=${tid}`;
}
function formatBigNumber(n, sep = " ") {
if (n) {
const numberString = n.toString();
let formattedParts = [];
for (let i = numberString.length - 1; i >= 0; i -= 3) {
let part = numberString.substring(Math.max(i - 2, 0), i + 1);
formattedParts.unshift(part);
}
return formattedParts.join(sep);
}
return "0";
}
function formatAverageAge(age, fractionDigits = 1) {
if (age) {
return age.toFixed(fractionDigits);
}
return "0.0";
}
function formatFileSize(b) {
const s = 1024;
let u = 0;
while (b >= s || -b >= s) {
b /= s;
u++;
}
return (u ? b.toFixed(1) + " " : b) + " KMGTPEZY"[u] + "B";
}
function getClubPlayers(doc, currency) {
const players = [];
const playerNodes = doc.querySelectorAll("table#playerAltViewTable tr");
for (const playerNode of playerNodes) {
const age = playerNode.querySelector("td:nth-child(5)")?.innerText.replace(/\s/g, "");
if (age) {
const value = playerNode.querySelector("td:nth-child(3)")?.innerText.replaceAll(currency, "").replace(/\s/g, "");
const shirtNumber = playerNode.querySelector("td:nth-child(0)")?.innerText.replace(/\s/g, "");
const pid = playerNode.querySelector("a")?.href;
players.push({
shirtNumber,
age: parseInt(age, 10),
value: parseInt(value, 10),
id: extractPlayerID(pid),
});
}
}
return players;
}
function getNumberOfFlags(infoTable) {
const images = infoTable.getElementsByTagName("img");
return images ? [...images].filter((img) => img.src.indexOf("/flags/") > -1).length : 0;
}
function isDomesticPlayer(infoTable) {
return getNumberOfFlags(infoTable) === 1;
}
function getNationalPlayers(doc, currency) {
const players = [];
const playerNodes = doc.querySelectorAll("div.playerContainer");
for (const playerNode of playerNodes) {
const id = extractPlayerID(playerNode.querySelector("h2 a")?.href);
const infoTable = playerNode.querySelector("div.dg_playerview_info table");
const age = infoTable.querySelector("tbody tr:nth-child(1) td strong").innerText;
const selector = isDomesticPlayer(infoTable) ? "tbody tr:nth-child(5) td span" : "tbody tr:nth-child(6) td span";
const value = infoTable.querySelector(selector)?.innerText.replaceAll(currency, "").replace(/\s/g, "");
players.push({
age: parseInt(age, 10),
value: parseInt(value, 10),
id,
});
}
return players;
}
function getNumberOfPlayers(players, ageLow = 0, ageHigh = 99) {
return players.filter((player) => player.age <= ageHigh && player.age >= ageLow).length;
}
function filterPlayers(players, count = 0, ageLow = 0, ageHigh = 99) {
if (players.length === 0) {
return { values: 0, avgAge: 0.0 };
}
const n = count === 0 ? players.length : count;
const filtered = players
.filter((player) => player.age <= ageHigh && player.age >= ageLow)
.sort((a, b) => b.value - a.value)
.slice(0, n);
if (filtered.length === 0) {
return { values: 0, avgAge: 0.0 };
}
const values = filtered.map((player) => player.value).reduce((a, b) => a + b, 0);
const avgAge = filtered.map((player) => player.age).reduce((a, b) => a + b, 0) / filtered.length;
return { values, avgAge };
}
async function getNationalPlayersAndCurrency(tid, sport) {
const url = `https://${location.hostname}/ajax.php?p=nationalTeams&sub=players&ntid=${tid}&sport=${sport}`;
let players = [];
let currency = '';
await fetch(url)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
currency = getNationalCurrency(doc);
players = getNationalPlayers(doc, currency);
})
.catch((error) => {
console.warn(error);
});
return { players, currency };
}
async function getClubPlayersAndCurrency(tid) {
const url = getSquadSummaryLink(tid);
let players = [];
let currency = '';
await fetch(url)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
currency = getClubCurrency(doc);
players = getClubPlayers(doc, currency);
})
.catch((error) => {
console.warn(error);
});
return { players, currency };
}
async function getPlayersAndCurrency(tid, sport) {
const url = getSquadSummaryLink(tid);
const isNational = await fetch(url, { method: "HEAD" })
.then((resp) => (resp.url.search("p=national_teams") > -1));
return isNational ? await getNationalPlayersAndCurrency(tid, sport) : await getClubPlayersAndCurrency(tid);
}
function getClubTopPlyers(doc) {
const currency = getClubCurrency(doc);
const players = getClubPlayers(doc, currency);
const sport = getSportType(doc);
const count = sport === "soccer" ? 11 : 21;
return players ? filterPlayers(players, count) : { values: 0, avgAge: 0 };
}
async function fetchPlayersProfileSummary(teamId) {
const url = `https://${location.hostname}/?p=players&tid=${teamId}`;
return await fetch(url)
.then((resp) => {
return resp.text();
}).then((content) => {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const players = doc.getElementById("players_container")?.querySelectorAll("div.playerContainer");
const info = {};
for (const player of players) {
const playerId = player.querySelector("span.player_id_span")?.innerText;
const inMarket = [...player.querySelectorAll("a")].find((el) => el.href?.indexOf("p=transfer&sub") > -1);
info[playerId] = {
detail: player,
shared: !!player.querySelector("i.special_player.fa-share-alt"),
market: !!inMarket,
marketLink: inMarket?.href,
}
}
return info;
}).catch((error) => {
console.warn(error);
return null;
});
}
function squadGetPlayersInfo(players, sport = "soccer") {
if (!players) {
return [];
}
const rows = [];
if (sport === "hockey") {
{
const all = filterPlayers(players);
const top21 = filterPlayers(players, 21);
rows.push({
title: "All",
count: players.length,
all: all.values,
allAge: all.avgAge,
top21: top21.values,
top21Age: top21.avgAge,
});
}
{
const all = filterPlayers(players, 0, 0, 23);
const top21 = filterPlayers(players, 21, 0, 23);
rows.push({
title: "U23",
count: getNumberOfPlayers(players, 0, 23),
all: all.values,
allAge: all.avgAge,
top21: top21.values,
top21Age: top21.avgAge,
});
}
{
const all = filterPlayers(players, 0, 0, 21);
const top21 = filterPlayers(players, 21, 0, 21);
rows.push({
title: "U21",
count: getNumberOfPlayers(players, 0, 21),
all: all.values,
allAge: all.avgAge,
top21: top21.values,
top21Age: top21.avgAge,
});
}
{
const all = filterPlayers(players, 0, 0, 18);
const top21 = filterPlayers(players, 21, 0, 18);
rows.push({
title: "U18",
count: getNumberOfPlayers(players, 0, 18),
all: all.values,
allAge: all.avgAge,
top21: top21.values,
top21Age: top21.avgAge,
});
}
} else {
{
const all = filterPlayers(players);
const top16 = filterPlayers(players, 16);
const top11 = filterPlayers(players, 11);
rows.push({
title: "All",
count: players.length,
all: all.values,
allAge: all.avgAge,
top16: top16.values,
top16Age: top16.avgAge,
top11: top11.values,
top11Age: top11.avgAge,
});
}
{
const all = filterPlayers(players, 0, 0, 23);
const top16 = filterPlayers(players, 16, 0, 23);
const top11 = filterPlayers(players, 11, 0, 23);
rows.push({
title: "U23",
count: getNumberOfPlayers(players, 0, 23),
all: all.values,
allAge: all.avgAge,
top16: top16.values,
top16Age: top16.avgAge,
top11: top11.values,
top11Age: top11.avgAge,
});
}
{
const all = filterPlayers(players, 0, 0, 21);
const top16 = filterPlayers(players, 16, 0, 21);
const top11 = filterPlayers(players, 11, 0, 21);
rows.push({
title: "U21",
count: getNumberOfPlayers(players, 0, 21),
all: all.values,
allAge: all.avgAge,
top16: top16.values,
top16Age: top16.avgAge,
top11: top11.values,
top11Age: top11.avgAge,
});
}
{
const all = filterPlayers(players, 0, 0, 18);
const top16 = filterPlayers(players, 16, 0, 18);
const top11 = filterPlayers(players, 11, 0, 18);
rows.push({
title: "U18",
count: getNumberOfPlayers(players, 0, 18),
all: all.values,
allAge: all.avgAge,
top16: top16.values,
top16Age: top16.avgAge,
top11: top11.values,
top11Age: top11.avgAge,
});
}
}
return rows;
}
async function fetchDocument(url) {
return await fetch(url)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
return parser.parseFromString(content, "text/html");
})
.catch((error) => {
console.warn(error);
return null;
});
}
async function fetchJson(url) {
return await fetch(url)
.then((resp) => resp.json())
.catch((error) => {
console.warn(error);
return null;
});
}
async function fetchPlayerProfileDocument(playerId) {
const url = `https://${location.hostname}/?p=players&pid=${playerId}`;
return await fetchDocument(url);
}
async function fetchTransferMonitorData(sport = "soccer") {
const url = `https://${location.hostname}/ajax.php?p=transfer&sub=your-bids&sport=${sport}`;
return await fetchJson(url);
}
async function fetchSquadSummaryDocument(teamId) {
const url = getSquadSummaryLink(teamId);
return await fetchDocument(url);
}
/* *********************** DOM Utils ********************************** */
function makeElementDraggable(element, dragHandleElement, dragEndCallback = null) {
let deltaX = 0, deltaY = 0, lastX = 0, lastY = 0;
dragHandleElement.style.cursor = "move";
dragHandleElement.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
lastX = e.clientX;
lastY = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
deltaX = lastX - e.clientX;
deltaY = lastY - e.clientY;
lastX = e.clientX;
lastY = e.clientY;
// set the element's new position:
let newTop = Math.max(0, element.offsetTop - deltaY);
let newLeft = Math.max(0, element.offsetLeft - deltaX);
const { right, bottom, width, height } = element.getBoundingClientRect();
if (right > window.innerWidth) {
newLeft = window.innerWidth - width;
}
if (bottom > window.innerHeight) {
newTop = window.innerHeight - height;
}
element.style.top = newTop + "px";
element.style.left = newLeft + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
if (dragEndCallback) {
dragEndCallback();
}
}
}
function getMzButtonColorClass(color) {
if (color) {
if (color === "red") {
return "button_red";
} else if (color === "blue") {
return "button_blue";
} else if (color === "grey") {
return "buttondiv_disabled";
}
}
// green or other values
return "button_account";
}
function createMzStyledButton(title, color = "", floatDirection = null) {
const div = document.createElement("div");
div.style.margin = "0.3rem";
if (floatDirection) {
// floatDirection: floatRight, floatLeft
div.classList.add(floatDirection);
}
const button = document.createElement("div");
button.classList.add("mzbtn", "buttondiv", getMzButtonColorClass(color));
button.innerHTML = `<span class="buttonClassMiddle"><span style="white-space: nowrap">${title}</span></span><span class="buttonClassRight"> </span>`;
div.appendChild(button);
return div;
}
function createMzStyledTitle(text = "") {
const div = document.createElement("div");
div.classList.add("win_bg");
const title = document.createElement("h3");
title.innerText = text;
title.style.margin = "0.4rem auto";
title.style.padding = "0 0.6rem";
div.appendChild(title);
return div;
}
function createSuggestionList(items) {
const datalist = document.createElement("datalist");
datalist.id = generateUuidV4();
for (const item of items) {
const option = document.createElement("option");
option.value = item.toString();
datalist.appendChild(option);
}
return datalist;
}
function createMenuTextInput(title = "input", placeholder = "example", datalistId = "") {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container-row");
div.style.justifyItems = "space-between";
div.innerHTML = `
<label style="margin: 0.5rem; font-weight: bold;">${title}: </label>
<input list="${datalistId}" style="margin: 0.5rem;" type="text" value="" placeholder="${placeholder}">
`;
return div;
}
function createSubMenuTextInput(title = "input", placeholder = "example", initialValue = 0, style = {
margin: "0.1rem 2.2rem",
inputSize: "5px"
}) {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container-row");
div.style.justifyItems = "space-between";
div.style.margin = style?.margin ?? "0.1rem 2.2rem";
div.innerHTML = `
<label style="margin-left: 0.5rem;">${title}: </label>
<input style="margin-left: 0.5rem;" type="text" size="${style?.inputSize ?? "5px"}" placeholder="${placeholder}", value="${initialValue}">
`;
return div;
}
function createMenuCheckBox(
label,
initialValue = true,
style = {
alignSelf: "flex-start",
margin: "0.3rem 0.7rem",
}
) {
const id = generateUuidV4();
const div = document.createElement("div");
div.style.alignSelf = style?.alignSelf ?? "flex-start";
div.style.margin = style?.margin ?? "0.3rem 0.7rem";
const checkbox = document.createElement("input");
checkbox.id = id;
checkbox.type = "checkbox";
checkbox.checked = initialValue;
const labelElement = document.createElement("label");
labelElement.htmlFor = id;
labelElement.innerHTML = label;
div.appendChild(checkbox);
div.appendChild(labelElement);
return div;
}
function createMenuGroup(title = "") {
const group = document.createElement("div");
group.classList.add("mazyar-flex-container");
group.style.alignSelf = "flex-start";
group.style.alignItems = "flex-start";
group.style.margin = "0.2rem 0.6rem";
const header = document.createElement("h4");
header.innerText = title;
header.style.margin = "0.3rem 0rem";
group.appendChild(header);
return group;
}
function appendOptionList(parent, options, selected) {
// options = a object full of 'key: {value, label}'
for (const key in options) {
if (options.hasOwnProperty(key)) {
const child = document.createElement("option");
child.value = options[key].value;
child.innerText = options[key].label;
if (child.value === selected) {
child.selected = true;
}
parent.appendChild(child);
}
}
}
function createMenuDropDown(label, options, initialValue) {
// options = a object full of 'key: {value, label}'
// initialValue = one of the options.value
const div = document.createElement("div");
const labelElement = document.createElement("label");
const dropdown = document.createElement("select");
div.style.alignSelf = "flex-start";
div.style.margin = "0.3rem 0.7rem";
labelElement.innerText = label;
labelElement.style.paddingRight = "0.5rem";
appendOptionList(dropdown, options, initialValue);
div.appendChild(labelElement);
div.appendChild(dropdown);
return div;
}
function createDeleteIcon(title) {
const icon = document.createElement("span");
icon.classList.add("mazyar-icon-delete");
if (title) {
icon.title = title;
}
return icon;
}
function createAddToDeadlineIcon(title, color) {
const icon = createLegalIcon();
icon.style.verticalAlign = "unset";
icon.style.borderRadius = "50%";
icon.style.border = "solid 1px";
icon.style.padding = "3px";
const span = document.createElement("span");
span.style.color = color;
span.classList.add("floatRight");
if (title) {
span.title = title;
}
span.appendChild(icon);
return span;
}
function createDeleteButtonWithTrashIcon(title = "Delete") {
const icon = createDeleteIcon();
const text = document.createElement("span");
text.innerText = title;
const button = document.createElement("button");
button.classList.add("mazyar-flex-container-row", "mazyar-button");
button.style.margin = "0.6rem 0 0";
button.appendChild(icon);
button.appendChild(text);
return button;
}
function filtersViewCreateTableHeader() {
const tr = document.createElement("tr");
const name = document.createElement("th");
name.classList.add("header");
name.innerText = "Name";
name.title = "Filter' name";
name.style.textAlign = "left";
name.style.textDecoration = "none";
name.style.width = "11rem";
const totalHits = document.createElement("th");
totalHits.classList.add("header");
totalHits.innerText = "Total";
totalHits.title = "Total hits founds for this filter";
totalHits.style.textAlign = "center";
totalHits.style.textDecoration = "none";
const scoutHits = document.createElement("th");
scoutHits.classList.add("header");
scoutHits.innerText = "Scout";
scoutHits.title = "Hits found after applying scout filters";
scoutHits.style.textAlign = "center";
scoutHits.style.textDecoration = "none";
const tools = document.createElement("th");
tools.classList.add("header");
tools.innerHTML = " ";
tools.style.textAlign = "center";
tools.style.textDecoration = "none";
tr.appendChild(tools);
tr.appendChild(name);
tr.appendChild(totalHits);
tr.appendChild(scoutHits);
const thead = document.createElement("thead");
thead.appendChild(tr);
return thead;
}
function startSpinning(element) {
element.classList.add("fa-spin");
}
function stopSpinning(element) {
element.classList.remove("fa-spin");
}
function filtersViewCreateTableBody(filters = []) {
const tbody = document.createElement("tbody");
for (const filter of filters) {
const tr = document.createElement("tr");
const filterName = filter.name.length > 32 ? filter.name.substring(0, 29) + "..." : filter.name;
const name = document.createElement("td");
name.innerHTML = `<a target="_blank" href="https://${location.hostname}/?p=transfer&mzy_filter_name=${filter.name}">${filterName}</a>`;
name.title = filter.name.length > 32 ? `Filter's full name: ${filter.name}` : "Filter's name";
const total = document.createElement("td");
const scout = document.createElement("td");
total.style.textAlign = "center";
scout.style.textAlign = "center";
mazyar.getFilterHitsFromIndexedDb(filter.id).then(({ totalHits, scoutHits }) => {
total.innerText = filterHitsIsValid(totalHits) ? totalHits.toString() : "n/a";
if (filter.scout) {
scout.innerHTML = `<a style="cursor: pointer;">${filterHitsIsValid(scoutHits) ? scoutHits.toString() : "n/a"}</a>`;
} else {
scout.innerHTML = "X";
}
});
if (filter.scout) {
scout.onclick = () => {
const info = { name: filter.name, scout: filter.scout, count: scout.innerText };
mazyar.displayFilterResults(filter.id, info);
};
}
const del = createDeleteIcon("Delete the filter permanently.");
del.style.verticalAlign = "bottom";
del.onclick = () => {
mazyar.deleteFilter(filter.id);
tbody.removeChild(tr);
if (tbody.children.length === 0) {
tbody.parentNode.dispatchEvent(new Event("destroy"));
}
};
const refresh = createRefreshIcon("Refresh filter hits.");
refresh.style.fontSize = "1.1rem";
refresh.onclick = async () => {
startSpinning(refresh);
total.replaceChildren(createLoadingIcon());
scout.replaceChildren(createLoadingIcon());
const { totalHits, scoutHits } = await mazyar.refreshFilterHits(filter.id);
total.innerText = filterHitsIsValid(totalHits) ? totalHits.toString() : "n/a";
if (filter.scout) {
scout.innerHTML = `<a style="cursor: pointer;">${filterHitsIsValid(scoutHits) ? scoutHits.toString() : "n/a"}</a>`;
} else {
scout.innerHTML = "X";
}
stopSpinning(refresh);
};
const tools = document.createElement("td");
tools.appendChild(del);
tools.appendChild(refresh);
tr.appendChild(tools);
tr.appendChild(name);
tr.appendChild(total);
tr.appendChild(scout);
tbody.appendChild(tr);
}
return tbody;
}
function filtersViewCreateTable(filters) {
const table = document.createElement("table");
const thead = filtersViewCreateTableHeader();
const tbody = filtersViewCreateTableBody(filters);
table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker");
table.style.margin = "0.5rem";
table.appendChild(thead);
table.appendChild(tbody);
return table;
}
function createIconFromFontAwesomeClass(classes = [], title = "") {
const icon = document.createElement("i");
icon.classList.add(...classes);
icon.setAttribute("aria-hidden", "true");
icon.style.cursor = "pointer";
if (title) {
icon.title = title;
}
return icon;
}
function createMoveIcon(title) {
return createIconFromFontAwesomeClass(["fa-solid", "fa-up-down-left-right"], title);
}
function createSharedIcon(title) {
return createIconFromFontAwesomeClass(["fa", "fa-share-alt"], title);
}
function createMarketIcon(title) {
return createIconFromFontAwesomeClass(["fa", "fa-legal"], title);
}
function createCogIcon(title = "") {
return createIconFromFontAwesomeClass(["fa", "fa-cog"], title);
}
function createCommentIcon(title = "") {
return createIconFromFontAwesomeClass(["fa-solid", "fa-comment"], title);
}
function createSearchIcon(title = "") {
return createIconFromFontAwesomeClass(["fa", "fa-search"], title);
}
function createNoteIcon(title = "") {
return createIconFromFontAwesomeClass(["fa-solid", "fa-note-sticky"], title);
}
function createRefreshIcon(title = "") {
return createIconFromFontAwesomeClass(["fa", "fa-refresh"], title);
}
function createLegalIcon(title = "") {
return createIconFromFontAwesomeClass(["fa", "fa-legal"], title);
}
function createTrashIcon(title = "") {
return createIconFromFontAwesomeClass(["fas", "fa-trash"], title);
}
function createLoadingIcon(title = "") {
const icon = createIconFromFontAwesomeClass(["fa", "fa-spinner", "fa-spin"], title);
icon.style.cursor = "unset";
return icon;
}
function createLoadingIcon2(title = "") {
const icon = createIconFromFontAwesomeClass(["fa-solid", "fa-loader", "fa-pulse", "fa-fw"], title);
icon.style.cursor = "unset";
return icon;
}
function createToolbar() {
const toolbar = document.createElement("div");
const logo = document.createElement("span");
const menu = createCogIcon("Settings");
const note = createNoteIcon("Notebook");
const separator = document.createElement("span");
const transfer = document.createElement("div");
const transferIcon = createSearchIcon("Transfer");
const transferCount = document.createElement("span");
toolbar.classList.add("mazyar-flex-container");
toolbar.style.position = "fixed";
toolbar.style.zIndex = "9998";
toolbar.style.top = "40%";
toolbar.style.right = "5px";
toolbar.style.background = "black";
toolbar.style.color = "white";
toolbar.style.textAlign = "center";
logo.innerText = "MZY";
logo.style.fontSize = "0.6rem";
logo.style.fontWeight = "bold";
logo.style.margin = "2px";
logo.style.padding = "1px";
menu.style.fontSize = "large";
transferIcon.style.fontSize = "large";
note.style.fontSize = "large";
note.style.marginTop = "5px";
separator.innerText = "-------";
separator.style.textAlign = "center";
separator.style.fontSize = "0.6rem";
separator.style.fontWeight = "bolder";
separator.style.margin = "0";
separator.style.padding = "0";
transfer.classList.add("mazyar-flex-container");
transfer.style.cursor = "pointer";
transferCount.id = "mazyar-transfer-filter-hits";
transferCount.innerText = "0";
transferCount.style.fontSize = "0.6rem";
transferCount.style.fontWeight = "bold";
transferCount.style.margin = "1px 0";
transferCount.style.padding = "1px";
transfer.appendChild(transferIcon);
transfer.appendChild(transferCount);
toolbar.appendChild(logo);
toolbar.appendChild(menu);
toolbar.appendChild(note);
toolbar.appendChild(separator);
toolbar.appendChild(transfer);
return { toolbar, menu, transfer, note };
}
function createDeadlineIndicator() {
const div = document.createElement("div");
const transferIcon = createLegalIcon();
div.classList.add("mazyar-flex-container");
div.style.position = "fixed";
div.style.zIndex = "9997";
div.style.top = "48%";
div.style.right = "35px";
div.style.color = "white";
div.style.textAlign = "center";
transferIcon.style.fontSize = "2rem";
div.appendChild(transferIcon);
return div;
}
/* *********************** Squad - Icons (Shared Skills & Transfer) ********************************** */
function squadAddIconsHeaderToSummaryTable(table, ownView = false) {
if (ownView) {
const age = table.querySelector("thead tr th:nth-child(5) a");
age.innerText = age.innerText[0];
const born = table.querySelector("thead tr th:nth-child(6) a");
born.innerText = born.innerText[0];
}
const th = document.createElement("th");
th.style.width = "0px";
const target = table.querySelector("thead tr th:nth-child(2)");
target.parentNode.insertBefore(th, target);
}
function squadAddIconsBodyToSummaryTable(player, info) {
const td = document.createElement("td");
if (info.shared) {
const icon = createSharedIcon("Click to see player profile.");
icon.style.fontSize = "13px";
icon.classList.add("special_player");
icon.addEventListener("click", () => {
mazyar.showPlayerInModal(info.detail);
})
td.appendChild(icon);
}
if (info.market) {
const icon = createMarketIcon("Player is in Transfer Market.");
icon.style.fontSize = "13px";
icon.classList.add("special_player");
const link = document.createElement("a");
link.href = info.marketLink;
link.target = "_blank";
link.appendChild(icon);
td.appendChild(link);
}
const target = player.querySelector("td:nth-child(2)");
target.parentNode.insertBefore(td, target);
}
async function squadInjectIconsToSummaryTable(table) {
const players = table?.querySelectorAll("tbody tr");
const ownView = players?.[0]?.children?.length > 6;
const text = document.createElement("div");
text.innerHTML = `<b>MZY:</b> fetching players' info ...`;
table.parentNode.insertBefore(text, table);
const teamId = extractTeamId(document.baseURI);
const playersInfo = await fetchPlayersProfileSummary(teamId);
if (!playersInfo) {
text.innerHTML = `<b>MZY:</b> fetching players' info ...<span style="color: red;"> failed</span>.`;
return;
}
text.innerHTML = `<b>MZY:</b> fetching players' info ...<span style="color: green;"> done</span>.`;
squadAddIconsHeaderToSummaryTable(table, ownView);
for (const player of players) {
const name = player.querySelector("a");
const playerId = extractPlayerID(name?.href);
squadAddIconsBodyToSummaryTable(player, playersInfo[playerId]);
}
}
/* *********************** Squad - Residency ********************************** */
function squadExtractResidencyDaysAndPrice(doc = document) {
if (!doc) {
return { days: 0, price: -1 };
}
const transfers = doc?.querySelector("div.baz > div > div.win_back > table.hitlist");
const history = transfers?.querySelector("tbody");
if (history?.children.length > 1) {
const arrived = history?.lastChild?.querySelector("td")?.innerText;
const days = Math.floor((new Date() - parseMzDate(arrived)) / 86_400_000);
const price = history.lastChild?.querySelector("td:last-child")?.innerText;
const currency = transfers?.querySelector("thead tr td:last-child")?.innerText?.match(/.*\((.*)\)/)?.[1];
return { days, price: price + ' ' + currency };
}
return { days: -1, price: -1 };
}
function squadAddDaysAtThisClubToPlayerProfile() {
if (mazyar.isDaysAtThisClubEnabledForPlayerProfiles()) {
const { days } = squadExtractResidencyDaysAndPrice(document);
const daysDiv = document.createElement("div");
if (days >= 0) {
const text = days === 0 ? 'N/A' : `≤ ${days}`;
daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`;
daysDiv.classList.add("mazyar-days-at-this-club");
} else if (mazyar.isDaysAtThisClubEnabledForOneClubPlayers()) {
const text = 'Entire Career';
daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`;
daysDiv.classList.add("mazyar-days-at-this-club-entire");
}
const profile = document.querySelector("div.playerContainer");
profile?.appendChild(daysDiv);
}
}
/* *********************** Squad - Total Balls ********************************** */
function squadSortPlayersByTotalSkillBalls(th) {
const table = document.getElementById("playerAltViewTable");
const players = table?.querySelectorAll("tbody tr");
if (table.ascending) {
table.ascending = false;
const icon = th.querySelector("i");
icon.classList.value = '';
icon.classList.add("fa-solid", "fa-sort-down");
icon.style.color = "#000";
const rows = [...players].sort((a, b) => a.skillBalls - b.skillBalls);
table.querySelector("tbody").replaceChildren(...rows);
} else {
table.ascending = true;
const icon = th.querySelector("i");
icon.classList.value = '';
icon.classList.add("fa-solid", "fa-sort-up");
icon.style.color = "#000";
const rows = [...players].sort((a, b) => b.skillBalls - a.skillBalls);
table.querySelector("tbody").replaceChildren(...rows);
}
}
function squadAddTotalSkillBallsHeaderToSummaryTable(table) {
const th = document.createElement("th");
th.title = "Total Skill Balls";
th.innerHTML = `<i aria-hidden="true" style="display: inline; font-size: 11px; color: #555;" class="fa-solid fa-sort"></i><span style="font-size: 11px;">T</span>`;
th.style.textDecoration = "none";
th.style.textAlign = "right";
const target = table.querySelector("thead tr th:last-child");
target.parentNode.insertBefore(th, target);
th.addEventListener("click", () => {
squadSortPlayersByTotalSkillBalls(th);
});
}
function squadAddPlayerTotalSkillBallsToSummaryTable(player, sport) {
let sum = 0;
const start = sport === "soccer" ? 6 : 7;
const end = 17;
for (const skill of [...player.children].slice(start, end)) {
sum += Number(skill.innerText);
}
const td = document.createElement("td");
td.innerText = sum.toString();
td.style.fontWeight = `${Math.ceil(900 * sum / 110)}`
td.style.textAlign = "right";
const target = player.querySelector("td:last-child");
target.parentNode.insertBefore(td, target);
player.skillBalls = sum;
}
function squadAddTotalSkillBallsToSummaryTable(table) {
const sport = getSportType();
const players = table?.querySelectorAll("tbody tr");
if (players?.[0]?.children?.length > 6) {
squadAddTotalSkillBallsHeaderToSummaryTable(table);
for (const player of players) {
squadAddPlayerTotalSkillBallsToSummaryTable(player, sport);
}
}
}
/* *********************** Squad - Top Players ********************************** */
function squadCreateCompactElement(title, value) {
const dd = document.createElement("dd");
dd.innerHTML = `<span class="listHeadColor">${title}</span><span class="clippable">${value}</span>`;
return dd;
}
function squadSummaryCreateCompactRow(row, currency = "USD", sport = "soccer") {
const dl = document.createElement("dl");
dl.classList.add("hitlist-compact-list", "columns");
dl.appendChild(squadCreateCompactElement("Count", row.count));
dl.appendChild(squadCreateCompactElement("Total", `${formatBigNumber(row.all)} ${currency}`));
if (sport == "soccer") {
dl.appendChild(squadCreateCompactElement("Top 16", `${formatBigNumber(row.top16)} ${currency}`));
dl.appendChild(squadCreateCompactElement("Top 11", `${formatBigNumber(row.top11)} ${currency}`));
} else {
dl.appendChild(squadCreateCompactElement("Top 21", `${formatBigNumber(row.top21)} ${currency}`));
}
return dl;
}
function squadCreateTopPlayersTable(rows, currency = "USD", sport = "soccer") {
const thead = document.createElement("thead");
const tr = document.createElement("tr");
const titleHeader = document.createElement("th");
titleHeader.classList.add("header");
titleHeader.innerText = "Group";
titleHeader.style.textAlign = "center";
titleHeader.style.textDecoration = "none";
const countHeader = document.createElement("th");
countHeader.classList.add("header");
countHeader.innerText = "Count";
countHeader.title = "Number of Players";
countHeader.style.textAlign = "center";
countHeader.style.textDecoration = "none";
const totalHeader = document.createElement("th");
totalHeader.classList.add("header");
totalHeader.innerHTML = "Total";
totalHeader.title = "Total Value of Players";
totalHeader.style.textAlign = "center";
totalHeader.style.textDecoration = "none";
tr.appendChild(titleHeader);
tr.appendChild(countHeader);
tr.appendChild(totalHeader);
if (sport === "soccer") {
const top16Header = document.createElement("th");
top16Header.classList.add("header");
top16Header.innerHTML = "Top 16";
top16Header.title = "Value of Top 16 Players";
top16Header.style.textAlign = "center";
top16Header.style.textDecoration = "none";
const top11Header = document.createElement("th");
top11Header.classList.add("header");
top11Header.innerHTML = "Top 11";
top11Header.title = "Value of Top 11 Players";
top11Header.style.textAlign = "center";
top11Header.style.textDecoration = "none";
tr.appendChild(top16Header);
tr.appendChild(top11Header);
} else {
const top21Header = document.createElement("th");
top21Header.classList.add("header");
top21Header.innerHTML = "Top 21";
top21Header.title = "Value of Top 21 Players";
top21Header.style.textAlign = "center";
top21Header.style.textDecoration = "none";
tr.appendChild(top21Header);
}
thead.appendChild(tr);
const tbody = document.createElement("tbody");
for (const row of rows) {
const title = document.createElement("td");
title.innerHTML = `${row.title}`;
title.classList.add("hitlist-compact-list-column");
const compact = squadSummaryCreateCompactRow(row, currency, sport);
title.appendChild(compact);
const count = document.createElement("td");
count.innerHTML = `${row.count}`;
count.style.textAlign = "center";
const all = document.createElement("td");
all.innerText = `${formatBigNumber(row.all)} ${currency}`;
all.style.textAlign = "end";
const tr = document.createElement("tr");
tr.appendChild(title);
tr.appendChild(count);
tr.appendChild(all);
tbody.appendChild(tr);
if (sport === "soccer") {
const top16 = document.createElement("td");
top16.innerText = `${formatBigNumber(row.top16)} ${currency}`;
top16.style.textAlign = "end";
const top11 = document.createElement("td");
top11.innerText = `${formatBigNumber(row.top11)} ${currency}`;
top11.style.textAlign = "end";
tr.appendChild(top16);
tr.appendChild(top11);
} else {
const top21 = document.createElement("td");
top21.innerText = `${formatBigNumber(row.top21)} ${currency}`;
top21.style.textAlign = "end";
tr.appendChild(top21);
}
}
const table = document.createElement("table");
table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker", "hitlist-compact-list-included");
table.appendChild(thead);
table.appendChild(tbody);
return table;
}
function squadInjectTopPlayersTable(table) {
const sport = getSportType(document);
const currency = getClubCurrency(document);
const players = getClubPlayers(document, currency);
const summary = squadGetPlayersInfo(players, sport);
const topPlayers = squadCreateTopPlayersTable(summary, currency, sport);
topPlayers.style.marginBottom = "10px";
const div = document.createElement("div");
div.classList.add("mazyar-flex-container");
div.appendChild(topPlayers);
table.parentNode.insertBefore(div, table);
}
function squadInjectInformation(table) {
squadAddTotalSkillBallsToSummaryTable(table);
squadInjectIconsToSummaryTable(table);
squadInjectTopPlayersTable(table);
}
function squadInjectInformationToSummary() {
const target = document.getElementById('squad_summary');
if (target) {
if (target.style.display !== 'none') {
// visiting squad summary directly
const table = document.querySelector("table#playerAltViewTable");
if (table && !table.injecting) {
table.injecting = true;
squadInjectInformation(table);
}
}
// add observer if user changes from other tabs to squad summary tab
const callback = () => {
const table = document.querySelector("table#playerAltViewTable");
if (table && !table.injecting) {
table.injecting = true;
squadInjectInformation(table);
}
};
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(target, config);
}
}
function squadInjectInformationToProfiles() {
if (document.baseURI.search("/?players&pid=") > -1) {
squadAddDaysAtThisClubToPlayerProfile();
mazyar.addPlayerComment();
} else {
const target = document.getElementById('squad_profiles');
if (target) {
if (target.style.display !== 'none') {
// visiting profiles directly
const container = document.getElementById('players_container');
if (container && !container.injecting) {
container.injecting = true;
mazyar.squadAddDaysAtThisClubToAllPlayers(container);
mazyar.addPlayerComment();
}
}
// add observer if user changes from other tabs to squad profiles tab
const callback = () => {
const container = document.getElementById('players_container');
if (container && !container.injecting) {
container.injecting = true;
mazyar.squadAddDaysAtThisClubToAllPlayers(container);
mazyar.addPlayerComment();
}
};
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(target, config);
}
}
}
/* *********************** Rankings ********************************** */
function createSortableHeaderCell(cellText = "", options = { title: "", textAlign: "", marginBetween: "" }) {
const cell = document.createElement("th");
const sortIcon = document.createElement("i");
const text = document.createElement("span");
cell.appendChild(sortIcon);
cell.appendChild(text);
cell.style.textDecoration = "none";
sortIcon.style.display = "inline";
sortIcon.style.fontSize = "11px";
sortIcon.style.color = "#555";
sortIcon.classList.add("fa-solid", "fa-sort", "mazyar-sort-icon");
sortIcon.setAttribute("aria-hidden", "true");
text.style.fontSize = "11px";
text.innerText = cellText;
if (options?.marginBetween) {
text.style.marginLeft = options.marginBetween;
}
if (options?.title) {
cell.title = options.title;
}
if (options?.textAlign) {
cell.style.textAlign = options.textAlign;
}
return cell;
}
function resetMazyarSortIcons(table) {
const sortIcons = table.querySelectorAll("thead th i.mazyar-sort-icon");
for (const icon of sortIcons) {
icon.classList.value = '';
icon.classList.add("fa-solid", "fa-sort", "mazyar-sort-icon");
}
}
function rankingSortTeamsByKey(table, th, key) {
resetMazyarSortIcons(table);
const teams = table?.querySelectorAll("tbody tr");
if (table.ascending) {
table.ascending = false;
const icon = th.querySelector("i");
icon.classList.value = '';
icon.classList.add("fa-solid", "fa-sort-down", "mazyar-sort-icon");
icon.style.color = "#000";
const rows = [...teams].sort((a, b) => a[key] - b[key]);
table.querySelector("tbody").replaceChildren(...rows);
} else {
table.ascending = true;
const icon = th.querySelector("i");
icon.classList.value = '';
icon.classList.add("fa-solid", "fa-sort-up", "mazyar-sort-icon");
icon.style.color = "#000";
const rows = [...teams].sort((a, b) => b[key] - a[key]);
table.querySelector("tbody").replaceChildren(...rows);
}
}
function rankingAddSquadHeaders(table) {
const header = table.querySelector("thead tr");
const value = createSortableHeaderCell("Value", { textAlign: "center", marginBetween: "3px" });
const age = createSortableHeaderCell("Age", { textAlign: "center", marginBetween: "2px" });
header.appendChild(value);
header.appendChild(age);
value.addEventListener("click", () => {
rankingSortTeamsByKey(table, value, "topPlayersValue");
});
age.addEventListener("click", () => {
rankingSortTeamsByKey(table, age, "topPlayersAge");
});
}
async function rankingInjectTeamInfo(team) {
const value = document.createElement("td");
value.style.whiteSpace = "nowrap";
value.style.textAlign = "center";
value.replaceChildren(createLoadingIcon2());
const age = document.createElement("td");
age.style.whiteSpace = "nowrap";
age.style.textAlign = "center";
age.replaceChildren(createLoadingIcon2());
team.appendChild(value);
team.appendChild(age);
team.topPlayersValue = 0;
team.topPlayersAge = 0;
const link = team.querySelector("td:nth-child(4) a");
const teamId = extractTeamId(link?.href);
const doc = await fetchSquadSummaryDocument(teamId);
if (doc) {
const currency = getClubCurrency(doc);
({ values: team.topPlayersValue, avgAge: team.topPlayersAge } = getClubTopPlyers(doc));
value.innerHTML = `<strong>${formatBigNumber(team.topPlayersValue, ",")}</strong> ${currency}`;
age.innerHTML = `<strong>${formatAverageAge(team.topPlayersAge)}</strong>`;
}
}
async function rankingInjectSquadValue() {
const table = document.getElementById("userRankTable");
const teams = table.querySelectorAll("tbody tr");
if (teams[0]?.childElementCount > 1) {
rankingAddSquadHeaders(table);
const jobs = [];
for (const team of teams) {
jobs.push(rankingInjectTeamInfo(team));
}
await Promise.all(jobs);
}
}
/* *********************** Clash ********************************** */
async function clashFetchAndTeamLeagueAndFlag(team) {
const url = `https://${location.hostname}/?p=team&tid=${team.teamId}`;
await fetch(url)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
return parser.parseFromString(content, "text/html");
}).then((doc) => {
const leagueRow = doc.querySelector("#infoAboutTeam > dd:nth-child(6)");
const flag = leagueRow.querySelector("img");
const seriesName = leagueRow.querySelector("span:last-child");
team.querySelector("td.flag").appendChild(flag);
team.querySelector("td.league").appendChild(seriesName);
})
.catch((error) => {
console.warn(error);
});
}
async function clashFetchAndUpdateTeamsInfo(team, mobileView) {
if (!mobileView) {
clashFetchAndTeamLeagueAndFlag(team);
}
const info = {
currency: "",
averageAge: 0,
};
const successful = await fetch(team.url)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
return parser.parseFromString(content, "text/html");
}).then((doc) => {
info.currency = getClubCurrency(doc);
({ values: team.topPlayersValue, avgAge: info.averageAge } = getClubTopPlyers(doc));
return true;
})
.catch((error) => {
console.warn(error);
team.topPlayersAverageAge = 0;
return false;
});
team.querySelector("td.value").innerHTML = successful
? `${formatBigNumber(team.topPlayersValue, ",")} ${info.currency}`
: 'N/A';
if (!mobileView) {
team.querySelector("td.age").innerHTML = successful
? `<strong>${formatAverageAge(info.averageAge)}</strong>` :
'N/A';
}
return successful;
}
function clashSortTeams(teams) {
teams.sort((a, b) => b.topPlayersValue - a.topPlayersValue);
let rank = 0;
for (const team of teams) {
rank++;
team.className = rank % 2 ? "odd" : "even";
const target = team.querySelector("button.mazyar-donut.rank");
if (target) {
target.classList.remove("mazyar-loading-donut");
target.classList.add("mazyar-final-donut");
target.innerText = `${rank}`;
}
}
const newOrderWithPlayedMatches = [];
for (const row of teams) {
newOrderWithPlayedMatches.push(row);
for (const playedMatch of row.playedMatches) {
playedMatch.className = row.className;
newOrderWithPlayedMatches.push(playedMatch);
}
}
const tbody = document.querySelector("div.panel-2 table tbody");
tbody.replaceChildren(...newOrderWithPlayedMatches);
}
async function clashCalculateRankOfTeams(teams, mobileView) {
const jobs = [];
for (const team of teams) {
jobs.push(clashFetchAndUpdateTeamsInfo(team, mobileView));
}
const results = await Promise.all(jobs);
if (results.every(Boolean)) {
// data for all teams are fetched successfully
clashSortTeams(teams);
} else {
// data for some teams are not fetched
for (const team of teams) {
const target = team.querySelector("button.mazyar-donut.rank");
target.classList.remove("mazyar-loading-donut");
target.classList.add("mazyar-final-donut");
target.innerText = `-`;
}
}
}
function clashAddRankElements(team, mobileView) {
if (!mobileView) {
const league = document.createElement("td");
league.style.whiteSpace = "collapse";
league.style.width = "max-content";
league.innerText = "";
league.classList.add("league");
league.style.textAlign = "center";
team.insertBefore(league, team.firstChild);
const flag = document.createElement("td");
flag.style.width = "max-content";
flag.innerText = "";
flag.classList.add("flag");
flag.style.textAlign = "center";
team.insertBefore(flag, team.firstChild);
const age = document.createElement("td");
age.style.width = "max-content";
age.innerText = "";
age.classList.add("age");
age.style.textAlign = "center";
team.insertBefore(age, team.firstChild);
}
const value = document.createElement("td");
value.style.width = "max-content";
value.innerText = "";
value.classList.add("value");
value.style.textAlign = "right";
team.insertBefore(value, team.firstChild);
const rank = document.createElement("td");
rank.style.width = "max-content";
team.insertBefore(rank, team.firstChild);
const button = document.createElement("button");
button.innerText = "_";
button.classList.add("mazyar-donut", "mazyar-loading-donut", "rank", "fix-width");
button.title = "Click to see squad summary";
rank.appendChild(button);
button.onclick = () => {
mazyar.displaySquadSummary(team.url);
};
}
function clashInjectRanks() {
const table = document.querySelector("table.hitlist.challenges-list");
const headers = table.querySelector("thead tr");
const mobileView = !headers;
// mobile view has not headers section
if (headers) {
const league = document.createElement("th");
league.style.textAlign = "center";
league.innerText = "League";
league.style.whiteSpace = "collapse";
league.style.width = "11%";
headers.insertBefore(league, headers.firstChild);
const flag = document.createElement("th");
flag.style.textAlign = "center";
flag.innerText = "";
flag.style.width = "2%";
headers.insertBefore(flag, headers.firstChild);
const age = document.createElement("th");
age.style.textAlign = "center";
age.innerText = "Age";
age.title = "Average Age of Top Players";
age.style.width = "4%";
headers.insertBefore(age, headers.firstChild);
const values = document.createElement("th");
values.style.textAlign = "center";
values.innerText = "Values";
values.title = "Top Players Total Values";
values.style.width = "13%";
headers.insertBefore(values, headers.firstChild);
const rank = document.createElement("th");
rank.style.textAlign = "center";
rank.innerText = "Rank";
rank.title = "Team Rank in This Federation";
rank.style.width = "3%";
headers.insertBefore(rank, headers.firstChild);
}
const rows = table.querySelectorAll("tbody tr");
for (const row of rows) {
// in mobile view played challenges are also <tr> and for this rows, the team name is not a hyperlink
const name = row.querySelector("a.team-name");
if (name?.href) {
// this is info row
row.teamId = extractTeamId(name.href);
row.url = getSquadSummaryLink(row.teamId);
clashAddRankElements(row, mobileView);
row.playedMatches = [];
} else {
// this is match row (in mobile view)
// expand to match the previous row
row.querySelector("td").colSpan = mobileView ? "3" : "6";
row.previousSibling.playedMatches?.push(row);
}
}
const teams = [...rows].filter((team) => team.url?.length > 0);
clashCalculateRankOfTeams(teams, mobileView);
}
/* *********************** Federation Page ********************************** */
function federationGetUsername(node) {
return node?.querySelector("a")?.innerText;
}
async function federationUpdateMemberInfo(member, username, sport) {
let values = 0;
let currency = "";
const teamXmlUrl = `https://${location.hostname}/xml/manager_data.php?username=${username}`;
const { teamId, teamName } = await fetch(teamXmlUrl)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
return parser.parseFromString(content, "text/xml");
}).then((doc) => {
const teamId = doc.querySelector(`Team[sport="${sport}"]`).getAttribute("teamId");
const teamName = doc.querySelector(`Team[sport="${sport}"]`).getAttribute("teamName");
return { teamId, teamName };
})
.catch((error) => {
console.warn(error);
return { teamId: null, teamName: null };
});
if (teamId) {
const squadUrl = getSquadSummaryLink(teamId);
await fetch(squadUrl)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
return parser.parseFromString(content, "text/html");
}).then((doc) => {
currency = getClubCurrency(doc);
values = getClubTopPlyers(doc).values;
})
.catch((error) => {
console.warn(error);
});
}
const name = document.createElement("div");
name.style.color = "blue";
name.style.width = "100%";
name.style.marginTop = "0.5em";
name.title = teamName;
name.innerHTML = `<strong style="color:black;">Team: </strong>${teamName.length > 20 ? teamName.substring(0, 16) + " >>>" : teamName}`;
member.querySelector("td").appendChild(name);
const value = document.createElement("div");
value.style.color = "blue";
value.style.width = "100%";
value.style.marginTop = "0.5em";
value.innerHTML = `<strong style="color:black;">Top${sport === "soccer" ? 11 : 21}: </strong>${formatBigNumber(values, ",")} ${currency}`;
member.querySelector("td").appendChild(value);
const separator = document.createElement("hr");
separator.style.marginBottom = "-3px";
member.querySelector("td").appendChild(separator);
return {
member,
values,
currency
};
}
function federationGetTableHeader(target) {
const thead = target.querySelector("thead td");
return thead.innerText;
}
function federationSetTableHeader(target, text) {
const thead = target.querySelector("thead td");
thead.innerText = text;
}
async function federationSortTeamsByTopPlayers(target) {
const sport = getSportType();
const jobs = [];
const tbody = target?.querySelector(" tbody");
for (const member of tbody.children) {
const username = federationGetUsername(member);
if (username) {
jobs.push(federationUpdateMemberInfo(member, username, sport));
}
}
if (jobs.length > 0) {
const tableHeader = federationGetTableHeader(target);
let dots = 0;
const loadingInterval = setInterval(() => {
federationSetTableHeader(target, tableHeader + " " + ".".repeat(1 + (dots % 3)));
dots++;
}, 1000);
const members = await Promise.all(jobs);
clearInterval(loadingInterval);
federationSetTableHeader(target, tableHeader + " ▼");
members.sort((a, b) => b.values - a.values);
const newOrder = members.map((t) => t.member);
let rank = 0;
for (const row of newOrder) {
rank++;
row.className = rank % 2 ? "odd" : "even";
}
tbody.replaceChildren(...newOrder);
let totalValue = 0;
for (const member of members) {
totalValue += member.values;
}
const total = document.createElement("tr");
total.style.color = "blue";
total.style.textAlign = "center";
total.style.width = "100%";
total.innerHTML = `<td><strong style="color:black;">Total: </strong>${formatBigNumber(totalValue, ",")} ${members[0].currency}</td>`;
tbody.appendChild(total);
}
}
function federationFrontPage() {
const content = document.getElementById('federation-content');
if (content) {
const target = document.getElementById("federation_clash_members_list");
if (target && !target.topPlayersInjected) {
target.topPlayersInjected = true;
federationSortTeamsByTopPlayers(target);
}
const callback = () => {
const target = document.getElementById("federation_clash_members_list");
if (target && !target.topPlayersInjected) {
target.topPlayersInjected = true;
federationSortTeamsByTopPlayers(target);
}
};
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(content, config);
}
}
/* *********************** Match ********************************** */
// ---------------- Top Players -------------
function matchCreateTopPlayersHeader(sport = "soccer") {
const thead = document.createElement("thead");
const tr = document.createElement("tr");
const title = document.createElement("th");
const count = document.createElement("th");
const tops = document.createElement("th");
const age = document.createElement("th");
const playerCount = sport === "soccer" ? 11 : 21;
title.classList.add("header");
title.innerText = "Group";
title.style.textAlign = "center";
title.style.textDecoration = "none";
count.classList.add("header");
count.innerText = "Count";
count.title = "Number of Players";
count.style.textAlign = "center";
count.style.textDecoration = "none";
tops.classList.add("header");
tops.innerHTML = `Top ${playerCount}`;
tops.title = "Value of Top 11 Players";
tops.style.textAlign = "center";
tops.style.textDecoration = "none";
age.classList.add("header");
age.innerHTML = "Average Age";
age.title = `Average Age of Top ${playerCount} Players`;
age.style.textAlign = "center";
age.style.textDecoration = "none";
tr.appendChild(title);
tr.appendChild(count);
tr.appendChild(tops);
tr.appendChild(age);
thead.appendChild(tr);
return thead;
}
function matchCreateTopPlayersBody(rows, currency = "USD", sport = "soccer") {
const tbody = document.createElement("tbody");
for (const row of rows) {
const tr = document.createElement("tr");
const title = document.createElement("td");
const count = document.createElement("td");
const top = document.createElement("td");
const age = document.createElement("td");
title.innerHTML = `${row.title}`;
title.classList.add("hitlist-compact-list-column");
count.innerHTML = `${row.count}`;
count.style.textAlign = "center";
top.style.textAlign = "end";
age.style.textAlign = "center";
if (sport === "soccer") {
top.innerText = `${formatBigNumber(row.top11)} ${currency}`;
age.innerText = `${formatAverageAge(row.top11Age)}`;
} else {
top.innerText = `${formatBigNumber(row.top21)} ${currency}`;
age.innerText = `${formatAverageAge(row.top21Age)}`;
}
tr.appendChild(title);
tr.appendChild(count);
tr.appendChild(top);
tr.appendChild(age);
tbody.appendChild(tr);
}
return tbody;
}
function matchCreateSummaryTable(rows, currency = "USD", sport = "soccer") {
const table = document.createElement("table");
const thead = matchCreateTopPlayersHeader(sport);
const tbody = matchCreateTopPlayersBody(rows, currency, sport);
table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker", "hitlist-compact-list-included");
table.style.marginBottom = "10px";
table.style.marginTop = "2em";
table.appendChild(thead);
table.appendChild(tbody);
return table;
}
function matchInjectTopPlayersValues(players, team, currency, sport) {
const summary = squadGetPlayersInfo(players, sport);
const table = matchCreateSummaryTable(summary, currency, sport);
const div = document.createElement("div");
div.classList.add("mazyar-flex-container");
div.appendChild(table);
team.appendChild(div);
}
// ---------------- Lineup --------------------
function matchGetLineupPlayers(teamNode, teamPlayers, sport) {
const lineup = [];
const teamPlayerIDs = teamPlayers.map((p) => p.id);
const lineupPlayers = teamNode.querySelectorAll("tbody tr");
for (const playerNode of lineupPlayers) {
const pos = playerNode.querySelector("td:nth-child(2)");
const order = Number(pos.querySelector("span").innerText);
const pid = extractPlayerID(playerNode.querySelector("a").href);
const playerInfo = {
id: pid,
order,
exPlayer: !teamPlayerIDs.includes(pid),
starting: sport === "hockey" || order < 12,
value: teamPlayers.find((p) => p.id === pid)?.value ?? 0,
age: teamPlayers.find((p) => p.id === pid)?.age,
};
const shirtNumber = playerNode.querySelector("td");
if (playerInfo.starting) {
shirtNumber.style.background = "lightgreen";
}
if (playerInfo.exPlayer) {
shirtNumber.style.background = "#DD0000";
}
const value = document.createElement("td");
value.innerText = `${playerInfo.value ? formatBigNumber(playerInfo.value, ",") : "N/A"}`;
playerNode.appendChild(value);
const age = document.createElement("td");
age.innerText = `${playerInfo.age ?? "N/A"}`;
playerNode.appendChild(age);
lineup.push(playerInfo);
}
return lineup;
}
function matchInjectLineupValues(players, team, currency, sport) {
const valueHeader = document.createElement("td");
valueHeader.innerText = `Value`;
valueHeader.title = `Player Value (in ${currency})`;
team.querySelector("table thead tr:nth-child(2)").appendChild(valueHeader);
team.querySelector("table tfoot tr td").colSpan += 1;
const ageHeader = document.createElement("td");
ageHeader.innerText = `Age`;
ageHeader.title = `Player Age`;
team.querySelector("table thead tr:nth-child(2)").appendChild(ageHeader);
team.querySelector("table tfoot tr td").colSpan += 1;
team.querySelector("table thead tr td").colSpan += 1;
const lineupPlayers = matchGetLineupPlayers(team, players, sport).filter((player) => player.starting && !player.exPlayer);
const value = lineupPlayers?.map((player) => player.value).reduce((a, b) => a + b, 0);
let averageAge = lineupPlayers?.map((player) => player.age).reduce((a, b) => a + b, 0);
if (averageAge) {
averageAge /= lineupPlayers.length;
}
const div = document.createElement("div");
div.innerHTML =
`${sport === "soccer" ? "Starting " : ""}Lineup` +
`<br>Value: <b>${formatBigNumber(value, ",")}</b> ${currency}` +
`<br>Average Age: <b>${formatAverageAge(averageAge)}</b>` +
`<br><br>Note: <span style="background:lightgreen">YYY</span>` +
` are ${sport === "soccer" ? "starting " : "current"} players and ` +
`<span style="background:#DD0000">NNN</span> are ex-players.` +
`<br>ex-player's value is N/A and not included in Lineup Value calculation.`;
div.style.margin = "10px";
div.style.padding = "5px";
div.style.border = "2px solid green";
div.style.borderRadius = "10px";
const place = team.querySelector("table");
team.insertBefore(div, place);
}
async function matchAddTopPlayersValue(team, sport) {
const teamLink = team.querySelector("a").href;
const tid = extractTeamId(teamLink);
const { players, currency } = await getPlayersAndCurrency(tid, sport);
matchInjectTopPlayersValues(players, team, currency, sport);
}
async function matchAddLineupValues(team, sport) {
const teamLink = team.querySelector("a").href;
const tid = extractTeamId(teamLink);
const { players, currency } = await getPlayersAndCurrency(tid, sport);
matchInjectLineupValues(players, team, currency, sport);
}
function matchInjectTeamValues() {
const sport = getSportType();
const teams = document.querySelectorAll("div.team-table");
for (const team of teams) {
if (team.querySelector("table")) {
matchAddLineupValues(team, sport);
} else {
matchAddTopPlayersValue(team, sport);
}
}
}
/* ******************** In Progress Results ************************* */
function matchGetWinner(context) {
const h = Number(context.homeGoals);
const a = Number(context.awayGoals);
if (h > a) {
return context.homeId;
} else if (h < a) {
return context.awayId;
}
return null;
}
function matchUpdateResult(result, context) {
result.innerText = context.homeGoals + " - " + context.awayGoals;
if ([context.homeId, context.awayId].includes(context.targetId)) {
const winnerId = matchGetWinner(context);
if (winnerId) {
result.style.background = winnerId === context.targetId ? "#5D7F13" : "#930000";
result.style.color = "#fff";
} else {
// equal
result.style.background = "#F2D624";
result.style.color = "#000";
}
} else {
result.style.background = "coral";
result.style.color = "#fff";
}
}
function matchGetPossiblyInProgressMatches(section) {
let days = 0;
const possiblyInProgress = [];
for (const child of [...section.children]) {
const classes = [...child.classList];
if (classes?.includes("odd")) {
if (!child.updated) {
possiblyInProgress.push(child);
}
} else if (classes?.includes("group")) {
days += 1;
if (days == 3) {
break;
}
}
}
return possiblyInProgress;
}
function matchWaitAndInjectInProgressResults(timeout = 32000) {
const step = 500;
const interval = setInterval(() => {
const matchesSection = document.getElementById("fixtures-results-list");
if (matchesSection) {
const games = matchesSection.querySelectorAll("dd.group");
if (games.length > 0) {
matchAddInProgressResults(matchesSection);
clearInterval(interval);
}
} else {
timeout -= step;
if (timeout < 0) {
clearInterval(interval);
}
}
}, step);
}
function matchAttachChangeEventToFilterForm() {
const filterForm = document.getElementById("matchListForm");
if (filterForm && !filterForm.eventAttached) {
filterForm.eventAttached = true;
// Note: when this event is fired, it is removed from the element or element is changed somehow.
// so for now we will attach it again after each change event.
filterForm.addEventListener("change", matchWaitAndInjectInProgressResults);
}
}
function matchAddInProgressResults(section) {
matchAttachChangeEventToFilterForm();
const sport = getSportType();
const matches = matchGetPossiblyInProgressMatches(section);
// when you visit someone else fixture, this return its id
let teamId = extractTeamId(document.baseURI);
for (const match of matches) {
const result = match.querySelector("dd.teams-wrapper a.score-shown");
if (!isMatchInProgress(result.innerText)) {
match.updated = true;
continue;
}
const mid = extractMatchID(result.href);
// this always returns your id
const visitorId = extractTeamId(result.href);
const url = `http://${location.hostname}/xml/match_info.php?sport_id=${sport === "soccer" ? 1 : 2}&match_id=${mid}`;
GM_xmlhttpRequest({
method: "GET",
url,
context: { match, result, teamId: teamId ?? visitorId },
onload: function (resp) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(resp.responseText, "text/xml");
if (!xmlDoc.querySelector("ManagerZone_Error")) {
const home = xmlDoc.querySelector(`Team[field="home"]`);
const away = xmlDoc.querySelector(`Team[field="away"]`);
const context = {
homeGoals: home.getAttribute("goals"),
homeId: home.getAttribute("id"),
awayGoals: away.getAttribute("goals"),
awayId: away.getAttribute("id"),
targetId: resp.context.teamId,
};
matchUpdateResult(resp.context.result, context);
}
resp.context.match.updated = true;
},
});
}
}
function matchInjectInProgressResults() {
if (mazyar.mustDisplayInProgressResults()) {
const fixturesLink = document.getElementById("matches_sub_nav")?.querySelector("div.flex-grow-0 span a");
if (fixturesLink) {
fixturesLink.addEventListener("click", matchWaitAndInjectInProgressResults);
if ([...fixturesLink.classList].includes("selected")) {
const matchesSection = document.getElementById("fixtures-results-list");
if (matchesSection) {
matchAddInProgressResults(matchesSection);
}
}
}
}
}
/* *********************** League ********************************** */
function tableGetAgeLimit(url) {
if (url.search("type=u23") > -1) {
return 23;
} else if (url.search("type=u21") > -1) {
return 21;
} else if (url.search("type=u18") > -1) {
return 18;
}
return 99;
}
function tableGetValeTitle(sport = "soccer", age = 99) {
const count = sport === "soccer" ? 11 : 21;
if ([18, 21, 23].includes(age)) {
return `Top ${count} - U${age}`;
}
return `Top ${count} - All`;
}
function tableGetAgeClass(age = 99) {
if ([18, 21, 23].includes(age)) {
return `values-u${age}`;
}
return "values-all";
}
function tableGetNextAge(age = 99) {
if (age === 99) {
return 23;
} else if (age === 23) {
return 21;
} else if (age === 21) {
return 18;
}
return 99;
}
function tableDisplayAgeInfo(team, ageLimit) {
const target = team ?? document;
target.querySelectorAll("td.mazyar-injected span")?.forEach((el) => {
el.style.display = "none";
});
const ageClass = tableGetAgeClass(ageLimit);
target.querySelectorAll("td.mazyar-injected span." + ageClass)?.forEach((el) => {
el.style.display = "unset";
});
}
function tableGetSquadSummaryUrl(team) {
const teamLink = team.querySelector("td:nth-child(2) a:last-child")?.href;
const tid = extractTeamId(teamLink);
return getSquadSummaryLink(tid);
}
function tableModifyTeamInBodyForPcView(team, url) {
const teamValue = document.createElement("td");
team.appendChild(teamValue);
const ageValue = document.createElement("td");
team.appendChild(ageValue);
teamValue.innerText = "loading...";
teamValue.classList.add("mazyar-injected", "team-value");
teamValue.title = "Click to see squad summary";
teamValue.style.textAlign = "center";
teamValue.style.whiteSpace = "nowrap";
teamValue.style.padding = "auto 3px";
teamValue.onclick = () => {
mazyar.displaySquadSummary(url);
};
ageValue.classList.add("mazyar-injected", "age-value");
ageValue.innerText = "...";
ageValue.style.textAlign = "center";
ageValue.style.whiteSpace = "nowrap";
ageValue.style.padding = "auto 3px";
}
function tableAddTeamToBodyForMobileView(team, url) {
const firstRow = team.cloneNode(true);
team.parentNode.insertBefore(firstRow, team.nextSibling);
const secondRow = document.createElement("tr");
firstRow.parentNode.insertBefore(secondRow, firstRow.nextSibling);
const value = document.createElement("td");
const age = document.createElement("td");
secondRow.appendChild(value);
secondRow.appendChild(age);
firstRow.children[0].rowSpan = "2";
firstRow.children[1].rowSpan = "2";
firstRow.classList.add("responsive-show", "mazyar-responsive-show");
secondRow.classList.add("responsive-show", "mazyar-responsive-show");
value.colSpan = "6";
value.innerText = "loading...";
value.classList.add("mazyar-injected", "team-value");
value.title = "Click to see squad summary";
value.style.textAlign = "center";
value.style.whiteSpace = "nowrap";
value.style.padding = "auto 3px";
value.style.backgroundColor = "aquamarine";
value.onclick = () => {
mazyar.displaySquadSummary(url);
};
age.colSpan = "2";
age.classList.add("mazyar-injected", "age-value");
age.innerText = "...";
age.style.textAlign = "center";
age.style.whiteSpace = "nowrap";
age.style.padding = "auto 3px";
age.style.backgroundColor = "aqua";
// modify borders to match the original view
firstRow.style.borderBottom = "inherit";
secondRow.style.borderBottom = team.style.borderBottom;
return secondRow;
}
async function tableAddTeamTopPlayersInfo(team, ageLimit, sport) {
const url = tableGetSquadSummaryUrl(team);
// for mobile
const mobileView = tableAddTeamToBodyForMobileView(team, url);
// for pc
team.classList.add("responsive-hide");
tableModifyTeamInBodyForPcView(team, url);
const pcView = team;
const tid = extractTeamId(url);
const { players, currency } = await getPlayersAndCurrency(tid, sport);
const playersOfSport = sport === "soccer" ? 11 : 21;
const all = filterPlayers(players, playersOfSport, 0, 99);
const u23 = filterPlayers(players, playersOfSport, 0, 23);
const u21 = filterPlayers(players, playersOfSport, 0, 21);
const u18 = filterPlayers(players, playersOfSport, 0, 18);
for (const view of [pcView, mobileView]) {
const valueElement = view.querySelector("td.team-value");
// prettier-ignore
valueElement.innerHTML =
`<span class="values-all" style="display:none">${formatBigNumber(all?.values)} ${currency}</span>` +
`<span class="values-u23" style="display:none">${formatBigNumber(u23?.values)} ${currency}</span>` +
`<span class="values-u21" style="display:none">${formatBigNumber(u21?.values)} ${currency}</span>` +
`<span class="values-u18" style="display:none">${formatBigNumber(u18?.values)} ${currency}</span>`;
valueElement.style.textAlign = "right";
const ageElement = view.querySelector("td.age-value");
// prettier-ignore
ageElement.innerHTML =
`<span class="values-all" style="display:none;">${formatAverageAge(all?.avgAge)}</span>` +
`<span class="values-u23" style="display:none;">${formatAverageAge(u23?.avgAge)}</span>` +
`<span class="values-u21" style="display:none;">${formatAverageAge(u21?.avgAge)}</span>` +
`<span class="values-u18" style="display:none;">${formatAverageAge(u18?.avgAge)}</span>`;
tableDisplayAgeInfo(view, ageLimit);
}
}
function tableDisplayNextAgeInfo() {
const headers = document.querySelectorAll("th.team-value-header");
let changed = false;
for (const header of headers) {
header.age = tableGetNextAge(header.age);
header.innerText = tableGetValeTitle(header.sport, header.age);
if (!changed) {
changed = true;
tableDisplayAgeInfo(null, header.age);
}
}
}
function tableModifyHeaderForPcView(header, sport = "soccer", ageLimit = 99) {
const firstRow = header.querySelector("tr");
const value = document.createElement("th");
firstRow.appendChild(value);
const age = document.createElement("th");
firstRow.appendChild(age);
value.classList.add("responsive-hide", "team-value-header");
value.style.textAlign = "center";
value.innerText = tableGetValeTitle(sport, ageLimit);
value.title = "Click to see other age limits";
value.age = ageLimit;
value.sport = sport;
value.onclick = tableDisplayNextAgeInfo;
age.classList.add("responsive-hide");
age.style.textAlign = "center";
age.innerText = `Age`;
age.title = "Average Age Of the Top Players";
}
function tableAddHeaderForMobileView(mainHeader, sport = "soccer", ageLimit = 99) {
const mobileHeader = mainHeader.cloneNode(true);
mainHeader.parentNode.insertBefore(mobileHeader, mainHeader.nextSibling);
const secondRow = document.createElement("tr");
mobileHeader.appendChild(secondRow);
const value = document.createElement("th");
const age = document.createElement("th");
secondRow.appendChild(value);
secondRow.appendChild(age);
mobileHeader.classList.add("responsive-show", "mazyar-responsive-show");
const firstRow = mobileHeader.querySelector("tr");
firstRow.children[0].rowSpan = "2";
firstRow.children[1].rowSpan = "2";
value.classList.add("team-value-header");
value.colSpan = "6";
value.style.textAlign = "center";
value.innerText = tableGetValeTitle(sport, ageLimit);
value.title = "Click to see other age limits";
value.age = ageLimit;
value.sport = sport;
value.onclick = tableDisplayNextAgeInfo;
value.style.backgroundColor = "aquamarine";
age.colSpan = "2";
age.style.textAlign = "center";
age.innerText = `Age`;
age.title = "Average Age Of the Top Players";
age.style.backgroundColor = "aqua";
}
function tableInjectInProgressResults() {
const sport = getSportType();
const matches = document.querySelectorAll("table.hitlist.marker td > a");
const inProgressMatches = [...matches].filter((match) => isMatchInProgress(match.innerText));
for (const match of inProgressMatches) {
const mid = extractMatchID(match.href);
const url = `http://${location.hostname}/xml/match_info.php?sport_id=${sport === "soccer" ? 1 : 2}&match_id=${mid}`;
GM_xmlhttpRequest({
method: "GET",
url,
context: { match },
onload: function (resp) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(resp.responseText, "text/xml");
if (!xmlDoc.querySelector("ManagerZone_Error")) {
const home = xmlDoc.querySelector(`Team[field="home"]`).getAttribute("goals");
const away = xmlDoc.querySelector(`Team[field="away"]`).getAttribute("goals");
resp.context.match.innerText = home + " - " + away;
resp.context.match.classList.add("mazyar-in-progress-result");
}
},
});
}
}
function tableAddTopPlayersInfo(table) {
const ageLimit = tableGetAgeLimit(document.baseURI);
const sport = getSportType(document);
const mainHeader = table.querySelector("thead");
// for mobile
tableAddHeaderForMobileView(mainHeader, sport, ageLimit);
// for PC
mainHeader.classList.add("responsive-hide");
tableModifyHeaderForPcView(mainHeader, sport, ageLimit);
const teams = table.querySelectorAll("tbody tr");
for (const team of teams) {
tableAddTeamTopPlayersInfo(team, ageLimit, sport);
}
}
function tableWaitAndInjectTopPlayersInfo(timeout = 16000) {
const step = 500;
const interval = setInterval(() => {
const table = document.querySelector("table.nice_table");
if (table) {
clearInterval(interval);
if (!table.TopPlayersInfoInjected) {
table.TopPlayersInfoInjected = true;
if (mazyar.mustDisplayTopPlayersInTables()) {
tableAddTopPlayersInfo(table);
}
if (mazyar.mustDisplayInProgressResults()) {
tableInjectInProgressResults();
}
}
} else {
timeout -= step;
if (timeout < 0) {
clearInterval(interval);
}
}
}, step);
}
function tableInjectTopPlayersToOfficialLeague() {
// default sub-page (or tab) for leagues is Table. so try to inject team value after table is loaded
tableWaitAndInjectTopPlayersInfo();
// also add 'onclick' handler to Table tab
const links = document.getElementsByTagName("a");
for (const link of links) {
if (["p=league", "sub=table"].every((text) => link.href.indexOf(text) > -1)) {
link.onclick = tableWaitAndInjectTopPlayersInfo;
}
}
}
function tableInjectTopPlayersInfoToFriendlyLeague() {
const links = document.getElementsByTagName("a");
for (const link of links) {
if (["p=friendlySeries", "sub=standings"].every((text) => link.href.indexOf(text) > -1)) {
link.onclick = tableWaitAndInjectTopPlayersInfo;
}
}
}
function tableInjectTopPlayersInfoToCup() {
const links = document.getElementsByTagName("a");
for (const link of links) {
if (["p=cups", "sub=groupplay"].every((text) => link.href.indexOf(text) > -1)) {
link.onclick = tableWaitAndInjectTopPlayersInfo;
}
}
}
/* *********************** Schedule ********************************** */
function scheduleClearAllColorings(teams) {
teams.forEach((team) => {
team.style.backgroundColor = team.originalColor;
});
}
function scheduleResultColors(result) {
if (result.length < 2) {
return ["cyan", "cyan"];
}
if (result[0] === "X") {
return ["cyan", "cyan"];
}
if (result[0] < result[1]) {
return ["orangered", "lime"];
} else if (result[0] > result[1]) {
return ["lime", "orangered"];
}
return ["yellow", "yellow"];
}
function scheduleColorizeThisTeam(teams, selected) {
teams.forEach((team) => {
if (team.innerText === selected) {
const tr = team.parentNode;
const opponents = tr.querySelectorAll("td:nth-child(odd)");
const result = tr.querySelector("td:nth-child(2)").innerText.split(" - ");
const colors = scheduleResultColors(result);
if (opponents[0].innerText === selected) {
opponents[0].style.setProperty("background-color", colors[0], "important");
}
if (opponents[1].innerText === selected) {
opponents[1].style.setProperty("background-color", colors[1], "important");
}
}
});
}
function scheduleInjectColoring(tab) {
let selected = "";
const teams = tab.querySelectorAll("div.mainContent td:nth-child(odd)");
for (const team of teams) {
team.originalColor = team.style.backgroundColor;
team.onclick = null;
team.addEventListener("click", function (evt) {
if (selected && selected !== this.innerText) {
// new team is selected
scheduleClearAllColorings(teams);
selected = "";
}
if (selected === this.innerText) {
// de-colorize
selected = "";
scheduleClearAllColorings(teams);
} else {
// colorize
selected = this.innerText;
scheduleColorizeThisTeam(teams, selected);
}
});
}
}
function scheduleHasDuplicateName(round) {
const teams = round.querySelectorAll("td:nth-child(odd)");
const names = [...teams].map((t) => t.innerText);
return hasDuplicates(names);
}
function scheduleWaitAndInjectColoring(timeout = 16000) {
const step = 500;
const interval = setInterval(() => {
const firstRound = document.querySelector("div[aria-labelledby='league_tab_schedule'] div.mainContent");
if (firstRound) {
const schedule = firstRound.parentNode;
if (scheduleHasDuplicateName(firstRound)) {
const note = document.createElement("p");
note.innerHTML = `<b style="color: red;">Note: </b><span>Some teams have similar names. Coloring will not work as expected.</span>`;
note.style.fontSize = "1.2em";
schedule.insertBefore(note, schedule.firstChild);
}
clearInterval(interval);
scheduleInjectColoring(schedule);
} else {
timeout -= step;
if (timeout < 0) {
clearInterval(interval);
}
}
}, step);
}
function scheduleInjectColoringToOfficialLeague() {
const link = document.getElementById("league_tab_schedule");
if (link) {
const tab = link.parentNode;
if (!tab.coloringInjected) {
tab.coloringInjected = true;
tab.onclick = scheduleWaitAndInjectColoring;
}
}
}
/* *********************** Transfer Agent ********************************** */
function transferGetInputValue(el) {
if (el.type == "checkbox") {
return el.checked ? 1 : 0;
}
return el.value;
}
function transferGetSearchParams() {
const inputs = document.getElementById("searchform")?.querySelectorAll("input,select");
return [...inputs].map((el) => `&${el.name}=${transferGetInputValue(el)}`).join("");
}
function transferGetFilterNameFromUrl() {
const url = new URL(document.URL);
const params = new URLSearchParams(url.search);
if (params.has("mzy_filter_name")) {
return params.get("mzy_filter_name");
}
return null;
}
function transferLoadFilter(filterParams) {
const url = new URL(`https://${location.hostname}/?${filterParams}`);
const params = new URLSearchParams(url.search);
for (const param of params) {
const element = document.getElementById(param[0]);
if (element) {
if (element.type === "text") {
element.value = param[1];
element.dispatchEvent(new Event("blur"));
} else if (element.type === "checkbox") {
element.checked = param[1] === "1";
} else if (element.tagName === "SELECT") {
element.value = param[1];
}
}
}
}
function transferInjectButtons(form) {
const target = form.querySelector("div.buttons-wrapper.clearfix");
if (target) {
const filterButton = createMzStyledButton("MZY Filters", "red", "floatLeft");
filterButton.style.margin = "0";
filterButton.onclick = () => {
document.getElementById("mazyar-transfer-filter-hits")?.click();
};
const saveButton = createMzStyledButton("MZY Save Filter", "blue", "floatLeft");
saveButton.style.margin = "0";
saveButton.onclick = () => {
const params = transferGetSearchParams();
mazyar.displayFilterSaveMenu(params);
};
target.appendChild(filterButton);
target.appendChild(saveButton);
}
}
function transferCreateScoutOptions() {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container-row");
div.style.justifyContent = "left";
div.style.marginTop = "6px";
const highs = document.createElement("div");
highs.classList.add("mazyar-flex-container-row");
highs.style.justifyContent = "left";
highs.style.border = "1px inset black";
highs.style.marginRight = "2rem";
const lows = document.createElement("div");
lows.classList.add("mazyar-flex-container-row");
lows.style.justifyContent = "left";
lows.style.border = "1px outset black";
lows.style.marginRight = "2rem";
const transferOptions = mazyar.getTransferOptions();
const H4 = createMenuCheckBox("H4", transferOptions.H4, { margin: "auto 4px auto 1px" });
const H3 = createMenuCheckBox("H3", transferOptions.H3, { margin: "auto 4px auto 1px" });
const L2 = createMenuCheckBox("L2", transferOptions.L2, { margin: "auto 4px auto 1px" });
const L1 = createMenuCheckBox("L1", transferOptions.L1, { margin: "auto 4px auto 1px" });
const hide = createMenuCheckBox("Hide Others", transferOptions.hide, { margin: "auto 4px auto 1px" });
H4.onclick = () => {
mazyar.updateTransferOptions("H4", H4.querySelector("input[type=checkbox]").checked);
mazyar.updateDisplayForTransferSearchResults(true);
};
H3.onclick = () => {
mazyar.updateTransferOptions("H3", H3.querySelector("input[type=checkbox]").checked);
mazyar.updateDisplayForTransferSearchResults(true);
};
L2.onclick = () => {
mazyar.updateTransferOptions("L2", L2.querySelector("input[type=checkbox]").checked);
mazyar.updateDisplayForTransferSearchResults(true);
};
L1.onclick = () => {
mazyar.updateTransferOptions("L1", L1.querySelector("input[type=checkbox]").checked);
mazyar.updateDisplayForTransferSearchResults(true);
};
hide.onclick = () => {
mazyar.updateTransferOptions("hide", hide.querySelector("input[type=checkbox]").checked);
mazyar.updateDisplayForTransferSearchResults(true);
};
highs.appendChild(H4);
highs.appendChild(H3);
lows.appendChild(L2);
lows.appendChild(L1);
div.appendChild(highs);
div.appendChild(lows);
div.appendChild(hide);
div.appendChild(hide);
return div;
}
function transferInject() {
const form = document.getElementById("searchform");
if (form) {
const scoutOptions = transferCreateScoutOptions();
form.parentNode.appendChild(scoutOptions);
transferInjectButtons(form);
const filterName = transferGetFilterNameFromUrl();
if (filterName) {
const filterParams = mazyar.getFilterParams(filterName);
if (filterParams) {
setTimeout(() => {
transferLoadFilter(filterParams);
document.getElementById("searchb")?.click();
}, 5000);
}
}
}
}
/* *********************** Predictor ********************************** */
async function getNationalRankings() {
const rankings = [];
const url = 'https://${location.hostname}/?p=rank&sub=countryrank';
const resp = await fetch(url).catch((error) => {
console.warn(error);
});
if (resp) {
const parser = new DOMParser();
const doc = parser.parseFromString(await resp.text(), "text/html");
const teams = doc.querySelectorAll("table#countryRankTable tbody tr");
for (const team of teams) {
const columns = team.querySelectorAll("td");
let name = columns[2].innerText;
if (name === 'United States') {
name = 'USA';
}
rankings.push({
name,
rank: Number(columns[0].innerText),
})
}
}
return rankings;
}
function predictorAttachInfo(match, teams, rankings) {
teams[0].rank = rankings.find((t) => t.name.trim() === teams[0].name.trim())?.rank;
teams[1].rank = rankings.find((t) => t.name.trim() === teams[1].name.trim())?.rank;
teams[0].valueColor = (teams[0].value > teams[1].value) ? "blue" : "unset";
teams[1].valueColor = (teams[1].value > teams[0].value) ? "blue" : "unset";
teams[0].rankColor = (teams[0].rank < teams[1].rank) ? "blue" : "unset";
teams[1].rankColor = (teams[1].rank < teams[0].rank) ? "blue" : "unset";
const tbody = document.createElement("tbody");
for (const team of teams) {
tbody.innerHTML += `
<td style="text-align:left;padding: 0; width: 100px;"><b>${team.name}</b></td>
<td style="text-align:left;padding: 0;">[Rank: <b style="display:inline-block; width: 1.1rem; text-align:right; color: ${team.rankColor};">${team.rank ?? 'xx'}</b>]</td>
<td style="text-align:right;padding: 0 4px;">[Value: <b style="color: ${team.valueColor};">${formatBigNumber(team.value)}</b> ${team.currency}]</td>
`;
}
const table = document.createElement("table");
table.appendChild(tbody);
table.classList.add("mazyar-table");
const div = document.createElement("div");
div.appendChild(table);
const target = match.querySelector("td");
target.appendChild(div);
}
async function predictorInjectRecommendation(match, rankings) {
const href = match.querySelector("td > a")?.href;
const url = href.replace("&play=2d", "").replace("&type=2d", "");
const sport = getSportType();
const resp = await fetch(url).catch((error) => {
console.warn(error);
});
if (resp) {
const results = [];
const parser = new DOMParser();
const doc = parser.parseFromString(await resp.text(), "text/html");
const teams = doc.querySelectorAll("div.team-table");
for (const team of teams) {
const link = team.querySelector("a");
const name = link.innerText;
const tid = extractTeamId(link.href);
const { players, currency } = await getPlayersAndCurrency(tid, sport);
const playersOfSport = sport === "soccer" ? 11 : 21;
const all = filterPlayers(players, playersOfSport, 0, 99);
results.push({
name,
value: all?.values,
currency: currency,
})
}
predictorAttachInfo(match, results, rankings);
}
}
async function predictorInject() {
const rankings = await getNationalRankings();
const matches = document.querySelectorAll("#match-predictor-container table.match-list tbody tr");
const jobs = [];
for (const match of matches) {
jobs.push(predictorInjectRecommendation(match, rankings));
}
Promise.all(jobs);
}
/* *********************** Club Page ********************************** */
function countTrophies(doc) {
const trophies = doc.querySelectorAll("div.trophy-wrapper:not(.icon)");
return [...trophies].map((el) => {
const text = el.innerText.trim();
return text ? Number(text) : 1;
}).reduce((a, b) => a + b, 0);
}
async function getTrophiesCount(url) {
const resp = await fetch(url).catch((error) => {
console.warn(error);
return 'N/A';
});
if (resp?.status === 200) {
const parser = new DOMParser();
const doc = parser.parseFromString(await resp.text(), "text/html");
return countTrophies(doc);
}
return 'N/A'
}
function getTrophiesSection() {
const items = document.querySelectorAll("dl#trophies-achievements-lists dd");
for (const item of items) {
const link = item.querySelector("a");
if (link?.href.search("awards") > -1) {
return { trophies: item, url: link.href };
}
}
return { trophies: null, url: null };
}
async function addTrophyCountToClubPage() {
const { trophies, url } = getTrophiesSection();
if (trophies) {
const el = document.createTextNode(` (Total: ?)`);
trophies.querySelector("span")?.appendChild(el);
const count = await getTrophiesCount(url);
el.nodeValue = ` (Total: ${count})`;
}
}
/* *********************** Trainers ********************************** */
function trainersAddColumns(table) {
const headerRow = table.querySelector('thead tr');
const thSalary = document.createElement('th');
thSalary.textContent = 'Salary Range';
thSalary.style.textAlign = 'center';
thSalary.style.textDecoration = 'none';
headerRow?.appendChild(thSalary);
const thBonus = document.createElement('th');
thBonus.textContent = 'Bonus';
thBonus.style.textAlign = 'center';
thBonus.style.textDecoration = 'none';
headerRow?.appendChild(thBonus);
const thWeeks = document.createElement('th');
thWeeks.textContent = 'Weeks';
thWeeks.style.textAlign = 'center';
thWeeks.style.textDecoration = 'none';
headerRow?.appendChild(thWeeks);
const rows = table.querySelectorAll('tbody tr:not(.minified-view)');
rows.forEach(row => {
const salaryCell = row.insertCell(-1);
salaryCell.style.textAlign = 'center';
salaryCell.replaceChildren(createLoadingIcon2());
const bonusCell = row.insertCell(-1);
bonusCell.style.textAlign = 'center';
bonusCell.replaceChildren(createLoadingIcon2());
const weeksCell = row.insertCell(-1);
weeksCell.style.textAlign = 'center';
weeksCell.replaceChildren(createLoadingIcon2());
});
}
function trainersFetchSalaryAndWeeks(coachId, salaryCell, bonusCell, weeksCell) {
GM_xmlhttpRequest({
method: "GET",
url: `https://${location.hostname}/?p=trainers&sub=offer&extra=freeagent&cid=${coachId}`,
onload: function (response) {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, "text/html");
const salaryElement = doc.querySelector('td#salary_range nobr');
const weeksElement = doc.querySelector('td#weeks_range nobr');
const bonusElement = doc.querySelector("div#paper-content-wrapper > table td.contract_paper:nth-child(2)");
const salaryText = salaryElement?.innerText?.trim()?.match(/\d+(.*?\d+)* -(.*?\d+)+/g)?.[0];
salaryCell.textContent = salaryText ?? 'N/A';
salaryCell.style.fontStyle = 'normal';
const weeksText = weeksElement?.innerText?.trim()?.match(/\d+(.*?\d+)* -(.*?\d+)+/g)?.[0];
weeksCell.textContent = weeksText ?? 'N/A';
weeksCell.style.fontStyle = 'normal';
const bonusText = bonusElement?.innerText?.trim()?.match(/\d+(.*?\d+)*/g)?.[0];
bonusCell.textContent = bonusText ?? 'N/A';
bonusCell.style.fontStyle = 'normal';
}
});
}
function trainersUpdateSalariesAndWeeks(table) {
trainersAddColumns(table);
const rows = table?.querySelectorAll('tbody tr:not(.minified-view)');
rows.forEach(row => {
const linkElement = row.querySelector('td a[href*="cid="]');
if (linkElement) {
const urlParams = new URLSearchParams(linkElement.search);
const coachId = urlParams.get('cid');
const salaryCell = row.cells[row.cells.length - 3];
const bonusCell = row.cells[row.cells.length - 2];
const weeksCell = row.cells[row.cells.length - 1];
trainersFetchSalaryAndWeeks(coachId, salaryCell, bonusCell, weeksCell);
}
});
}
function trainersAddRequestedSalaries() {
const callback = () => {
const table = document.getElementById("coaches_list");
if (table && !table.injecting) {
table.injecting = true;
trainersUpdateSalariesAndWeeks(table);
}
};
const target = document.querySelector('div.in_page_navigation_top');
if (target) {
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(target, config);
}
}
/* *********************** Training Report ********************************** */
function trainingOpenPlayerTrainingCamp(player) {
player.querySelector('td.playerColumn a')?.click();
const interval = setInterval(() => {
const modal = document.querySelector("#lightbox_player_profile #players_container div.playerContainer");
if (modal) {
clearInterval(interval);
modal.querySelector('div.box_dark.player_tc_package_information a')?.click();
}
}, 300);
}
function trainingInjectCampOpener(table) {
const players = table.querySelectorAll("tbody tr");
for (const player of players) {
const campIcon = player.querySelector("td.dailyReportRightColumn img.content_middle");
if (campIcon) {
campIcon.style.cursor = "pointer";
campIcon.addEventListener("click", () => {
trainingOpenPlayerTrainingCamp(player);
});
}
}
}
function trainingAddCampOpenerToReport() {
const callback = () => {
const tables = document.querySelectorAll('#training_report > table');
for (const table of tables) {
if (!table.injecting) {
table.injecting = true;
trainingInjectCampOpener(table);
}
}
};
const target = document.getElementById('training_report');
if (target) {
const observer = new MutationObserver(callback);
const config = { childList: true, subtree: true };
observer.observe(target, config);
}
}
function trainingAddDays() {
const cssSelector = '#lightboxContent_player_profile #players_container';
const callback = () => {
const profile = document.querySelector(cssSelector);
if (profile && !profile.daysInjected) {
profile.daysInjected = true;
mazyar.trainingAddDaysAtThisClubToPlayerProfile(profile);
}
}
const target = document.body;
const config = { childList: true, subtree: true };
const observer = new MutationObserver(callback);
observer.observe(target, config);
}
/* *********************** Class ********************************** */
class Mazyar {
#modal = null; // dom element
#content = null; // dom element
#notebook = {
element: null,
text: "",
style: {
hide: true,
top: 0,
left: 0,
width: 200,
height: 250,
}
}
#settings = {
in_progress_results: true,
top_players_in_tables: true,
transfer: false,
transfer_maxed: false,
mz_predictor: false,
player_comment: false,
coach_salary: false,
deadline: {
enabled: false,
play_bell: false,
timeout: 30,// minutes,
},
days: {
display_in_profiles: false,
display_in_transfer: false,
display_in_training: false,
display_for_one_clubs: false,
}
};
#transferOptions = {
hide: true,
H4: false,
H3: false,
L2: false,
L1: false,
};
#filters = { soccer: [], hockey: [] }; // each key is like [{id, name, params, scout, interval}]
#sport = "soccer";
#deadlines = {}; // {pid1: {name, deadline}, ...}
#db;
#deadlineLockAcquired;
constructor() {
this.#initializeIndexedDb("Mazyar");
this.#fetchSettings();
this.#fetchTransferOptions();
this.#fetchFilters();
this.#sport = getSportType();
this.#createNotebook();
this.#addToolbar();
this.#createModal();
this.#showChangelog();
if (!this.isTransferFiltersEnabled()) {
this.#resetTransferOptions();
}
}
// -------------------------------- Settings -------------------------------------
#fetchSettings() {
this.#settings.in_progress_results = GM_getValue("display_in_progress_results", true);
this.#settings.top_players_in_tables = GM_getValue("display_top_players_in_tables", true);
this.#settings.transfer = GM_getValue("enable_transfer_filters", false);
this.#settings.transfer_maxed = GM_getValue("display_maxed_in_transfer", false);
this.#settings.mz_predictor = GM_getValue("mz_predictor", false);
this.#settings.player_comment = GM_getValue("player_comment", false);
this.#settings.coach_salary = GM_getValue("coach_salary", true);
this.#settings.deadline.enabled = GM_getValue("deadline", true);
this.#settings.deadline.play_bell = GM_getValue("deadline_play_bell", false);
this.#settings.deadline.timeout = GM_getValue("deadline_timeout", 30);
this.#settings.days.display_in_profiles = GM_getValue("display_days_in_profiles", false);
this.#settings.days.display_in_transfer = GM_getValue("display_days_in_transfer", false);
this.#settings.days.display_in_training = GM_getValue("display_days_in_training", false);
this.#settings.days.display_for_one_clubs = GM_getValue("display_days_for_one_clubs", false);
}
#saveSettings() {
GM_setValue("display_in_progress_results", this.#settings.in_progress_results);
GM_setValue("display_top_players_in_tables", this.#settings.top_players_in_tables);
GM_setValue("enable_transfer_filters", this.#settings.transfer);
GM_setValue("display_maxed_in_transfer", this.#settings.transfer_maxed);
GM_setValue("mz_predictor", this.#settings.mz_predictor);
GM_setValue("player_comment", this.#settings.player_comment);
GM_setValue("coach_salary", this.#settings.coach_salary);
GM_setValue("deadline", this.#settings.deadline.enabled);
GM_setValue("deadline_play_bell", this.#settings.deadline.play_bell);
GM_setValue("deadline_timeout", this.#settings.deadline.timeout);
GM_setValue("display_days_in_profiles", this.#settings.days.display_in_profiles);
GM_setValue("display_days_in_transfer", this.#settings.days.display_in_transfer);
GM_setValue("display_days_in_training", this.#settings.days.display_in_training);
GM_setValue("display_days_for_one_clubs", this.#settings.days.display_for_one_clubs);
}
updateSettings(settings) {
this.#settings = settings;
this.#saveSettings();
this.#resetTransferOptions();
if (this.isTransferFiltersEnabled()) {
this.#checkAllFilters(true);
} else {
this.#setFilterHitsInToolbar(0);
}
}
mustDisplayInProgressResults() {
return this.#settings.in_progress_results;
}
mustDisplayTopPlayersInTables() {
return this.#settings.top_players_in_tables;
}
isTransferFiltersEnabled() {
return this.#settings.transfer;
}
#isTransferMaxedSkillsEnabled() {
return this.#settings.transfer_maxed;
}
isDaysAtThisClubEnabledForPlayerProfiles() {
return this.#settings.days.display_in_profiles;
}
#isDaysAtThisClubEnabledForTraining() {
return this.#settings.days.display_in_training;
}
#isDaysAtThisClubEnabledForTransferMarket() {
return this.#settings.days.display_in_transfer;
}
isDaysAtThisClubEnabledForOneClubPlayers() {
return this.#settings.days.display_for_one_clubs;
}
mustHelpWithPredictor() {
return this.#settings.mz_predictor;
}
#mustAddPlayerComment() {
return this.#settings.player_comment;
}
mustAddCoachSalaries() {
return this.#settings.coach_salary;
}
#isTransferDeadlineAlertEnabled() {
return this.#settings.deadline.enabled;
}
#mustMarkMaxedSkills() {
return this.#settings.transfer_maxed;
}
// -------------------------------- Database -------------------------------------
async #initializeIndexedDb(name = "db") {
this.#db = new Dexie(name);
this.#db.version(1).stores({
comment: "[sport+pid]",
scout: "[sport+pid]",
hit: "[fid+pid],deadline",
filter: "&fid,totalHits,scoutHits,lastCheck",
});
this.#db.version(2).stores({
scout: "[sport+pid],ts",
player: "[sport+pid],ts,maxed,days",
hide: "[sport+pid],ts",
deadline: "[sport+pid],ts,deadline, name",
}).upgrade(trans => {
return trans.table("scout").toCollection().modify(report => {
report.ts = 0;
});
});
this.#db.open();
await this.#cleanOutdatedDataFromIndexedDb();
const info = await navigator?.storage?.estimate();
if (info) {
console.log("ManagerZone IndexedDB size: " + formatFileSize(info.usage));
}
}
async #cleanOutdatedDataFromIndexedDb() {
const scoutOutdate = Date.now() - 7 * 24 * 60 * 60 * 1000;
await this.#db.scout.where("ts").below(scoutOutdate).delete().then((deleteCount) => {
console.log("Deleted " + deleteCount + " outdated scout reports");
}).catch((error) => {
console.warn(error);
});
const startOfDay = Date.now() - Date.now() % (24 * 60 * 60 * 1000);
await this.#db.player.where("ts").below(startOfDay).delete().then((deleteCount) => {
console.log("Deleted " + deleteCount + " outdated player profile.");
}).catch((error) => {
console.warn(error);
});
}
async #clearIndexedDb() {
// TODO: clear comments?
await this.#db.scout.clear();
await this.#db.hit.clear();
await this.#db.filter.clear();
await this.#db.player.clear();
await this.#db.hide.clear();
}
async #isPlayerInHideListInIndexDb(pid) {
const player = await this.#db.hide.get({ pid, sport: this.#sport });
return player?.ts > 0;
}
async #addPlayerToHideListInIndexDb(pid) {
if (pid) {
await this.#db.hide.put({ pid, sport: this.#sport, ts: Date.now() });
}
}
async #addPlayerToDeadlineListInIndexDb(player = { pid: "", deadline: 0, name: "" }) {
if (player?.pid) {
this.#deadlines[player.pid] = player;
await this.#db.deadline.put({
sport: this.#sport,
ts: Date.now(),
pid: player.pid,
deadline: player.deadline,
name: player.name
});
}
}
async #removePlayerFromDeadlineList(pid) {
if (pid) {
delete this.#deadlines[pid];
await this.#db.deadline.delete([this.#sport, pid])
.catch((err) => console.warn(err));
}
}
async #fetchDeadlinePlayersFromIndexedDb() {
if (!this.#isTransferDeadlineAlertEnabled()) {
return [];
}
return await this.#db.deadline.toArray()
.then((players) => {
return players?.map(({ pid, deadline, name }) => ({ pid, deadline, name }));
}
).catch((err) => {
console.warn(err);
return [];
});
}
async #deletePlayersFromHideListInIndexDb(days = 0) {
const outdate = Date.now() - days * 24 * 60 * 60 * 1000;
return await this.#db.hide.where("ts").below(outdate).delete().then((deleteCount) => {
return deleteCount;
}).catch((error) => {
console.warn(error);
return 0;
});
}
async #countPlayersOfHideListInIndexDb() {
return await this.#db.hide.where("ts").above(0).count();
}
async #fetchPlayerCommentFromIndexedDb(pid) {
const player = await this.#db.comment.get({ pid, sport: this.#sport });
return player?.comment ?? "";
}
async #setPlayerCommentInIndexedDb(pid, comment) {
if (pid) {
await this.#db.comment.put({ pid, sport: this.#sport, comment: comment ?? "" });
}
}
async #fetchScoutReportFromIndexedDb(pid) {
return await this.#db.scout.get({ pid, sport: this.#sport });
}
async #setScoutReportInIndexedDb(report) {
if (report) {
report.sport = this.#sport;
report.ts = Date.now();
await this.#db.scout.put(report);
}
}
async #setHitInIndexedDb(filterId = "", playerId = "") {
if (filterId && playerId) {
await this.#db.hit.put({ fid: filterId, pid: playerId });
}
}
async #removeHitFromIndexedDb(filterId = "", playerId = "") {
if (filterId && playerId) {
await this.#db.hit.delete([filterId, playerId]);
}
}
async #removeAllHitsOfFilterFromIndexedDb(filterId = "") {
if (filterId) {
await this.#db.hit.where("fid").equals(filterId).delete();
}
}
async #getHitsFromIndexedDb(filterId = "") {
if (filterId) {
return this.#db.hit.filter((hit) => hit.fid === filterId).toArray();
}
return [];
}
async #setFilterHitsInIndexedDb(filterId = "", totalHits = 0, scoutHits = -1) {
if (filterId) {
const item = {
fid: filterId,
totalHits,
scoutHits,
lastCheck: Date.now(),
};
await this.#db.filter.put(item);
}
}
async getFilterHitsFromIndexedDb(filterId = "") {
if (filterId) {
const filter = await this.#db.filter.get(filterId);
if (filter) {
return { totalHits: filter.totalHits, scoutHits: filter.scoutHits, lastCheck: filter.lastCheck };
}
}
return { totalHits: -1, scoutHits: -1, lastCheck: Date.now() };
}
async #removeFilterFromIndexedDb(filterId = "") {
await this.#db.filter.delete(filterId);
await this.#removeAllHitsOfFilterFromIndexedDb(filterId);
}
async #removeAllFilterFromIndexedDb() {
await this.#db.filter.clear();
await this.#db.hit.clear();
}
#extractSkillNamesFromPlayerInfo(player) {
const skills = player?.querySelectorAll("table.player_skills > tbody > tr > td > span.clippable");
return [...skills].map((el) => el.innerText);
}
#extractStarsFromScoutReport(section) {
return section.querySelectorAll(".stars i.lit")?.length;
}
#extractSkillsFromScoutReport(section, skillList) {
const skills = section.querySelectorAll("div.flex-grow-1 ul li.blurred span");
return [...skills].map((el) => skillList.indexOf(el.innerText));
}
async #fetchPlayerProfileFromIndexedDb(pid) {
return await this.#db.player.get({ pid, sport: this.#sport });
}
async #setPlayerProfileInIndexedDb(profile) {
if (profile) {
profile.sport = this.#sport;
profile.ts = Date.now();
await this.#db.player.put(profile);
}
}
async #extractPlayerProfile(playerId) {
const doc = await fetchPlayerProfileDocument(playerId);
if (doc) {
const { days, price } = squadExtractResidencyDaysAndPrice(doc);
const skills = doc.querySelectorAll("#thePlayers_0 table.player_skills tbody tr");
let i = 0;
const maxed = [];
for (const skill of skills) {
if (skill.querySelector(".maxed")) {
maxed.push(i);
}
i++;
}
return {
pid: playerId,
maxed,
days,
price,
};
}
return null;
}
async #fetchOrExtractPlayerProfile(playerId) {
let profile = await this.#fetchPlayerProfileFromIndexedDb(playerId);
if (!profile || (profile.days > 0 && !profile.price)) {
profile = await this.#extractPlayerProfile(playerId);
this.#setPlayerProfileInIndexedDb(profile);
}
return profile;
}
async #extractPlayerScoutReport(pid, skills) {
const url = `https://${location.hostname}/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=${this.#sport}`;
const resp = await fetch(url).catch((error) => {
console.warn(error);
});
if (resp) {
const parser = new DOMParser();
const doc = parser.parseFromString(await resp.text(), "text/html");
const report = doc.querySelectorAll(".paper-content.clearfix dl dd");
if (report.length == 3) {
const high = this.#extractStarsFromScoutReport(report[0]);
const highSkills = this.#extractSkillsFromScoutReport(report[0], skills);
const low = this.#extractStarsFromScoutReport(report[1]);
const lowSkills = this.#extractSkillsFromScoutReport(report[1], skills);
const trainingSpeed = this.#extractStarsFromScoutReport(report[2]);
return {
pid,
H: high,
HS: highSkills,
L: low,
LS: lowSkills,
S: trainingSpeed,
};
}
}
return null;
}
#colorizeMaxedSkills(player, maxed = []) {
if (maxed) {
const playerSkills = player.querySelectorAll("table.player_skills tr td.skillval span");
for (const skill of maxed) {
playerSkills[skill].classList.add("maxed");
}
}
}
#colorizeSkills(player, report) {
if (report) {
const playerSkills = player.querySelectorAll("table.player_skills tr td:nth-child(1)");
playerSkills[report.HS[0]]?.classList.add("mazyar-scout-h", `mazyar-scout-${report.H}`);
playerSkills[report.HS[1]]?.classList.add("mazyar-scout-h", `mazyar-scout-${report.H}`);
playerSkills[report.LS[0]]?.classList.add(`mazyar-scout-${report.L}`);
playerSkills[report.LS[1]]?.classList.add(`mazyar-scout-${report.L}`);
}
}
async #fetchOrExtractPlayerScoutReport(player) {
const playerId = player.querySelector("h2 a")?.href?.match(/pid=(\d+)/)?.[1];
let report = await this.#fetchScoutReportFromIndexedDb(playerId);
if (!report) {
const skills = this.#extractSkillNamesFromPlayerInfo(player);
report = await this.#extractPlayerScoutReport(playerId, skills);
if (report) {
this.#setScoutReportInIndexedDb(report);
}
}
return report;
}
async #getPlayerScoutReportForSearchResult(players) {
const jobs = [];
for (const player of [...players.children]) {
if (player.classList.contains("playerContainer") && player.querySelector("span.scout_report > a")) {
jobs.push(this.#fetchOrExtractPlayerScoutReport(player));
}
}
const reports = await Promise.all(jobs);
return reports.filter((report) => report != null);
}
#isQualifiedForTransferScoutFilter(report, lows = [], highs = []) {
return highs.includes(report.H) && lows.includes(report.L);
}
#areTransferLowOptionsSelected() {
return this.#transferOptions.L2 || this.#transferOptions.L1;
}
#areTransferHighOptionsSelected() {
return this.#transferOptions.H4 || this.#transferOptions.H3;
}
#areTransferScoutOptionsSelected() {
return this.#areTransferLowOptionsSelected() || this.#areTransferHighOptionsSelected();
}
#getAcceptableHighsAndLows() {
let lows = [];
let highs = [];
if (this.#areTransferLowOptionsSelected()) {
if (this.#transferOptions.L2) {
lows.push(2);
}
if (this.#transferOptions.L1) {
lows.push(1);
}
} else {
lows = [1, 2, 3];
}
if (this.#areTransferHighOptionsSelected()) {
if (this.#transferOptions.H4) {
highs.push(4);
}
if (this.#transferOptions.H3) {
highs.push(3);
}
} else {
highs = [1, 2, 3, 4];
}
return { lows, highs };
}
#applyTransferFilters(report, lows, highs) {
if (!this.#isQualifiedForTransferScoutFilter(report, lows, highs)) {
const player = document.getElementById(`player_id_${report.pid}`);
if (player) {
const playerContainer = player.parentNode.parentNode.parentNode;
if (this.#transferOptionsMustHide()) {
playerContainer.classList.add("mazyar-hide");
} else {
playerContainer.classList.add("mazyar-dim-60");
}
}
}
}
#clearTransferFilters() {
document.querySelectorAll(".mazyar-hide")?.forEach((el) => {
el.classList.remove("mazyar-hide");
});
document.querySelectorAll(".mazyar-dim-50")?.forEach((el) => el.classList.remove("mazyar-dim-50"));
document.querySelectorAll(".mazyar-dim-60")?.forEach((el) => el.classList.remove("mazyar-dim-60"));
}
#isMaxedOrDaysEnabledForTransfer() {
return this.#isDaysAtThisClubEnabledForTransferMarket() || this.#isTransferMaxedSkillsEnabled()
}
async #updateMaxedAndDaysInTransfer(player) {
const playerId = getPlayerIdFromContainer(player);
await this.#fetchOrExtractPlayerProfile(playerId).then((profile) => {
if (this.#isDaysAtThisClubEnabledForTransferMarket()) {
this.#squadAddDaysAtThisClubForSinglePlayer(player, profile, true);
}
if (this.#mustMarkMaxedSkills()) {
this.#colorizeMaxedSkills(player, profile?.maxed);
}
});
};
async #hidePlayerAccordingToHideList(player) {
const playerId = getPlayerIdFromContainer(player);
if (await this.#isPlayerInHideListInIndexDb(playerId)) {
player.style.display = 'none';
}
}
#addHideButtonToPlayerInTransferMarket(player) {
if (player.hideButtonInjected) {
return;
}
player.hideButtonInjected = true;
const playerId = getPlayerIdFromContainer(player);
const hideIcon = createDeleteIcon("Hide player from search result.");
hideIcon.classList.add("floatRight");
hideIcon.style.marginTop = "0.2rem";
player.querySelector("h2.clearfix div")?.appendChild(hideIcon);
hideIcon.addEventListener("click", () => {
this.#addPlayerToHideListInIndexDb(playerId);
player.style.display = 'none';
});
}
async #processTransferSearchResults(results) {
const { lows, highs } = this.#getAcceptableHighsAndLows();
const players = [...results.children].filter((player) => player.classList.contains("playerContainer"));
const deadlines = await this.#fetchDeadlinePlayersFromIndexedDb();
const jobs = [];
for (const player of players) {
this.#addHideButtonToPlayerInTransferMarket(player);
if (this.#isTransferDeadlineAlertEnabled()) {
this.#addDeadlineButtonToPlayerInTransferMarket(player, deadlines);
}
jobs.push(this.#hidePlayerAccordingToHideList(player));
if (this.#isMaxedOrDaysEnabledForTransfer()) {
jobs.push(this.#updateMaxedAndDaysInTransfer(player));
}
if (this.#areTransferScoutOptionsSelected()) {
if (player.querySelector("span.scout_report > a")) {
jobs.push(this.#fetchOrExtractPlayerScoutReport(player).then((report) => {
this.#colorizeSkills(player, report);
this.#applyTransferFilters(report, lows, highs);
}));
} else {
const className = this.#transferOptionsMustHide() ? "mazyar-hide" : "mazyar-dim-50";
player.classList.add(className);
}
}
}
await Promise.all(jobs);
}
#mustUpdateDisplayForTransferSearchResults() {
return this.#areTransferScoutOptionsSelected()
|| this.#isTransferMaxedSkillsEnabled()
|| this.#isDaysAtThisClubEnabledForTransferMarket();
}
async updateDisplayForTransferSearchResults(clear = false) {
if (clear) {
this.#clearTransferFilters();
}
if (this.#mustUpdateDisplayForTransferSearchResults()) {
const results = document.getElementById("players_container");
if (results) {
await this.#processTransferSearchResults(results);
}
}
}
#injectHideButtonToTransferMarket() {
const target = document.querySelector("#searchform div.buttons-wrapper.clearfix");
if (target) {
const hideButton = createMzStyledButton("MZY Hide", "red", "floatLeft");
hideButton.style.margin = "0";
hideButton.onclick = () => {
this.#displayTransferHideMenu();
};
target.appendChild(hideButton);
}
}
async executeTransferTasks() {
this.#injectHideButtonToTransferMarket();
this.updateDisplayForTransferSearchResults();
const callback = (mutationsList) => {
if (mutationsList.find(mutation => mutation.type == "childList")) {
this.updateDisplayForTransferSearchResults();
}
};
const target = document.getElementById("players_container");
if (target) {
const observer = new MutationObserver(callback);
const config = { childList: true };
observer.observe(target, config);
}
}
#getSelectedScoutsOptionText() {
const texts = [];
const options = this.getTransferOptions();
if (options.H4) {
texts.push('<span class="mazyar-scout-4">H4<span>');
}
if (options.H3) {
texts.push('<span class="mazyar-scout-3">H3<span>');
}
if (options.L2) {
texts.push('<span class="mazyar-scout-2">L2<span>');
}
if (options.L1) {
texts.push('<span class="mazyar-scout-1">L1<span>');
}
return texts.join(", ");
}
#getSelectedScoutsOptionTextForFilter(scout) {
const texts = [];
if (scout.high.includes(4)) {
texts.push('<span class="mazyar-scout-4">H4<span>');
}
if (scout.high.includes(3)) {
texts.push('<span class="mazyar-scout-3">H3<span>');
}
if (scout.low.includes(2)) {
texts.push('<span class="mazyar-scout-2">L2<span>');
}
if (scout.low.includes(1)) {
texts.push('<span class="mazyar-scout-1">L1<span>');
}
return texts.join(", ");
}
// -------------------------------- Player Options -------------------------------------
async #addPlayerCommentIcon(player) {
const playerId = getPlayerIdFromContainer(player);
const parent = document.createElement("span");
parent.classList.add("player_icon_placeholder");
const a = document.createElement("a");
a.classList.add("player_icon");
parent.appendChild(a);
const iconSpan = document.createElement("span");
iconSpan.classList.add("player_icon_wrapper");
a.appendChild(iconSpan);
const commentIcon = createCommentIcon("MZY Comment");
commentIcon.style.fontSize = "1.2rem";
iconSpan.appendChild(commentIcon);
if (location.hostname === 'www.managerzone.com') {
parent.classList.add("mazyar-player-comment-icon-www");
}
if (await this.#fetchPlayerCommentFromIndexedDb(playerId)) {
commentIcon.classList.add("mazyar-player-comment-icon-active");
} else {
commentIcon.classList.add("mazyar-player-comment-icon-inactive");
}
commentIcon.addEventListener("click", (event) => {
this.#displayPlayerComment(event?.target, playerId);
});
const whitespace = document.createTextNode(" ");
player.querySelector(".p_links")?.appendChild(whitespace);
player.querySelector(".p_links")?.appendChild(parent);
}
async addPlayerComment() {
if (this.#mustAddPlayerComment()) {
const jobs = [];
const players = document.querySelectorAll(".playerContainer");
for (const player of players) {
jobs.push(this.#addPlayerCommentIcon(player));
}
Promise.all(jobs);
}
}
async #squadAddDaysAtThisClubForSinglePlayer(player, profile, addPrice = false) {
if (player.daysInjected) {
return;
}
player.daysInjected = true;
const daysDiv = document.createElement("div");
if (profile?.days >= 0) {
const text = profile?.days === 0 ? 'N/A' : `≤ ${profile?.days}`;
daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`;
if (addPrice && profile?.price !== null) {
daysDiv.innerHTML += ` <span style="margin-left: 25px;">Fee: <strong style="color: blue;">${profile?.price}</strong><span>`;
}
daysDiv.classList.add("mazyar-days-at-this-club");
} else if (this.isDaysAtThisClubEnabledForOneClubPlayers()) {
const text = 'Entire Career';
daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`;
daysDiv.classList.add("mazyar-days-at-this-club-entire");
}
player.querySelector("div.mainContent")?.appendChild(daysDiv);
}
async squadAddDaysAtThisClubToAllPlayers(container) {
if (this.isDaysAtThisClubEnabledForPlayerProfiles()) {
const jobs = [];
const players = container.querySelectorAll("div.playerContainer");
for (const player of players) {
jobs.push((async (player) => {
const playerId = getPlayerIdFromContainer(player);
await this.#fetchOrExtractPlayerProfile(playerId).then((profile) => {
this.#squadAddDaysAtThisClubForSinglePlayer(player, profile);
});
})(player));
}
Promise.all(jobs);
}
}
async trainingAddDaysAtThisClubToPlayerProfile(container) {
if (this.#isDaysAtThisClubEnabledForTraining()) {
const player = container.querySelector("div.playerContainer");
const playerId = getPlayerIdFromContainer(player);
await this.#fetchOrExtractPlayerProfile(playerId).then((profile) => {
this.#squadAddDaysAtThisClubForSinglePlayer(player, profile);
});
}
}
// -------------------------------- Transfer Options -------------------------------------
#fetchTransferOptions() {
this.#transferOptions.hide = GM_getValue("transfer_options_hide", true);
this.#transferOptions.H4 = GM_getValue("transfer_options_H4", false);
this.#transferOptions.H3 = GM_getValue("transfer_options_H3", false);
this.#transferOptions.L2 = GM_getValue("transfer_options_L2", false);
this.#transferOptions.L1 = GM_getValue("transfer_options_L1", false);
}
#saveTransferOptions() {
GM_setValue("transfer_options_hide", this.#transferOptions.hide);
GM_setValue("transfer_options_H4", this.#transferOptions.H4);
GM_setValue("transfer_options_H3", this.#transferOptions.H3);
GM_setValue("transfer_options_L2", this.#transferOptions.L2);
GM_setValue("transfer_options_L1", this.#transferOptions.L1);
}
#transferOptionsMustHide() {
return this.#transferOptions.hide;
}
getTransferOptions() {
return this.#transferOptions;
}
updateTransferOptions(key, value) {
this.#transferOptions[key] = value;
this.#saveTransferOptions();
}
#resetTransferOptions() {
this.#transferOptions.hide = false;
this.#transferOptions.H4 = false;
this.#transferOptions.H3 = false;
this.#transferOptions.L2 = false;
this.#transferOptions.L1 = false;
this.#saveTransferOptions();
}
// -------------------------------- Filters -------------------------------------
#fetchFilters() {
this.#filters = GM_getValue("transfer_filters", { soccer: [], hockey: [] });
}
#saveFilters() {
GM_setValue("transfer_filters", {
soccer: this.#filters.soccer.map(({ id, name, params, scout, interval }) => ({
id,
name,
params,
scout,
interval,
})),
hockey: this.#filters.hockey.map(({ id, name, params, scout, interval }) => ({
id,
name,
params,
scout,
interval,
})),
});
}
#getCurrentFilters() {
return this.#filters[this.#sport];
}
deleteFilter(id = "") {
const filterIndex = this.#getCurrentFilters().findIndex((f) => f.id === id);
if (filterIndex > -1) {
this.#removeFilterFromIndexedDb(id);
this.#getCurrentFilters().splice(filterIndex, 1);
this.#checkAllFilters(true);
this.#saveFilters();
}
}
deleteAllFilters() {
this.#removeAllFilterFromIndexedDb();
this.#getCurrentFilters().length = 0;
this.#checkAllFilters(true);
this.#saveFilters();
}
getFilterParams(name = "") {
return this.#getCurrentFilters().find((f) => f.name === name)?.params;
}
async #updateFilterDetails(name, params, scout, interval) {
const filters = this.#getCurrentFilters();
let filter = filters.find((f) => f.name === name);
if (filter) {
filter.params = params;
filter.scout = scout;
filter.interval = interval;
} else {
// create a new filter if name does not exist
filter = {
name,
params,
scout,
interval,
id: generateUuidV4(),
};
filters.push(filter);
}
this.#saveFilters();
const { totalHits, scoutHits } = await this.#getFilterTotalHits(filter);
if (totalHits >= 0) {
await this.#setFilterHitsInIndexedDb(filter.id, totalHits, scoutHits);
this.#checkAllFilters(true);
}
}
#itsTimeToCheckFilter(filter, lastCheck) {
const passed = Date.now() - lastCheck;
if (filter.interval === TRANSFER_INTERVALS.never.value) {
return false;
} else if (filter.interval === TRANSFER_INTERVALS.onceDay.value) {
return passed > 24 * 60 * 60 * 1000;
} else if (filter.interval === TRANSFER_INTERVALS.onceHour.value) {
return passed > 1 * 60 * 60 * 1000;
} else if (filter.interval === TRANSFER_INTERVALS.onceMinute.value) {
return passed > 1 * 60 * 1000;
}
// 'always' or any invalid value means always
return true;
}
#getAcceptableHighsAndLowsForFilter(scout) {
const lows = scout.low.length > 0 ? scout.low : [1, 2, 3];
const highs = scout.high.length > 0 ? scout.high : [1, 2, 3, 4];
return { lows, highs };
}
async #getFilterHitsByOffset(filter, offset = 0) {
let totalHits = -1;
let scoutHits = -1;
const url = `https://${location.hostname}/ajax.php?p=transfer&sub=transfer-search&sport=${this.#sport}${filter.params}&o=${offset}`;
const response = await fetch(url).catch((error) => {
console.warn(error);
});
if (response) {
const data = await response.json();
totalHits = Number(data?.totalHits);
const searchResults = document.createElement("div");
searchResults.innerHTML = data.players;
if (filter.scout) {
const playersReport = await this.#getPlayerScoutReportForSearchResult(searchResults);
const { lows, highs } = this.#getAcceptableHighsAndLowsForFilter(filter.scout);
const scouted = playersReport.filter((report) => this.#isQualifiedForTransferScoutFilter(report, lows, highs));
scoutHits = scouted.length;
// save scout hits in indexed db
const jobs = [];
for (const player of scouted) {
jobs.push(this.#setHitInIndexedDb(filter.id, player.pid));
}
await Promise.all(jobs);
}
}
return { totalHits, scoutHits };
}
async #getFilterTotalHits(filter) {
// get first page of the results
await this.#removeAllHitsOfFilterFromIndexedDb(filter.id);
let { totalHits, scoutHits } = await this.#getFilterHitsByOffset(filter);
if (totalHits >= 0) {
if (filter.scout) {
const pages = Math.min(Math.ceil(totalHits / 20), 26);
const jobs = [];
// when filter uses scout report, get next pages too
for (let i = 1; i < pages; i++) {
jobs.push(this.#getFilterHitsByOffset(filter, 20 * i));
}
const results = await Promise.all(jobs);
for (const result of results) {
scoutHits += result.scoutHits;
}
}
}
return { totalHits, scoutHits };
}
async refreshFilterHits(id = "") {
const filter = this.#getCurrentFilters().find((filter) => filter.id === id);
if (filter) {
const { totalHits, scoutHits } = await this.#getFilterTotalHits(filter);
if (totalHits >= 0) {
this.#setFilterHitsInIndexedDb(filter.id, totalHits, scoutHits);
this.#checkAllFilters(false);
return { totalHits, scoutHits };
}
}
return { totalHits: -1, scoutHits: -1 };
}
// -------------------------------- Display -------------------------------------
#createModal() {
this.#modal = document.createElement("div");
this.#modal.id = "mazyar-modal";
this.#modal.classList.add("mazyar-flex-container");
this.#content = document.createElement("div");
this.#content.id = "mazyar-modal-content";
this.#content.classList.add("mazyar-flex-container", "mazyar-scrollable-vertical");
this.#modal.appendChild(this.#content);
this.#hideModal();
document.body?.appendChild(this.#modal);
}
#addToolbar() {
const { toolbar, menu, transfer, note } = createToolbar();
menu.addEventListener("click", () => {
this.#displaySettingsMenu();
});
transfer.addEventListener("click", () => {
this.#displayTransferFilters();
});
note.addEventListener("click", () => {
if (this.#notebook.style.hide) {
this.#showNotebook();
this.#saveNotebookStyle();
} else {
this.#hideNotebook();
this.#saveNotebookStyle();
}
});
document.body?.appendChild(toolbar);
}
#displayModal() {
this.#modal.style.display = "flex";
}
#clearModalContent() {
this.#content.innerText = "";
}
#replaceModalContent(elements = []) {
this.#clearModalContent();
for (const element of elements) {
if (element) {
this.#content.appendChild(element);
}
}
this.#displayModal();
}
#displayLoading(title = "MZY") {
const header = createMzStyledTitle(title);
const loading = document.createElement("p");
loading.innerText = "Loading...";
loading.style.padding = "1rem";
this.#replaceModalContent([header, loading]);
}
async cleanInstall() {
this.updateSettings({
in_progress_results: false,
top_players_in_tables: false,
transfer: false,
transfer_maxed: false,
mz_predictor: false,
player_comment: false,
coach_salary: false,
deadline: false,
days: {
display_in_profiles: false,
display_in_transfer: false,
display_for_training: false,
display_for_one_clubs: false,
}
});
this.deleteAllFilters();
await this.#clearIndexedDb();
this.#resetTransferOptions();
}
#displayCleanMenu() {
const div = document.createElement("div");
const title = createMzStyledTitle("MZY Settings");
const notice = document.createElement("div");
const buttons = document.createElement("div");
const cancel = createMzStyledButton("Cancel", "red");
const clean = createMzStyledButton("Clean", "blue");
div.classList.add("mazyar-flex-container");
buttons.classList.add("mazyar-flex-container-row");
notice.innerHTML = "All Settings, Filters, Scout Reports and ... will be deleted.<br>Are you sure?";
notice.style.padding = "1rem";
clean.addEventListener("click", async () => {
await this.cleanInstall();
this.#hideModal();
this.#displaySettingsMenu();
});
cancel.addEventListener("click", () => {
this.#hideModal();
this.#displaySettingsMenu();
});
div.appendChild(title);
div.appendChild(notice);
buttons.appendChild(cancel);
buttons.appendChild(clean);
div.appendChild(buttons);
this.#replaceModalContent([div]);
}
// --------------------------- Notebook ------------------------------------
#fetchNotebookStyle() {
const defaultStyle = {
hide: true,
top: 0,
left: 0,
width: 200,
height: 250,
};
this.#notebook.style = GM_getValue("notebook_style", defaultStyle);
// reject invalid data
if (this.#notebook.style.top < 0
|| this.#notebook.style.left < 0
|| this.#notebook.style.left > window.innerWidth
|| this.#notebook.style.top > window.innerHeight) {
this.#notebook.style.top = 0;
this.#notebook.style.left = 0;
this.#saveNotebookStyle();
}
}
#saveNotebookStyle() {
GM_setValue("notebook_style", this.#notebook.style);
}
#fetchNotebookText() {
this.#notebook.text = GM_getValue("notebook_text", "");
}
#saveNotebookText() {
GM_setValue("notebook_text", this.#notebook.text);
}
#updateNotebookDisplay(content, text) {
this.#fetchNotebookStyle();
this.#fetchNotebookText();
if (this.#notebook.style.hide) {
this.#notebook.element.style.display = "none";
} else {
this.#notebook.element.style.display = "flex";
}
text.value = this.#notebook.text;
content.style.width = this.#notebook.style.width + "px";
content.style.height = this.#notebook.style.height + "px";
content.style.top = this.#notebook.style.top + "px";
content.style.left = this.#notebook.style.left + "px";
}
#updateNotebookLocation(content) {
const { top, left, width, height } = content.getBoundingClientRect();
this.#notebook.style.top = top;
this.#notebook.style.left = left;
this.#notebook.style.width = width;
this.#notebook.style.height = height;
}
#hideNotebook() {
this.#notebook.element.style.display = "none";
this.#notebook.style.hide = true;
}
#showNotebook() {
this.#notebook.element.style.display = "flex";
this.#notebook.style.hide = false;
}
#createNotebook() {
this.#notebook.element = document.createElement("div");
const content = document.createElement("div");
const contentHeader = createMzStyledTitle("MZY Notebook");
const text = document.createElement("textarea");
const hide = createMzStyledButton("Hide", "blue");
const save = createMzStyledButton("Save", "green");
const warning = document.createElement("div");
const discard = createMzStyledButton("Discard", "red");
const buttons = document.createElement("div");
this.#notebook.element.classList.add("mazyar-flex-container", "mazyar-notebook-plain", "mazyar-scrollable-vertical");
content.classList.add("mazyar-flex-container", "mazyar-resizable", "mazyar-scrollable-vertical", "mazyar-notebook-modal");
text.classList.add("mazyar-notebook-textarea");
buttons.classList.add("mazyar-flex-container-row");
warning.innerText = "You have unsaved changes!";
warning.style.color = "red";
warning.style.display = "none";
warning.style.marginTop = "5px";
save.style.display = "none";
discard.style.display = "none";
this.#updateNotebookDisplay(content, text);
document.addEventListener("focus", () => {
this.#updateNotebookDisplay(content, text);
save.style.display = "none";
warning.style.display = "none";
discard.style.display = "none";
});
makeElementDraggable(content, contentHeader, () => {
this.#updateNotebookLocation(content);
this.#saveNotebookStyle();
});
content.addEventListener("mouseup", () => {
this.#updateNotebookLocation(content);
this.#saveNotebookStyle();
})
text.addEventListener("input", () => {
if (text.value !== this.#notebook.text) {
save.style.display = "unset";
warning.style.display = "unset";
discard.style.display = "unset";
} else {
save.style.display = "none";
warning.style.display = "none";
discard.style.display = "none";
}
})
discard.addEventListener("click", () => {
text.value = this.#notebook.text;
save.style.display = "none";
warning.style.display = "none";
discard.style.display = "none";
});
hide.addEventListener("click", () => {
this.#hideNotebook();
this.#saveNotebookStyle();
});
save.addEventListener("click", () => {
this.#notebook.text = text.value;
this.#saveNotebookText();
save.style.display = "none";
warning.style.display = "none";
discard.style.display = "none";
});
buttons.appendChild(hide);
buttons.appendChild(discard);
buttons.appendChild(save);
content.appendChild(contentHeader);
content.appendChild(text);
content.appendChild(warning);
content.appendChild(buttons);
this.#notebook.element.appendChild(content);
document.body?.appendChild(this.#notebook.element);
}
// ----------------------------------------------------------------------------------
#createDeadlineOptions(submenuStyle) {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container");
const enabled = createMenuCheckBox("Enable deadline alert", this.#settings.deadline.enabled, submenuStyle);
const playBell = createMenuCheckBox("Sound Notification", this.#settings.deadline.play_bell, { margin: "0.1rem 2.2rem" });
const timeout = createSubMenuTextInput("Timeout", "30", this.#settings.deadline.timeout);
const unit = document.createTextNode("minute(s)");
timeout.appendChild(unit);
timeout.style.display = this.#settings.deadline.enabled ? "unset" : "none";
playBell.style.display = this.#settings.deadline.enabled ? "unset" : "none";
div.appendChild(enabled); // child node 0
div.appendChild(timeout); // child node 1
div.appendChild(playBell); // child node 2
enabled.addEventListener("input", () => {
timeout.style.display = enabled.querySelector("input[type='checkbox']").checked ? "unset" : "none";
playBell.style.display = enabled.querySelector("input[type='checkbox']").checked ? "unset" : "none";
});
timeout.addEventListener("input", () => {
const minutes = timeout.querySelector("input[type='text']");
if (minutes?.value?.match(/^\d+$/)) {
const value = Number(minutes?.value)
if (value < 1) {
minutes.value = "1";
} else if (value > 360) {
minutes.value = "360";
}
} else {
minutes.value = "";
}
});
return div;
}
#displaySettingsMenu() {
const submenuStyle = { margin: "0.1rem 1.2rem" };
const div = document.createElement("div");
const title = createMzStyledTitle("MZY Settings");
const miscellaneousGroup = createMenuGroup("Miscellaneous:");
const playerComment = createMenuCheckBox("Enable player comment", this.#settings.player_comment, submenuStyle);
const inProgress = createMenuCheckBox("Display in progress results", this.#settings.in_progress_results, submenuStyle);
const tableInjection = createMenuCheckBox("Display teams' top players in tables", this.#settings.top_players_in_tables, submenuStyle);
const mzPredictor = createMenuCheckBox("Help with World Cup Predictor", this.#settings.mz_predictor, submenuStyle);
mzPredictor.style.display = 'none';
miscellaneousGroup.appendChild(playerComment);
miscellaneousGroup.appendChild(inProgress);
miscellaneousGroup.appendChild(tableInjection);
miscellaneousGroup.appendChild(mzPredictor);
const coachesGroup = createMenuGroup("Coaches:");
const coachSalaries = createMenuCheckBox("Display salaries in search results", this.#settings.coach_salary, submenuStyle);
coachesGroup.appendChild(coachSalaries);
const transferGroup = createMenuGroup("Transfer Market:");
const transferFilters = createMenuCheckBox("Enable transfer filters", this.#settings.transfer, submenuStyle);
const transferMaxed = createMenuCheckBox("Mark maxed skills", this.#settings.transfer_maxed, submenuStyle);
const transferDeadline = this.#createDeadlineOptions(submenuStyle);
transferGroup.appendChild(transferFilters);
transferGroup.appendChild(transferMaxed);
transferGroup.appendChild(transferDeadline);
const daysGroup = createMenuGroup("Days at this club:");
const daysInProfiles = createMenuCheckBox("Display in player profiles", this.#settings.days.display_in_profiles, submenuStyle);
const daysInTransfer = createMenuCheckBox("Display in transfer market", this.#settings.days.display_in_transfer, submenuStyle);
const daysInTraining = createMenuCheckBox("Display in training report", this.#settings.days.display_in_training, submenuStyle);
const daysForOneClubs = createMenuCheckBox("Display for One-Club players", this.#settings.days.display_for_one_clubs, submenuStyle);
daysGroup.appendChild(daysInProfiles);
daysGroup.appendChild(daysInTransfer);
daysGroup.appendChild(daysInTraining);
daysGroup.appendChild(daysForOneClubs);
const buttons = document.createElement("div");
const clean = createMzStyledButton(`<i class="fa fa-exclamation-triangle" style="font-size: 0.9rem;"></i> Clean Install`, "blue");
const cancel = createMzStyledButton("Cancel", "red");
const save = createMzStyledButton("Save", "green");
div.classList.add("mazyar-flex-container");
buttons.classList.add("mazyar-flex-container-row");
cancel.addEventListener("click", () => {
this.#hideModal();
});
save.onclick = () => {
const deadlineTimeout = Number(transferDeadline.childNodes[1].querySelector("input[type=text]")?.value);
this.updateSettings({
in_progress_results: inProgress.querySelector("input[type=checkbox]").checked,
top_players_in_tables: tableInjection.querySelector("input[type=checkbox]").checked,
transfer: transferFilters.querySelector("input[type=checkbox]").checked,
transfer_maxed: transferMaxed.querySelector("input[type=checkbox]").checked,
deadline: {
enabled: transferDeadline.childNodes[0].querySelector("input[type=checkbox]")?.checked,
play_bell: transferDeadline.childNodes[2].querySelector("input[type=checkbox]")?.checked,
timeout: deadlineTimeout > 0 && deadlineTimeout <= 360 ? deadlineTimeout : 30,
},
mz_predictor: mzPredictor.querySelector("input[type=checkbox]").checked,
player_comment: playerComment.querySelector("input[type=checkbox]").checked,
coach_salary: coachSalaries.querySelector("input[type=checkbox]").checked,
days: {
display_in_profiles: daysInProfiles.querySelector("input[type=checkbox]").checked,
display_in_transfer: daysInTransfer.querySelector("input[type=checkbox]").checked,
display_in_training: daysInTraining.querySelector("input[type=checkbox]").checked,
display_for_one_clubs: daysForOneClubs.querySelector("input[type=checkbox]").checked,
}
});
this.#hideModal();
};
clean.style.marginBottom = "0";
clean.addEventListener("click", () => {
this.#hideModal();
this.#displayCleanMenu();
});
div.appendChild(title);
div.appendChild(transferGroup);
div.appendChild(daysGroup);
div.appendChild(coachesGroup);
div.appendChild(miscellaneousGroup);
div.appendChild(clean);
buttons.appendChild(cancel);
buttons.appendChild(save);
div.appendChild(buttons);
this.#replaceModalContent([div]);
}
#getSelectedHighLows(useScout) {
const high = [];
const low = [];
if (useScout.querySelector("input[type=checkbox]").checked) {
const options = this.getTransferOptions();
if (options.H4) {
high.push(4);
}
if (options.H3) {
high.push(3);
}
if (options.L2) {
low.push(2);
}
if (options.L1) {
low.push(1);
}
}
return { low, high };
}
displayFilterSaveMenu(params) {
const filters = this.#getCurrentFilters();
const scoutText = this.#getSelectedScoutsOptionText();
const title = createMzStyledTitle("MZY Transfer Filter");
const div = document.createElement("div");
const datalist = createSuggestionList(filters.map((f) => f.name));
const filterName = createMenuTextInput("Filter Name", "U21 K-10 ST-10", datalist.id);
const useScout = createMenuCheckBox(`Use scout reports too (${scoutText})`);
const checkInterval = createMenuDropDown("Check Interval", TRANSFER_INTERVALS, TRANSFER_INTERVALS.onceHour.value);
const validation = document.createElement("div");
const buttons = document.createElement("div");
const cancel = createMzStyledButton("Cancel", "red");
const save = createMzStyledButton("Save", "green");
div.classList.add("mazyar-flex-container");
validation.innerText = "Error: Name is empty.";
validation.style.color = "red";
validation.style.display = "none";
validation.style.alignSelf = "flex-start";
validation.style.margin = "auto 0.5rem";
filterName.oninput = () => {
const name = filterName.querySelector("input[type='text']").value;
if (name) {
validation.style.display = "none";
save.classList.remove(getMzButtonColorClass("grey"));
} else {
validation.style.display = "unset";
save.classList.add(getMzButtonColorClass("grey"));
}
};
buttons.classList.add("mazyar-flex-container-row");
cancel.addEventListener("click", () => {
this.#hideModal();
});
save.addEventListener("click", () => {
// save then close
const name = filterName.querySelector("input[type=text]").value;
if (name) {
const { low, high } = this.#getSelectedHighLows(useScout);
const scout = high.length === 0 && low.length === 0 ? null : { high, low };
const interval = checkInterval.querySelector("select").value;
this.#updateFilterDetails(name, params, scout, interval);
this.#hideModal();
} else {
validation.style.display = "unset";
save.classList.add(getMzButtonColorClass("grey"));
}
});
buttons.appendChild(cancel);
buttons.appendChild(save);
div.appendChild(title);
div.appendChild(filterName);
div.appendChild(checkInterval);
if (scoutText) {
div.appendChild(useScout);
}
div.appendChild(datalist);
div.appendChild(validation);
div.appendChild(buttons);
this.#replaceModalContent([div]);
}
#setFilterHitsInToolbar(total = -1) {
// pass negative to create loading animation.
const hits = document.getElementById("mazyar-transfer-filter-hits");
if (hits) {
if (total < 0) {
hits.replaceChildren(createLoadingIcon());
} else {
hits.innerText = total > 100 ? "+100" : total.toString();
hits.style.color = total > 0 ? "cyan" : "white";
}
}
}
async #checkAllFilters(forced = false) {
this.#setFilterHitsInToolbar(-1);
const filters = this.#getCurrentFilters();
let total = 0;
for (const filter of filters) {
let { totalHits: hits, lastCheck } = await this.getFilterHitsFromIndexedDb(filter.id);
const needRefresh = this.#itsTimeToCheckFilter(filter, lastCheck);
if (!filterHitsIsValid(hits) || forced || needRefresh) {
const { totalHits, scoutHits } = await this.#getFilterTotalHits(filter);
if (totalHits >= 0) {
hits = filter.scout ? scoutHits : totalHits;
await this.#setFilterHitsInIndexedDb(filter.id, totalHits, scoutHits);
}
}
total += hits;
}
this.#setFilterHitsInToolbar(total);
return total;
}
async setInitialFiltersHitInToolbar() {
await this.#checkAllFilters(false);
}
async #displayTransferHideMenu() {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container");
const title = createMzStyledTitle("MZY Transfer Hide List");
await this.#countPlayersOfHideListInIndexDb();
const body = document.createElement("div");
const description = document.createElement("div");
const dayClearDiv = document.createElement("div");
const daysInput = createMenuTextInput("Days", "0", "");
const clear = createMzStyledButton("Remove", "red", "floatRight");
const validation = document.createElement("div");
const result = document.createElement("div");
dayClearDiv.appendChild(daysInput);
dayClearDiv.appendChild(clear);
body.appendChild(description);
body.appendChild(dayClearDiv);
body.appendChild(validation);
body.appendChild(result);
body.classList.add("mazyar-flex-container");
body.style.maxWidth = "320px";
dayClearDiv.classList.add("mazyar-flex-container-row");
daysInput.querySelector("input[type='text']").style.width = "2rem";
const close = createMzStyledButton("Close", "green");
description.innerHTML = `<p>There are <strong style="color:red;">${await this.#countPlayersOfHideListInIndexDb()}</strong> player(s) hidden from transfer market.</p>
<p>You can remove players from hide list.</p>
<p>Enter the number of days to remove players hidden before this period.<br>Enter 0 to remove all.</p>`;
description.style.paddingLeft = "0.7rem";
validation.innerText = "Error: Invalid value for days.";
validation.style.color = "red";
validation.style.display = "none";
validation.style.margin = "auto 0.5rem";
result.style.display = "none";
result.style.margin = "auto 0.5rem";
result.innerHTML = `Removed <strong style="color:blue;">0</strong> player(s) from hide list.</p>`
daysInput.addEventListener("input", () => {
result.style.display = "none";
const days = daysInput.querySelector("input[type='text']").value;
if (days.match(/^\d+$/)) {
validation.style.display = "none";
clear.classList.remove(getMzButtonColorClass("grey"));
} else {
validation.style.display = "unset";
clear.classList.add(getMzButtonColorClass("grey"));
}
});
clear.addEventListener("click", async () => {
const days = daysInput.querySelector("input[type='text']").value
if (days.match(/\d+/)) {
const deleteCount = await this.#deletePlayersFromHideListInIndexDb(days);
result.querySelector("strong").innerText = deleteCount;
result.style.display = "unset";
description.querySelector("strong").innerText = await this.#countPlayersOfHideListInIndexDb();
} else {
validation.style.display = "unset";
clear.classList.add(getMzButtonColorClass("grey"));
}
});
close.addEventListener("click", () => {
this.#hideModal();
});
div.appendChild(title);
div.appendChild(body);
div.appendChild(close);
this.#replaceModalContent([div]);
}
async #displayTransferFilters() {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container");
const title = createMzStyledTitle("MZY Transfer Filters");
const filtersView = document.createElement("div");
filtersView.classList.add("mazyar-flex-container");
const noFilterView = document.createElement("span");
noFilterView.innerText = "There is no filter to display";
noFilterView.style.display = "none";
noFilterView.style.margin = "1rem";
const filters = this.#getCurrentFilters();
if (filters.length > 0) {
const deleteAll = createDeleteButtonWithTrashIcon("Delete all filters");
deleteAll.addEventListener("click", () => {
this.deleteAllFilters();
filtersView.style.display = "none";
noFilterView.style.display = "unset";
});
const table = filtersViewCreateTable(filters);
table.addEventListener("destroy", () => {
// remove 'delete all' button if no filter is left
filtersView.style.display = "none";
noFilterView.style.display = "unset";
});
filtersView.appendChild(deleteAll);
filtersView.appendChild(table);
} else {
noFilterView.style.display = "unset";
}
const close = createMzStyledButton("Close", "green");
close.addEventListener("click", () => {
this.#hideModal();
});
div.appendChild(title);
div.appendChild(filtersView);
div.appendChild(noFilterView);
div.appendChild(close);
this.#replaceModalContent([div]);
}
async displaySquadSummary(url) {
this.#displayLoading("MZY Squad Summary");
await fetch(url)
.then((resp) => resp.text())
.then((content) => {
const parser = new DOMParser();
const doc = parser.parseFromString(content, "text/html");
const sport = getSportType(doc);
const currency = getClubCurrency(doc);
const players = getClubPlayers(doc, currency);
const summary = squadGetPlayersInfo(players, sport);
const topPlayers = squadCreateTopPlayersTable(summary, currency, sport);
topPlayers.style.margin = "2px 5px";
topPlayers.style.padding = "0";
const header = createMzStyledTitle("MZY Squad Summary");
const button = createMzStyledButton("Close", "red");
button.addEventListener("click", () => {
this.#hideModal();
});
this.#replaceModalContent([header, topPlayers, button]);
})
.catch((error) => {
console.warn(error);
});
}
async #displayTransferDeadlines() {
const div = document.createElement("div");
div.classList.add("mazyar-flex-container");
const title = createMzStyledTitle("MZY Transfer Deadlines");
const middle = document.createElement("div");
middle.classList.add("mazyar-scrollable-vertical");
middle.style.flex = "1";
middle.style.margin = "5px 2px";
const bids = document.createElement("table");
bids.style.margin = "5px 2px";
middle.appendChild(bids);
const thead = document.createElement("thead");
thead.innerHTML = `<tr><th style="width: 15px;"></th><th style="text-align: left;"><strong>Player</strong></th><th><strong>Deadline</strong></th></tr>`;
bids.appendChild(thead);
const sortedBids = Object.values(this.#deadlines)?.sort((a, b) => a.deadline - b.deadline);
const tbody = document.createElement("tbody");
bids.appendChild(tbody);
for (const bid of sortedBids) {
const row = document.createElement("tr");
const deleteButton = document.createElement("td");
deleteButton.addEventListener("click", async () => {
await this.#removePlayerFromDeadlineList(bid.pid);
row.remove();
if (tbody.childElementCount === 0) {
this.#deadlineUpdateIconStyle();
this.#hideModal();
}
});
const trashIcon = createTrashIcon("Remove from deadline list");
deleteButton.appendChild(trashIcon);
const name = document.createElement("td");
name.innerHTML = `<a href="/?p=transfer&sub=players&u=${bid.pid}" target="_blank">${bid.name}</a>`;
name.style.paddingRight = "15px";
const deadline = document.createElement("td");
deadline.innerHTML = `<strong>${bid.deadline}</strong> minutes`;
deadline.style.paddingRight = "15px";
if (bid.deadline <= this.#settings.deadline.timeout) {
deadline.style.color = "red";
}
row.appendChild(deleteButton);
row.appendChild(name);
row.appendChild(deadline);
tbody.appendChild(row);
}
const close = createMzStyledButton("Close", "green");
close.addEventListener("click", () => {
this.#hideModal();
});
div.appendChild(title);
div.appendChild(middle);
div.appendChild(close);
this.#replaceModalContent([div]);
}
#addDeadlineIndicator() {
const deadline = createDeadlineIndicator();
deadline.id = "mazyar-deadline";
deadline.style.display = "none";
deadline.style.border = "1px solid black";
deadline.style.borderRadius = "50%";
deadline.style.padding = "0.5rem";
document.body?.appendChild(deadline);
deadline.addEventListener("click", () => {
this.#displayTransferDeadlines();
});
return deadline;
}
async #deadlineFetchAndProcessMonitor() {
const response = await fetchTransferMonitorData();
if (response) {
const yourBids = document.createElement("div");
yourBids.innerHTML = response.content;
const bids = yourBids.querySelectorAll(`table[cellpadding="0"][border="0"] table a:not([class="player_icon"])`);
const deadlines = yourBids.querySelectorAll(`table[cellpadding="0"][border="0"] table img[src="img/icon_deadline.gif"]`);
const players = [...Array(bids.length).keys()].map((n) => ({
name: bids[n].innerText,
pid: extractPlayerIDFromTransferMonitor(bids[n].href),
deadline: 1 + Math.ceil((parseMzDateTime(deadlines[n]?.parentNode?.parentNode?.innerText?.trim()) - new Date()) / 60_000)
}));
for (const player of players) {
if (player.deadline > 0) {
await this.#addPlayerToDeadlineListInIndexDb(player);
} else {
await this.#removePlayerFromDeadlineList(player.pid);
}
}
}
}
async #updatePlayerDeadlineFromMarket(pid) {
const url = `https://${location.hostname}/ajax.php?p=transfer&sub=transfer-search&sport=${this.#sport}&u=${pid}`;
const result = await fetch(url)
.then((resp) => resp.json())
.catch((err) => {
console.warn(err);
return null;
});
if (result) {
if (result.totalHits > 0) {
const parser = new DOMParser();
const playerDiv = parser.parseFromString(result?.players, "text/html").body.firstChild;
const deadline = playerDiv.querySelector(".transfer-control-area div.box_dark:nth-child(1) table:nth-child(1) tr:nth-child(3) strong")?.innerText;
const player = {
name: playerDiv.querySelector(".player_name")?.innerText,
pid,
deadline: 1 + Math.ceil((parseMzDateTime(deadline.trim()) - new Date()) / 60_000),
};
await this.#addPlayerToDeadlineListInIndexDb(player);
} else {
await this.#removePlayerFromDeadlineList(pid);
}
}
}
async #deadlineProcessPlayersInIndexedDb() {
const players = await this.#fetchDeadlinePlayersFromIndexedDb();
const jobs = [];
for (const player of players) {
if (this.#deadlineLockAcquired) {
jobs.push(this.#updatePlayerDeadlineFromMarket(player.pid));
} else if (player.deadline > 0) {
this.#deadlines[player.pid] = player;
} else {
delete this.#deadlines[player.pid];
}
}
await Promise.all(jobs);
}
#deadlineIsLockerTabDead() {
return Date.now() - Number(GM_getValue("deadline_locker_last_call", 0)) > (2 * DEADLINE_INTERVAL_SECONDS * 1000);
}
#deadlineUpdateLockStatus() {
if (this.#deadlineLockAcquired) {
// we have the lock
GM_setValue("deadline_locker_last_call", Date.now());
} else {
// check whether if lock is available to acquire
const isLocked = GM_getValue("deadline_is_locked", false);
if (isLocked) {
// other instance of mazyar is already required the lock
this.#deadlineLockAcquired = false;
if (this.#deadlineIsLockerTabDead()) {
// it seems locker tab is not alive. make it available for other tabs
console.warn("locker tab is dead. release the lock.");
GM_setValue("deadline_is_locked", false);
}
} else {
// we can acquire the lock
GM_setValue("deadline_is_locked", true);
GM_setValue("deadline_locker_last_call", Date.now());
this.#deadlineLockAcquired = true;
window.onbeforeunload = () => {
// release lock
GM_setValue("deadline_is_locked", false);
this.#deadlineLockAcquired = false;
}
}
}
}
#playDeadlineAlert() {
if (this.#settings.deadline.play_bell) {
const ding = new Audio(deadlineAlertSound);
ding.play();
}
}
#deadlineUpdateIconStyle() {
const deadlineIcon = document.getElementById("mazyar-deadline");
const strobe = Object.values(this.#deadlines).filter((player) => player.deadline <= this.#settings.deadline.timeout).length > 0;
if (strobe && deadlineIcon) {
deadlineIcon.style.display = 'unset';
deadlineIcon.classList.add("mazyar-deadline-throb-lightgreen");
this.#playDeadlineAlert();
} else {
deadlineIcon.style.display = 'none';
deadlineIcon.classList.remove("mazyar-deadline-throb-lightgreen");
}
}
async #updateDeadlines() {
this.#deadlineUpdateLockStatus();
// remove old data
this.#deadlines = {};
await this.#deadlineProcessPlayersInIndexedDb();
if (this.#deadlineLockAcquired) {
await this.#deadlineFetchAndProcessMonitor();
}
this.#deadlineUpdateIconStyle();
}
async injectTransferDeadlineAlert() {
if (this.#isTransferDeadlineAlertEnabled()) {
this.#addDeadlineIndicator();
await this.#updateDeadlines();
setInterval(() => {
this.#updateDeadlines();
}, 1000 * DEADLINE_INTERVAL_SECONDS);
}
}
#addDeadlineButtonToPlayerInTransferMarket(player, deadlines) {
if (player.deadlineIconInjected) {
return;
}
player.deadlineIconInjected = true;
const playerId = getPlayerIdFromContainer(player);
let alreadyAdded = deadlines.find((player) => player.pid == playerId);
const addButton = createAddToDeadlineIcon("Deadline Monitor Add/Remove", alreadyAdded ? "red" : "green")
player.querySelector("h2.clearfix div")?.appendChild(addButton);
addButton.addEventListener("click", async () => {
if (alreadyAdded) {
alreadyAdded = false;
addButton.style.color = "green";
await this.#removePlayerFromDeadlineList(playerId);
} else {
alreadyAdded = true;
addButton.style.color = "red";
await this.#updatePlayerDeadlineFromMarket(playerId);
}
this.#deadlineUpdateIconStyle();
});
}
#createFilterInfo(data = { name: "", scout: { high: [], low: [] }, count: "" }) {
const info = document.createElement("div");
const nameSpan = document.createElement("span");
const scoutSpan = document.createElement("span");
const countSpan = document.createElement("span");
info.style.margin = "3px";
info.style.padding = "3px";
info.style.alignSelf = "flex-start";
nameSpan.innerHTML = `<strong>Filter Name: </strong>${data.name}<br>`;
scoutSpan.innerHTML = `<strong>Selected: ${this.#getSelectedScoutsOptionTextForFilter(data.scout)}<strong><br>`;
countSpan.innerHTML = `<strong>Hit Count: </strong><span class="filter-count-span">${data.count}<span>`;
info.appendChild(nameSpan);
info.appendChild(scoutSpan);
info.appendChild(countSpan);
return info;
}
async #appendFilterResultToModal(middle, searchResults, filterId) {
for (const result of searchResults) {
const parser = new DOMParser();
const player = parser.parseFromString(result.content.players, "text/html").body.firstChild;
if (this.#isMaxedOrDaysEnabledForTransfer()) {
this.#updateMaxedAndDaysInTransfer(player);
}
player.id = "";
this.#fetchOrExtractPlayerScoutReport(player).then(report => {
this.#colorizeSkills(player, report);
});
const a = player.querySelector("h2>div>a.subheader");
if (a) {
a.href = `https://${location.hostname}/?p=transfer&u=${result.playerId}`;
a.target = "_blank";
const tools = player.querySelector("td span.player_icon_placeholder.bid_button")?.parentNode;
if (tools) {
tools.style.display = "none";
}
player.querySelector("div.floatRight.transfer-control-area")?.classList.add("mazyar-transfer-control-area");
middle.appendChild(player);
} else {
this.#removeHitFromIndexedDb(filterId, result.playerId);
}
}
}
async displayFilterResults(filterId, filterInfo) {
const div = document.createElement("div");
const header = createMzStyledTitle("MZY Filter Results");
const info = this.#createFilterInfo(filterInfo);
const middle = document.createElement("div");
const close = createMzStyledButton("Close", "red");
div.classList.add("mazyar-flex-container");
middle.classList.add("mazyar-scrollable-vertical");
middle.style.flex = "1";
middle.style.margin = "5px 2px";
close.style.marginBottom = "1px";
close.addEventListener("click", () => {
this.#hideModal();
this.#displayTransferFilters();
});
const players = await this.#getHitsFromIndexedDb(filterId);
this.#displayLoading("MZY Filter Results");
const jobs = [];
for (const player of players) {
const url = `https://${location.hostname}/ajax.php?p=transfer&sub=transfer-search&sport=${this.#sport}&u=${player.pid}`;
jobs.push(
fetch(url)
.then((resp) => resp.json())
.then((content) => {
return { playerId: player.pid, content };
})
);
}
const searchResults = await Promise.all(jobs);
this.#appendFilterResultToModal(middle, searchResults, filterId);
const noResult = middle.childNodes.length === 0;
if (noResult) {
middle.innerHTML = "<h3>No Players To Display</h3><span>Please refresh the filter to update hits.</span>";
middle.style.padding = "10px";
middle.style.textAlign = "center";
} else {
info.querySelector(".filter-count-span").innerText = middle.childNodes.length.toString();
}
div.appendChild(header);
if (noResult) {
div.appendChild(middle);
div.appendChild(close);
} else {
div.appendChild(close);
div.appendChild(info);
div.appendChild(middle);
}
this.#replaceModalContent([div]);
}
async #displayPlayerComment(target, playerId) {
this.#displayLoading("MZY Player Note");
const header = createMzStyledTitle("MZY Player Note");
const text = document.createElement("textarea");
const cancel = createMzStyledButton("Cancel", "red");
const save = createMzStyledButton("Save", "green");
const buttons = document.createElement("div");
buttons.classList.add("mazyar-flex-container-row");
text.value = await this.#fetchPlayerCommentFromIndexedDb(playerId);
text.classList.add("mazyar-player-comment-textarea");
cancel.addEventListener("click", () => {
this.#hideModal();
});
save.addEventListener("click", async () => {
await this.#setPlayerCommentInIndexedDb(playerId, text.value);
if (text.value) {
target?.classList.remove("mazyar-player-comment-icon-inactive");
target?.classList.add("mazyar-player-comment-icon-active");
} else {
target?.classList.add("mazyar-player-comment-icon-inactive");
target?.classList.remove("mazyar-player-comment-icon-active");
}
this.#hideModal();
});
buttons.appendChild(cancel);
buttons.appendChild(save);
this.#replaceModalContent([header, text, buttons]);
}
#getVersionNumbers(v) {
return v.split('.').map((i) => Number(i));
}
#getVersionsOfChangelog(changelogs) {
return Object.keys(changelogs).map((v) => this.#getVersionNumbers(v));
}
#isVersionLesserThan(version = [0, 0], base = [0, 0]) {
return version[0] < base[0] ||
(version[0] === base[0] && version[1] < base[1]);
}
#isVersionGreaterThan(version = [0, 0], base = [0, 0]) {
return version[0] > base[0] ||
(version[0] === base[0] && version[1] > base[1]);
}
#showChangelog() {
const previousVersion = GM_getValue("previous_version", "");
if (!previousVersion) {
GM_setValue("previous_version", currentVersion);
return;
}
const previous = this.#getVersionNumbers(previousVersion);
const current = this.#getVersionNumbers(currentVersion);
if (!this.#isVersionLesserThan(previous, current)) {
return;
}
const headHTML = `<b>Mazyar</b> is updated<br>` +
`from <b style="color: red;">v${previousVersion}</b><span> to </span><b style="color: blue;">v${currentVersion}</b>`;
const head = document.createElement("div");
head.innerHTML = headHTML;
head.style.textAlign = "center";
const changesTitle = document.createElement("div");
changesTitle.innerHTML = `<b>Changelog:</b>`;
changesTitle.style.margin = "5px";
changesTitle.style.width = "100%";
changesTitle.style.width = "100%";
changesTitle.style.textAlign = "left";
let changesHTML = '';
const versions = this.#getVersionsOfChangelog(changelogs);
for (const version of versions) {
if (this.#isVersionGreaterThan(version, current)) {
continue;
}
if (this.#isVersionGreaterThan(version, previous)) {
const v = version.join('.');
changesHTML += `<div style="margin-bottom: 1rem;"><b>v${v}</b><ul style="margin: 0px 5px 5px;"><li>`
+ changelogs[v]?.join("</li><li>") + "</li></ul></div>";
}
}
const changes = document.createElement("div");
changes.innerHTML = changesHTML;
changes.style.maxHeight = "320px";
changes.style.maxWidth = "320px";
changes.style.paddingRight = "30px";
changes.style.backgroundColor = "khaki";
changes.style.padding = "5px";
changes.style.flex = "1";
changes.classList.add("mazyar-scrollable-vertical");
const text = document.createElement("div");
text.classList.add("mazyar-flex-container");
text.style.margin = "10px";
text.style.padding = "5px";
text.appendChild(head);
text.appendChild(changesTitle);
text.appendChild(changes);
const header = createMzStyledTitle("MZY Notice");
const close = createMzStyledButton("close", "green");
close.addEventListener("click", () => {
GM_setValue("previous_version", currentVersion);
this.#hideModal();
});
this.#replaceModalContent([header, text, close]);
}
showPlayerInModal(playerView) {
const player = document.createElement("div");
player.style.margin = "5px";
player.style.padding = "0px";
player.style.backgroundColor = "wheat";
player.appendChild(playerView);
const header = createMzStyledTitle("MZY Player View");
const close = createMzStyledButton("close", "green");
close.addEventListener("click", () => {
this.#hideModal();
});
this.#replaceModalContent([header, player, close]);
}
#hideModal() {
this.#modal.style.display = "none";
this.#clearModalContent();
}
}
/* *********************** Inject ********************************** */
function isVisitingTeamPage() {
const regex = /^\?((p=team)|(p=team&tid=\d+))$/g;
return regex.test(document.location.search);
}
async function inject() {
GM_addStyle(styles);
mazyar = new Mazyar();
if (mazyar.isTransferFiltersEnabled()) {
mazyar.setInitialFiltersHitInToolbar();
}
mazyar.injectTransferDeadlineAlert();
const uri = document.baseURI;
if (uri.search("/?p=federations") > -1) {
if (uri.search("&sub=clash") > -1) {
clashInjectRanks();
} else if (uri.search("&fid=") > -1 || uri.endsWith("p=federations")) {
federationFrontPage();
}
} else if (uri.search("/?p=players") > -1) {
squadInjectInformationToSummary();
squadInjectInformationToProfiles();
} else if (uri.search("mid=") > -1) {
matchInjectTeamValues();
} else if (uri.search("/?p=match") > -1 && !uri.search("&sub=result") > -1) {
matchInjectInProgressResults();
} else if (uri.search("/?p=league") > -1) {
tableInjectTopPlayersToOfficialLeague();
scheduleInjectColoringToOfficialLeague();
} else if (uri.search("/?p=friendlyseries") > -1) {
tableInjectTopPlayersInfoToFriendlyLeague();
} else if (uri.search("/?p=cup&") > -1 || uri.search("/?p=private_cup&") > -1) {
tableInjectTopPlayersInfoToCup();
} else if (uri.search("/?p=transfer") > -1) {
if (mazyar.isTransferFiltersEnabled()) {
transferInject();
}
mazyar.executeTransferTasks();
} else if (uri.search("/?p=clubhouse") > -1) {
if (mazyar.mustHelpWithPredictor()) {
predictorInject();
}
} else if (isVisitingTeamPage()) {
addTrophyCountToClubPage();
} else if (uri.search("/?p=trainers") > -1) {
if (mazyar.mustAddCoachSalaries()) {
trainersAddRequestedSalaries();
}
} else if (uri.search("/?p=training_report") > -1) {
trainingAddCampOpenerToReport();
trainingAddDays();
} else if (uri.search("/?p=rank&sub=userrank") > -1) {
rankingInjectSquadValue();
}
}
if (document.readyState === "loading") {
// Loading hasn't finished yet
document.addEventListener("DOMContentLoaded", () => {
inject();
});
} else {
// `DOMContentLoaded` has already fired
inject();
}
})();