// ==UserScript== // @name Mazyar // @namespace http://tampermonkey.net/ // @version 2.41 // @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 // @downloadURL none // ==/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.41": ["[new] 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": ["[fix] Transfer: change Fee font color to blue."], "2.39": ["[new] 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": ["[fix] Transfer Filters: delete icon was missing in 'MZY Transfer Filters' modal."], "2.37": ["[new] support Managerzone Test Site (test.managerzone.com). It is not fully tested. Please report any issues you encounter in Test site too."], "2.36": [ "[new] Deadline Alert: add 'Timeout' option in MZY Settings to set deadline timeout. Its value must be between 1 and 360 minutes.", "[new] Deadline Alert: add 'Sound Notification' option to play a bell sound when deadline of at least one of monitored players is less than timeout.", "[fix] Deadline Alert: trash icon was missing in 'MZY Transfer Deadlines' modal when 'van.mz.playerAdvanced' script is enabled.", "[fix] MZY Settings: 'Mark maxed skills' option was missing." ], "2.35": ["[new] 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": ["[new] (Experimental) Transfer: add deadline alert."], "2.33": ["[fix] Federation Front Page: add top players when current federation is changed."], "2.32": ["[improve] Federation: first team member sort"], "2.31": ["[new] Clash: add average age of top players and teams senior league for each team. this feature is not supported in mobile view."], "2.30": ["[fix] Transfer Filters: reset selected H & L checkboxes when Transfer filter is not enabled."], "2.29": ["[fix] Hide Players: fixed an issue about hide icon when transfer scout filters are used."], "2.28": ["[fix] Days at this club: after v2.27, it was broken in players page."], "2.27": ["[new] Transfer Market: it adds a trash icon next to player ID in search result. click on the icon to hide the player. To remove players from hide list, use 'MZY Hide' button."], "2.26": [ "[improve] Days at this club: it is optional. It is disabled by default. You can enable it from MZY Settings.", "[improve] Player Profile: it stores player profiles in local database to reduce number of requests.", "[improve] Local Database: it deletes outdated local data to reduce the size of database.", "[improve] Transfer: it uses less ajax requests now.", ], "2.25": ["[new] Training Report: click on player's camp package icon to open its camp report."], "2.24": ["[fix] Player Profile: fix Days at this club for injured or suspended players."], "2.23": [ "[new] Squad Profile: add 'days at this club' to each player profile.", "[fix] Player Comment: show comment icon for players when selected tab changes.", "[fix] 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": ["[new] Hire Coaches: adds salary range of each coach. Thanks to @douglaskampl for suggesting the idea and sharing his implementation."], "2.21": ["[new] Club Page: adds total trophy count."], "2.20": ["[new] Player Profile: add 'Days at this club' counter."], "2.19": ["[new] Squad Summary: it marks players whose skills are shared. click on share icon to see the player in place.", "[new] Squad Summary: it marks players that are in transfer market. click on transfer icon to see the player in market."], "2.18": ["[new] show changelog after script update.", "[improve] change icon style of player's comment."], "2.17": ["[fix] 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); } /* *********************** 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 = `${title} `; 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 = ` `; 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 = ` `; 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 = `${filterName}`; 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 = `${filterHitsIsValid(scoutHits) ? scoutHits.toString() : "n/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 = `${filterHitsIsValid(scoutHits) ? scoutHits.toString() : "n/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) { 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; if (ownView) { return; } const text = document.createElement("div"); text.innerHTML = `MZY: fetching players' info ...`; table.parentNode.insertBefore(text, table); const teamId = extractTeamId(document.baseURI); const playersInfo = await fetchPlayersProfileSummary(teamId); if (!playersInfo) { text.innerHTML = `MZY: fetching players' info ... failed.`; return; } text.innerHTML = `MZY: fetching players' info ... done.`; squadAddIconsHeaderToSummaryTable(table); 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: ${text}`; daysDiv.classList.add("mazyar-days-at-this-club"); } else if (mazyar.isDaysAtThisClubEnabledForOneClubPlayers()) { const text = 'Entire Career'; daysDiv.innerHTML = `Days at this club: ${text}`; 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 = `T`; 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 = `${title}${value}`; 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); } } } /* *********************** 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 ? `${formatAverageAge(info.averageAge)}` : '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 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 = `Team: ${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 = `Top${sport === "soccer" ? 11 : 21}: ${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 = `Total: ${formatBigNumber(totalValue, ",")} ${members[0].currency}`; 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` + `
Value: ${formatBigNumber(value, ",")} ${currency}` + `
Average Age: ${formatAverageAge(averageAge)}` + `

Note: YYY` + ` are ${sport === "soccer" ? "starting " : "current"} players and ` + `NNN are ex-players.` + `
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 = `` + `` + `` + ``; valueElement.style.textAlign = "right"; const ageElement = view.querySelector("td.age-value"); // prettier-ignore ageElement.innerHTML = `` + `` + `` + ``; 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 = `Note: Some teams have similar names. Coloring will not work as expected.`; 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 += ` ${team.name} [Rank: ${team.rank ?? 'xx'}] [Value: ${formatBigNumber(team.value)} ${team.currency}] `; } 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('H4'); } if (options.H3) { texts.push('H3'); } if (options.L2) { texts.push('L2'); } if (options.L1) { texts.push('L1'); } return texts.join(", "); } #getSelectedScoutsOptionTextForFilter(scout) { const texts = []; if (scout.high.includes(4)) { texts.push('H4'); } if (scout.high.includes(3)) { texts.push('H3'); } if (scout.low.includes(2)) { texts.push('L2'); } if (scout.low.includes(1)) { texts.push('L1'); } 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: ${text}`; if (addPrice && profile?.price !== null) { daysDiv.innerHTML += ` Fee: ${profile?.price}`; } daysDiv.classList.add("mazyar-days-at-this-club"); } else if (this.isDaysAtThisClubEnabledForOneClubPlayers()) { const text = 'Entire Career'; daysDiv.innerHTML = `Days at this club: ${text}`; 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.
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(` 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 = `

There are ${await this.#countPlayersOfHideListInIndexDb()} player(s) hidden from transfer market.

You can remove players from hide list.

Enter the number of days to remove players hidden before this period.
Enter 0 to remove all.

`; 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 0 player(s) from hide list.

` 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 = `PlayerDeadline`; 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 = `${bid.name}`; name.style.paddingRight = "15px"; const deadline = document.createElement("td"); deadline.innerHTML = `${bid.deadline} 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 = `Filter Name: ${data.name}
`; scoutSpan.innerHTML = `Selected: ${this.#getSelectedScoutsOptionTextForFilter(data.scout)}
`; countSpan.innerHTML = `Hit Count: ${data.count}`; 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 = "

No Players To Display

Please refresh the filter to update hits."; 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 = `Mazyar is updated
` + `from v${previousVersion} to v${currentVersion}`; const head = document.createElement("div"); head.innerHTML = headHTML; head.style.textAlign = "center"; const changesTitle = document.createElement("div"); changesTitle.innerHTML = `Changelog:`; 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 += `
v${v}
  • ` + changelogs[v]?.join("
  • ") + "
"; } } 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(); } } if (document.readyState === "loading") { // Loading hasn't finished yet document.addEventListener("DOMContentLoaded", () => { inject(); }); } else { // `DOMContentLoaded` has already fired inject(); } })();