您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Searches for players who match min. requirements (NC/NCA only)
// ==UserScript== // @name MZ - NT Player Search // @namespace douglaskampl // @version 4.02 // @description Searches for players who match min. requirements (NC/NCA only) // @author Douglas Vieira // @match https://www.managerzone.com/?p=national_teams&type=senior // @match https://www.managerzone.com/?p=national_teams&type=u21 // @icon https://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @connect mzlive.eu // @connect api.github.com // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @run-at document-idle // @license MIT // ==/UserScript== (async function () { 'use strict'; const FONT_URL = 'https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap'; const STYLE = `.nt-search-fab{position:fixed;bottom:2rem;right:2rem;width:60px;height:60px;background:linear-gradient(135deg, #ff6e40, #ff5252, #448aff);border-radius:50%;box-shadow:0 4px 12px rgba(0,0,0,.3);z-index:9998;cursor:pointer;transition:all .3s cubic-bezier(0.4,0,0.2,1);display:flex;justify-content:center;align-items:center}.nt-search-fab:hover{transform:scale(1.1);box-shadow:0 6px 16px rgba(83,11,237,.4)}.nt-search-fab i{color:white;font-size:24px;transition:transform .3s}.nt-search-fab.loading i{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.nt-search-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) scale(.95);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;padding:2rem;border-radius:12px;box-shadow:0 8px 32px rgba(83,11,237,.3),0 4px 8px rgba(0,0,0,.2);z-index:9999;visibility:hidden;width:800px;max-width:99%;opacity:0;transition:all .3s cubic-bezier(0.4,0,0.2,1);border:1px solid rgba(138,43,226,.1)}.nt-search-container.visible{visibility:visible;opacity:1;transform:translate(-50%,-50%) scale(1)}.nt-search-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem;padding-bottom:1rem;border-bottom:1px solid rgba(138,43,226,.2)}.nt-search-header h2{font-family:'Space Mono',monospace;margin:0;color:violet;font-size:1.5rem;text-shadow:0 0 10px rgba(138,43,226,.5)}.nt-search-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1rem;margin-bottom:1.5rem}.nt-search-field{display:flex;flex-direction:column;gap:.5rem}.nt-search-field label{color:#ff9966;font-size:.875rem;text-transform:uppercase;letter-spacing:1px}.nt-search-field select{padding:.75rem;border:1px solid rgba(138,43,226,.3);border-radius:8px;background:#1a1a2e;color:#f0f0f0;font-size:1rem;transition:all .2s;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ff9966' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;background-size:1rem}.nt-search-field select:focus{outline:none;border-color:#ff9966;box-shadow:0 0 0 2px rgba(138,43,226,.2)}.nt-search-field select:disabled{opacity:0.5;cursor:not-allowed;background:#333}.nt-search-buttons{display:flex;justify-content:center;align-items:center;gap:1rem;margin-top:1rem}.nt-search-button{width:auto;max-width:300px;padding:0.5rem 1rem;background:#009b3a;color:#ffdf00;border:none;border-radius:8px;font-weight:500;font-size:0.9rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:2px;box-shadow:0 4px 6px rgba(0,0,0,.1)}.nt-search-button:not(:disabled):hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-button:disabled{opacity:0.5;cursor:not-allowed;background:#666}.nt-search-log{margin-top:1rem;padding:1rem;background:rgba(26,26,46,.3);border-radius:8px;font-family:monospace;font-size:.875rem;max-height:150px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-log::-webkit-scrollbar{width:8px;height:8px}.nt-search-log::-webkit-scrollbar-track{background:#1a1a2e;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-log::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-search-log-entry{margin-bottom:.5rem;padding:.5rem;background:rgba(26,26,46,.5);border-radius:4px;color:#00ffff;animation:slideIn 0.3s ease-out forwards;opacity:0;transform:translateX(-20px)}@keyframes slideIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}.nt-search-guestbook-link{position:fixed;bottom:1rem;right:1rem;color:#ff9966;transition:all .2s}.nt-search-guestbook-link:hover{color:#6366f1;transform:scale(1.1)}.nt-search-country-select{width:200px}.nt-search-country-select select{width:100%}.nt-search-results-button{width:auto;max-width:300px;padding:0.5rem 1rem;background:#009b3a;color:#ffdf00;border:none;border-radius:8px;font-weight:500;font-size:0.9rem;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:2px;box-shadow:0 4px 6px rgba(0,0,0,.1);display:none}.nt-search-results-button:hover{transform:translateY(-2px);box-shadow:0 6px 8px rgba(0,0,0,.2)}.nt-search-results-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;padding:0;border-radius:12px;z-index:10001;width:90%;height:90vh;overflow:hidden;box-shadow:0 8px 32px rgba(83,11,237,.3);animation:modalSlideIn 0.3s ease-out forwards}@keyframes modalSlideIn{from{opacity:0;transform:translate(-50%,-48%)}to{opacity:1;transform:translate(-50%,-50%)}}.nt-search-results-header{position:sticky;top:0;display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:inherit;border-bottom:1px solid rgba(138,43,226,.2);z-index:1}.nt-search-results-title{font-family:'Space Mono',monospace;margin:0;font-size:1.5rem;color:#fff;text-shadow:0 0 10px rgba(138,43,226,.5)}.nt-search-results-close{background:none;border:none;color:#ff9966;font-size:1.5rem;cursor:pointer;transition:all 0.2s;padding:0.5rem}.nt-search-results-close:hover{color:#6366f1;transform:scale(1.1)}.nt-search-results-content{padding:1.5rem;height:calc(90vh - 5rem);overflow-y:auto;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-results-content::-webkit-scrollbar{width:8px}.nt-search-results-content::-webkit-scrollbar-track{background:#1a1a2e}.nt-search-results-content::-webkit-scrollbar-thumb{background:#6366f1;border-radius:4px}.nt-search-results-content::-webkit-scrollbar-thumb:hover{background:#4834d4}.nt-search-players-container{display:flex;flex-wrap:wrap;gap:1.5rem;margin:1rem 0}.nt-search-player-card{display:flex;flex-direction:row;gap:1.5rem;background:rgba(26,26,46,.5);border-radius:8px;padding:1.5rem;transition:all .2s;border:1px solid rgba(138,43,226,.1);flex:1 1 calc(50% - 1.5rem);box-sizing:border-box;min-width:500px}.nt-search-player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(83,11,237,.2)}.nt-search-player-summary{display:flex;flex-direction:column;flex-basis:45%;gap:.75rem}.nt-search-player-name{font-size:1.2rem;font-weight:bold;color:#fff;margin:0}.nt-search-player-name a{color:inherit;text-decoration:none}.nt-search-player-name a:hover{color:violet}.nt-search-player-details{display:flex;flex-direction:column;gap:.5rem;color:#ccc;font-size:0.875rem;margin-top:auto}.nt-search-player-details a{color:#ff9966;text-decoration:none;transition:color .2s ease-in-out}.nt-search-player-details a:hover{color:#fff;text-decoration:underline}.nt-search-skills-list{display:flex;flex-direction:column;gap:4px;flex-basis:55%}.nt-search-skill-row{display:flex;align-items:center;background:transparent;padding:0;box-shadow:none;min-height:24px}.nt-search-skill-name{font-size:.8rem;color:#f0f0f0;flex-basis:80px;margin-right:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nt-search-skill-value{display:flex;align-items:center;gap:4px;color:#ff9966;flex-shrink:0}.nt-search-skill-value img{height:.9em;width:auto;vertical-align:middle}.nt-search-skill-value-text{font-size:.8rem;white-space:nowrap}.nt-search-player-total-balls{font-weight:bold;color:#ffdf00;font-size:1rem}.nt-search-results-pagination{display:flex;justify-content:center;align-items:center;gap:1rem;padding:1rem 0;border-top:1px solid rgba(138,43,226,.1);border-bottom:1px solid rgba(138,43,226,.1);margin:0 -1.5rem 1rem -1.5rem}.nt-search-results-pagination.bottom{border-top:1px solid rgba(138,43,226,.1);border-bottom:none;margin-top:1rem;margin-bottom:0}.nt-search-results-pagination.top{border-bottom:1px solid rgba(138,43,226,.1);border-top:none;margin-bottom:1rem;margin-top:0}.nt-search-pagination-button{background:#1a1a2e;color:#f0f0f0;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem 1rem;cursor:pointer;transition:all 0.2s}.nt-search-pagination-button:not(:disabled):hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-pagination-button:disabled{opacity:0.5;cursor:not-allowed}.nt-search-pagination-info{color:#ff9966;font-size:0.875rem}.nt-search-header-controls{display:flex;align-items:center;gap:1rem;position:relative}.nt-search-export-button-group .nt-search-export-button{background:#1a1a2e;color:#f0f0f0;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem 1rem;cursor:pointer;transition:all 0.2s}.nt-search-export-button-group .nt-search-export-button:hover{background:#2a2a4e;transform:translateY(-1px)}.nt-search-export-options{display:none;position:absolute;top:100%;right:0;background:#1a1a2e;border:1px solid rgba(138,43,226,.3);border-radius:4px;padding:0.5rem;z-index:10;box-shadow:0 4px 8px rgba(0,0,0,.2);flex-direction:column;gap:0.5rem;width:200px}.nt-search-export-options.show{display:flex}.nt-search-export-options button{width:100%;text-align:left;background:transparent;color:#f0f0f0;border:none;padding:0.5rem 0.75rem;border-radius:4px;cursor:pointer}.nt-search-export-options button:hover{background:rgba(138,43,226,.2)}.nt-search-history-controls{display:flex;gap:1rem;}.nt-search-history-button{padding:0.5rem 1rem;background-color:transparent;color:#ff9966;border:1px solid #ff9966;border-radius:8px;cursor:pointer;transition:all .2s}.nt-search-history-button:hover{background-color:rgba(255,153,102,0.1);color:white}.nt-search-history-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:linear-gradient(135deg,#0a0a0a 0%,#1a1a2e 100%);color:#f0f0f0;border-radius:12px;z-index:10001;width:90%;max-width:1000px;height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(83,11,237,.3);animation:modalSlideIn .3s ease-out forwards;border:1px solid rgba(138,43,226,.1)}.nt-search-history-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.5rem;border-bottom:1px solid rgba(138,43,226,.2);flex-shrink:0}.nt-search-history-title{font-family:'Space Mono',monospace;margin:0;font-size:1.5rem;color:#fff;text-shadow:0 0 10px rgba(138,43,226,.5)}.nt-search-history-header-actions{display:flex;align-items:center;gap:1rem}.nt-search-history-content{padding:1.5rem;overflow-y:auto;flex-grow:1;scrollbar-width:thin;scrollbar-color:#6366f1 #1a1a2e}.nt-search-history-content::-webkit-scrollbar{width:8px}.nt-search-history-content::-webkit-scrollbar-track{background:#1a1a2e}.nt-search-history-content::-webkit-scrollbar-thumb{background:#6366f1}.nt-search-history-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1rem}.nt-search-history-item{background:rgba(26,26,46,.5);border:1px solid rgba(138,43,226,.1);border-radius:8px;padding:1rem;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s ease}.nt-search-history-item:hover{background:rgba(26,26,46,.8)}.nt-search-history-item-info{display:flex;flex-direction:column;gap:.5rem;max-width:70%}.nt-search-history-item-timestamp{font-family:'Space Mono',monospace;color:#ff9966;font-size:.875rem}.nt-search-history-item-filters{font-size:.8rem;color:#ccc;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nt-search-history-item-actions{display:flex;gap:.5rem}.nt-search-history-item-actions .nt-search-button{padding:.4rem .8rem;font-size:.8rem;letter-spacing:1px;background:#1a1a2e;color:#f0f0f0;border:1px solid #6366f1}.nt-search-history-item-actions .nt-search-button.delete{border-color:#ff5252}.nt-search-history-empty{text-align:center;padding:4rem 0;color:#888;font-style:italic}`; const COUNTRIES_DATA = [{"code":"AL","name":"Albania","cid":45,"u21ntid":1043509,"ntid":855930},{"code":"DZ","name":"Algeria","cid":52,"u21ntid":1043510,"ntid":855937},{"code":"AD","name":"Andorra","cid":46,"u21ntid":1043511,"ntid":855931},{"code":"AO","name":"Angola","cid":64,"u21ntid":1043512,"ntid":855949},{"code":"AR","name":"Argentina","cid":240,"u21ntid":1043513,"ntid":100},{"code":"AU","name":"Australia","cid":246,"u21ntid":1043514,"ntid":101},{"code":"AT","name":"Austria","cid":242,"u21ntid":1043515,"ntid":102},{"code":"AZ","name":"Azerbaijan","cid":54,"u21ntid":1043516,"ntid":855939},{"code":"BD","name":"Bangladesh","cid":67,"u21ntid":1043517,"ntid":855952},{"code":"BY","name":"Belarus","cid":34,"u21ntid":1043518,"ntid":855919},{"code":"BE","name":"Belgium","cid":237,"u21ntid":1043519,"ntid":103},{"code":"BO","name":"Bolivia","cid":6,"u21ntid":1043520,"ntid":498680},{"code":"BA","name":"Bosnia and Herzegovina","cid":25,"u21ntid":1043521,"ntid":855910},{"code":"BR","name":"Brazil","cid":1,"u21ntid":1043522,"ntid":104},{"code":"BG","name":"Bulgaria","cid":11,"u21ntid":1043523,"ntid":498686},{"code":"CA","name":"Canada","cid":239,"u21ntid":1043524,"ntid":105},{"code":"CL","name":"Chile","cid":15,"u21ntid":1043525,"ntid":498690},{"code":"CN","name":"China","cid":21,"u21ntid":1043526,"ntid":768059},{"code":"CO","name":"Colombia","cid":17,"u21ntid":1043527,"ntid":498693},{"code":"CR","name":"Costa Rica","cid":37,"u21ntid":1043528,"ntid":855922},{"code":"HR","name":"Croatia","cid":24,"u21ntid":1043529,"ntid":61},{"code":"CY","name":"Cyprus","cid":32,"u21ntid":1043530,"ntid":855917},{"code":"CZ","name":"Czech Republic","cid":3,"u21ntid":1043531,"ntid":106},{"code":"DK","name":"Denmark","cid":231,"u21ntid":1043532,"ntid":107},{"code":"DO","name":"Dominican Republic","cid":49,"u21ntid":1043533,"ntid":855934},{"code":"EC","name":"Ecuador","cid":13,"u21ntid":1043534,"ntid":498688},{"code":"EG","name":"Egypt","cid":12,"u21ntid":1043535,"ntid":498687},{"code":"SV","name":"El Salvador","cid":55,"u21ntid":1043536,"ntid":855940},{"code":"EN","name":"England","cid":224,"u21ntid":1043537,"ntid":108},{"code":"EE","name":"Estonia","cid":252,"u21ntid":1043538,"ntid":109},{"code":"FO","name":"Faroe Islands","cid":31,"u21ntid":1043539,"ntid":855916},{"code":"FI","name":"Finland","cid":229,"u21ntid":1043540,"ntid":110},{"code":"FR","name":"France","cid":228,"u21ntid":1043541,"ntid":111},{"code":"GE","name":"Georgia","cid":58,"u21ntid":1043543,"ntid":855943},{"code":"DE","name":"Germany","cid":230,"u21ntid":1043544,"ntid":112},{"code":"GR","name":"Greece","cid":232,"u21ntid":1043545,"ntid":113},{"code":"GT","name":"Guatemala","cid":44,"u21ntid":1043546,"ntid":855929},{"code":"HN","name":"Honduras","cid":57,"u21ntid":1043547,"ntid":855942},{"code":"HU","name":"Hungary","cid":245,"u21ntid":1043548,"ntid":114},{"code":"IS","name":"Iceland","cid":10,"u21ntid":1043549,"ntid":498685},{"code":"IN","name":"India","cid":39,"u21ntid":1043550,"ntid":855924},{"code":"ID","name":"Indonesia","cid":251,"u21ntid":1043551,"ntid":117},{"code":"IR","name":"Iran","cid":41,"u21ntid":1043552,"ntid":855926},{"code":"IE","name":"Ireland","cid":249,"u21ntid":1043553,"ntid":115},{"code":"IL","name":"Israel","cid":2,"u21ntid":1043554,"ntid":116},{"code":"IT","name":"Italy","cid":226,"u21ntid":1043555,"ntid":118},{"code":"JO","name":"Jordan","cid":59,"u21ntid":1043556,"ntid":855944},{"code":"KZ","name":"Kazakhstan","cid":51,"u21ntid":1043557,"ntid":855936},{"code":"KE","name":"Kenya","cid":62,"u21ntid":1043558,"ntid":855947},{"code":"KW","name":"Kuwait","cid":61,"u21ntid":1043559,"ntid":855946},{"code":"KG","name":"Kyrgyzstan","cid":53,"u21ntid":1043560,"ntid":855938},{"code":"LV","name":"Latvia","cid":250,"u21ntid":1043561,"ntid":119},{"code":"LB","name":"Lebanon","cid":68,"u21ntid":1043562,"ntid":855953},{"code":"LI","name":"Liechtenstein","cid":42,"u21ntid":1043563,"ntid":855927},{"code":"LT","name":"Lithuania","cid":5,"u21ntid":1043564,"ntid":120},{"code":"LU","name":"Luxembourg","cid":38,"u21ntid":1043565,"ntid":855923},{"code":"DC","name":"MZ Country","cid":20,"u21ntid":1043572,"ntid":768061},{"code":"MK","name":"Macedonia","cid":33,"u21ntid":1043542,"ntid":855918},{"code":"MY","name":"Malaysia","cid":27,"u21ntid":1043566,"ntid":855912},{"code":"MT","name":"Malta","cid":30,"u21ntid":1043567,"ntid":855915},{"code":"MX","name":"Mexico","cid":4,"u21ntid":1043568,"ntid":121},{"code":"MD","name":"Moldova","cid":35,"u21ntid":1043569,"ntid":855920},{"code":"ME","name":"Montenegro","cid":70,"u21ntid":1043570,"ntid":855955},{"code":"MA","name":"Morocco","cid":43,"u21ntid":1043571,"ntid":855928},{"code":"NL","name":"Netherlands","cid":236,"u21ntid":1043573,"ntid":122},{"code":"NG","name":"Nigeria","cid":63,"u21ntid":1043574,"ntid":855948},{"code":"NI","name":"Northern Ireland","cid":69,"u21ntid":1043575,"ntid":855954},{"code":"NO","name":"Norway","cid":234,"u21ntid":1043576,"ntid":123},{"code":"PK","name":"Pakistan","cid":56,"u21ntid":1043577,"ntid":855941},{"code":"PA","name":"Panama","cid":29,"u21ntid":1043578,"ntid":855914},{"code":"PY","name":"Paraguay","cid":7,"u21ntid":1043579,"ntid":498681},{"code":"PE","name":"Peru","cid":16,"u21ntid":1043580,"ntid":498691},{"code":"PH","name":"Philippines","cid":40,"u21ntid":1043581,"ntid":855925},{"code":"PL","name":"Poland","cid":233,"u21ntid":1043582,"ntid":124},{"code":"PT","name":"Portugal","cid":243,"u21ntid":1043583,"ntid":125},{"code":"RO","name":"Romania","cid":247,"u21ntid":1043584,"ntid":126},{"code":"RU","name":"Russia","cid":9,"u21ntid":1043585,"ntid":498684},{"code":"SA","name":"Saudi Arabia","cid":60,"u21ntid":1043586,"ntid":855945},{"code":"SC","name":"Scotland","cid":254,"u21ntid":1043587,"ntid":127},{"code":"SN","name":"Senegal","cid":66,"u21ntid":1043588,"ntid":855951},{"code":"RS","name":"Serbia","cid":71,"u21ntid":1043589,"ntid":855956},{"code":"SG","name":"Singapore","cid":28,"u21ntid":1043590,"ntid":855913},{"code":"SK","name":"Slovakia","cid":23,"u21ntid":1043591,"ntid":855909},{"code":"SI","name":"Slovenia","cid":22,"u21ntid":1043592,"ntid":855807},{"code":"ZA","name":"South Africa","cid":18,"u21ntid":1043593,"ntid":498694},{"code":"KR","name":"South Korea","cid":48,"u21ntid":1043594,"ntid":855933},{"code":"ES","name":"Spain","cid":227,"u21ntid":1043595,"ntid":128},{"code":"SE","name":"Sweden","cid":205,"u21ntid":1043596,"ntid":129},{"code":"CH","name":"Switzerland","cid":235,"u21ntid":1043597,"ntid":130},{"code":"TH","name":"Thailand","cid":19,"u21ntid":1043598,"ntid":498695},{"code":"TT","name":"Trinidad & Tobago","cid":65,"u21ntid":1043599,"ntid":855950},{"code":"TN","name":"Tunisia","cid":50,"u21ntid":1043600,"ntid":855935},{"code":"TR","name":"Turkey","cid":253,"u21ntid":1043601,"ntid":131},{"code":"UA","name":"Ukraine","cid":26,"u21ntid":1043602,"ntid":855911},{"code":"AE","name":"United Arab Emirates","cid":47,"u21ntid":1043603,"ntid":855932},{"code":"US","name":"United States","cid":225,"u21ntid":1043604,"ntid":132},{"code":"UY","name":"Uruguay","cid":8,"u21ntid":1043605,"ntid":498683},{"code":"VE","name":"Venezuela","cid":14,"u21ntid":1043606,"ntid":498689},{"code":"VN","name":"Vietnam","cid":36,"u21ntid":1043607,"ntid":855921},{"code":"WL","name":"Wales","cid":248,"u21ntid":1043608,"ntid":133}]; const MASSIVE_COUNTRIES = ['BR', 'CN', 'AR', 'SE', 'PL', 'TR']; const PLAYERS_PER_PAGE = 10; const ORDERED_SKILL_KEYS = [ "speed", "stamina", "playIntelligence", "passing", "shooting", "heading", "keeping", "ballControl", "tackling", "aerialPassing", "setPlays", "experience" ]; try { const response = await fetch(FONT_URL); if (response.ok) { const fontCss = await response.text(); GM_addStyle(fontCss + STYLE); } else { throw new Error(`Failed to fetch font CSS, status: ${response.status}`); } } catch (error) { GM_addStyle(STYLE); } class Logger { constructor(container, flushInterval = 400) { this.container = container; this.flushInterval = flushInterval; this.queue = []; this.timeout = null; this.scheduled = false; } getTimestamp() { const now = new Date(); return `[${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}]`; } log(message, type = 'info') { this.queue.push({ message: `${this.getTimestamp()} ${message}`, type }); if (!this.scheduled) { this.scheduled = true; this.timeout = setTimeout(() => this.flush(), this.flushInterval); } } flush() { if (!this.queue.length || !this.container) { this.scheduled = false; return; } const fragment = document.createDocumentFragment(); this.queue.forEach(({ message, type }) => { const entry = document.createElement('div'); entry.className = `nt-search-log-entry ${type}`; entry.textContent = message; fragment.appendChild(entry); }); this.container.appendChild(fragment); this.container.scrollTop = this.container.scrollHeight; this.queue = []; if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } this.scheduled = false; } } class HistoryManager { constructor(storageKey = 'MZ_NT_SEARCH_HISTORY') { this.storageKey = storageKey; } async getHistory() { const historyJson = await GM_getValue(this.storageKey, '[]'); return JSON.parse(historyJson); } async saveSearch(searchData) { let history = await this.getHistory(); history.unshift(searchData); await GM_setValue(this.storageKey, JSON.stringify(history)); } async deleteSearch(timestamp) { let history = await this.getHistory(); const newHistory = history.filter(entry => entry.timestamp !== timestamp); await GM_setValue(this.storageKey, JSON.stringify(newHistory)); } } class RequestQueue { constructor(maxConcurrent = 5, delay = 100) { this.queue = []; this.maxConcurrent = maxConcurrent; this.delay = delay; this.running = 0; this.processed = 0; } add(request) { return new Promise((resolve, reject) => { const wrappedRequest = async () => { try { await new Promise(res => setTimeout(res, this.delay)); const result = await request(); this.processed++; resolve(result); } catch (error) { reject(error); } finally { this.running--; this.processNext(); } }; this.queue.push(wrappedRequest); this.processNext(); }); } processNext() { while (this.running < this.maxConcurrent && this.queue.length > 0) { this.running++; const request = this.queue.shift(); request(); } } reset() { this.queue = []; this.running = 0; this.processed = 0; } } class ChunkProcessor { constructor(chunkSize = 25) { this.chunkSize = chunkSize; } async process(items, processFn, onChunkComplete) { const chunks = this.createChunks(items); let processed = 0; for (const chunk of chunks) { await Promise.all(chunk.map(processFn)); processed += chunk.length; if (onChunkComplete) { onChunkComplete(processed, items.length); } await new Promise(res => setTimeout(res, 50)); } } createChunks(items) { const chunks = []; for (let i = 0; i < items.length; i += this.chunkSize) { chunks.push(items.slice(i, i + this.chunkSize)); } return chunks; } } class NTPlayerParser { constructor(minRequirements) { this.minRequirements = minRequirements; this.logger = null; } parseSkills(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const rows = doc.querySelectorAll('.player_skills tr'); if (!rows.length) { return null; } const skills = {}; let totalBalls = 0; const totalBallsElement = doc.querySelector('td[title] span.bold'); if (totalBallsElement) { totalBalls = parseInt(totalBallsElement.textContent, 10) || 0; } let skillRows = Array.from(rows); if (skillRows.length > ORDERED_SKILL_KEYS.length) { skillRows = skillRows.slice(0, ORDERED_SKILL_KEYS.length); } skillRows.forEach((row, index) => { const valueCell = row.querySelector('.skillval'); if (!valueCell) { return; } const rawValue = valueCell.textContent.replace(/[()]/g, "").trim(); const value = parseInt(rawValue, 10); if (!isNaN(value)) { skills[ORDERED_SKILL_KEYS[index]] = value; } }); if (Object.keys(skills).length === 0) { return null; } ORDERED_SKILL_KEYS.forEach(key => { if (!(key in skills)) { skills[key] = 0; } }); if (!this.validateSkills(skills)) { return null; } return { skills, totalBalls }; } validateSkills(skills) { return Object.entries(this.minRequirements) .filter(([key]) => key in skills && typeof skills[key] === 'number') .every(([key, minValue]) => skills[key] >= minValue); } async fetchAndParsePlayer(playerId, ntid, cid) { const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=search&ntid=${ntid}&cid=${cid}&type=national_team&pid=${playerId}&sport=soccer`; try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const html = await response.text(); return this.parseSkills(html); } catch (error) { if (this.logger) { this.logger.log(`Error parsing player ${playerId}: ${error.message}`, 'error'); } return null; } } } class PlayerData { constructor(id, name, teamName, teamId, age, value, salary, totalBalls, skills) { this.id = id; this.name = name; this.teamName = teamName; this.teamId = teamId || null; this.age = age; this.value = value; this.salary = salary; this.totalBalls = totalBalls; this.skills = skills; } toJSON() { return { id: this.id, name: this.name, teamName: this.teamName, teamId: this.teamId, age: this.age, value: this.value, salary: this.salary, totalBalls: this.totalBalls, skills: this.skills, }; } toExcelRow() { const row = { 'ID': this.id, 'Name': this.name, 'Team': this.teamName, 'Age': this.age, 'Value': this.value, 'Salary': this.salary, 'Total Balls': this.totalBalls, }; ORDERED_SKILL_KEYS.forEach(key => { row[NTPlayerSearcher.prototype.formatSkillName(key)] = this.skills[key] || 0; }); return row; } } class NTPlayerSearcher { constructor() { this.requestQueue = new RequestQueue(5, 100); this.chunkProcessor = new ChunkProcessor(25); this.historyManager = new HistoryManager(); this.isInitialized = false; this.isInitializing = false; this.isSearching = false; this.searchValues = { speed: 0, stamina: 0, playIntelligence: 0, passing: 0, shooting: 0, heading: 0, keeping: 0, ballControl: 0, tackling: 0, aerialPassing: 0, setPlays: 0, experience: 0, minAge: 16, maxAge: 40, totalBalls: 9, country: '', countryData: null }; this.teamIds = new Set(); this.playerIds = new Map(); this.processedLeagues = 0; this.totalLeagues = 0; this.validPlayers = new Map(); this.floatingButton = null; this.logger = null; this.countries = []; this.userCountry = null; this.username = null; this.currentResultsPage = 1; this.resultsListeners = { prev: null, next: null, esc: null }; } async fetchTopPlayers(country, page = 0, isU21 = false) { try { const baseUrl = `https://mzlive.eu/mzlive.php?action=list&type=top100&mode=players&country=${country}&cy=EUR`; const url = isU21 ? `${baseUrl}&age=u21&page=${page}` : `${baseUrl}&page=${page}`; const response = await this.requestQueue.add(() => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, onload: res => resolve(res), onerror: err => reject(err), ontimeout: () => reject(new Error(`Timeout fetching Top100 page ${page}`)) }); }) ); const data = JSON.parse(response.responseText); const players = (data.players || []).filter(player => { return player.age >= this.searchValues.minAge && player.age <= this.searchValues.maxAge; }); const playerEntries = players.map(player => [ player.id.toString(), { id: player.id.toString(), name: player.name, teamName: player.team_name, teamId: player.team_id?.toString() || null, age: player.age, value: parseInt(player.value) || 0, salary: 0 } ]); this.playerIds = new Map([...this.playerIds, ...playerEntries]); return players.map(player => player.id.toString()); } catch (error) { if (this.logger) this.logger.log(`Error fetching Top100 (page ${page}): ${error.message}`, 'error'); return []; } } async fetchAllTop100Players(country) { const maxPages = MASSIVE_COUNTRIES.includes(country) ? 20 : 5; const isU21 = this.searchValues.maxAge <= 21; const pages = Array.from({ length: maxPages + 1 }, (_, i) => i); const chunkSize = 5; const results = []; if (this.logger) this.logger.log(`Fetching Top100 players...`); for (let i = 0; i < pages.length; i += chunkSize) { const chunk = pages.slice(i, i + chunkSize); const chunkResults = await Promise.all( chunk.map(page => this.fetchTopPlayers(country, page, isU21)) ); results.push(...chunkResults); await new Promise(res => setTimeout(res, 100)); } if (this.logger) this.logger.log(`Finished fetching Top100 players.`); return results.flat(); } async fetchCountriesList() { return Promise.resolve(COUNTRIES_DATA); } async fetchUserCountry() { const usernameElem = document.querySelector('#header-username'); if (!usernameElem) return { userCountry: null, username: null }; const username = usernameElem.textContent.trim(); try { const response = await fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${username}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) throw new Error(`XML parsing error: ${parserError.textContent}`); const countryCode = xmlDoc.querySelector('UserData')?.getAttribute('countryShortname') || null; return { userCountry: countryCode, username: username }; } catch (error) { if (this.logger) this.logger.log(`Error fetching user data: ${error.message}`, 'error'); return { userCountry: null, username: username }; } } async checkUserRole(ntid, cid, username) { if (!ntid || !cid || !username) { if (this.logger) this.logger.log("Missing ntid, cid, or username for role check.", "warn"); return false; } const url = `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=team&ntid=${ntid}&cid=${cid}&type=national_team&sport=soccer`; try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const profileLinks = doc.querySelectorAll('table.padding a[href*="/?p=profile&uid="]'); for (const link of profileLinks) { if (link.textContent.trim() === username) { if (this.logger) this.logger.log(`User confirmed as NC/NCA.`, 'info'); return true; } } if (this.logger) this.logger.log(`${username} is not NC/NCA.`, 'info'); return false; } catch (error) { if (this.logger) this.logger.log(`Error checking user role: ${error.message}`, 'error'); return false; } } async firstThingsFirst() { this.isInitializing = true; this.showLoading(); try { const [countries, { userCountry, username }] = await Promise.all([ this.fetchCountriesList(), this.fetchUserCountry() ]); this.countries = countries || []; this.userCountry = userCountry; this.username = username; let isAuthorized = false; let userCountryData = null; if (this.userCountry && this.username && this.countries.length > 0) { userCountryData = this.countries.find(c => c.code === this.userCountry); if (userCountryData) { const isU21Page = window.location.href.includes("type=u21"); const ntid = isU21Page ? userCountryData.u21ntid : userCountryData.ntid; isAuthorized = await this.checkUserRole(ntid, userCountryData.cid, this.username); } } const searchContainer = document.querySelector('.nt-search-container'); const countrySelect = searchContainer?.querySelector('select[name="country"]'); const searchButton = searchContainer?.querySelector('.nt-search-button'); const allSelects = searchContainer?.querySelectorAll('select'); if (isAuthorized && userCountryData) { this.searchValues.country = this.userCountry; this.searchValues.countryData = { ntid: userCountryData.ntid, u21ntid: userCountryData.u21ntid, cid: userCountryData.cid }; if (this.logger) this.logger.log(`Welcome, ${this.username}.`, 'info'); if (countrySelect) { countrySelect.innerHTML = this.generateCountryOptions(); countrySelect.disabled = false; } if (allSelects) allSelects.forEach(s => s.disabled = false); if (searchButton) searchButton.disabled = false; } else { if (this.logger) this.logger.log("Search tool disabled.", "warn"); if (allSelects) allSelects.forEach(s => s.disabled = true); if (searchButton) searchButton.disabled = true; if (countrySelect) { countrySelect.innerHTML = this.generateCountryOptions(); countrySelect.disabled = true; } } } catch (error) { if (this.logger) this.logger.log(`Initialization failed: ${error.message}`, 'error'); alert(`Initialization failed. Please check the browser console (F12) for more details.`); } finally { this.isInitialized = true; this.isInitializing = false; this.hideLoading(); if (this.logger) this.logger.flush(); } } initialize() { this.appendSearchTab(); this.setUpEvents(); } showLoading() { if (this.floatingButton) this.floatingButton.classList.add('loading'); } hideLoading() { if (this.floatingButton) this.floatingButton.classList.remove('loading'); } async getLeagueIds(countryCode) { try { const response = await this.requestQueue.add(() => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://mzlive.eu/mzlive.php?action=list&type=leagues&country=${countryCode}`, onload: res => resolve(res), onerror: err => reject(err), ontimeout: () => reject(new Error('Timeout fetching leagues')) }); }) ); const leagues = JSON.parse(response.responseText); const maxDivision = MASSIVE_COUNTRIES.includes(countryCode) ? 6 : 3; return leagues.filter(league => { const name = league.name.toLowerCase(); if (name.startsWith('div')) { const divLevel = parseInt(name.split('.')[0].replace('div', '')); return isNaN(divLevel) || divLevel <= maxDivision; } return true; }).map(league => league.id); } catch (error) { if (this.logger) this.logger.log(`Error fetching leagues: ${error.message}`, 'error'); return []; } } async getTeamIds(leagueId) { try { const response = await this.requestQueue.add(() => fetch(`https://www.managerzone.com/xml/team_league.php?sport_id=1&league_id=${leagueId}`)); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for league ${leagueId}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) throw new Error(`XML parsing error for league ${leagueId}: ${parserError.textContent}`); const teams = xmlDoc.getElementsByTagName('Team'); return Array.from(teams).map(team => team.getAttribute('teamId')); } catch (error) { if (this.logger) this.logger.log(`Error fetching teams for league ${leagueId}: ${error.message}`, 'error'); return []; } } async processLeagueBatch(leagueIds) { if (!leagueIds || leagueIds.length === 0) { if (this.logger) this.logger.log("No league IDs to process.", "warn"); return; } if (this.logger) this.logger.log(`Processing ${leagueIds.length} leagues...`); await this.chunkProcessor.process( leagueIds, async (leagueId) => { try { const teamIds = await this.getTeamIds(leagueId); if (teamIds && teamIds.length > 0) teamIds.forEach(id => this.teamIds.add(id)); this.processedLeagues++; } catch (error) { if (this.logger) this.logger.log(`Failed to process league ${leagueId}: ${error.message}`, 'error'); } } ); if (this.logger) this.logger.log(`Finished processing leagues. Found ${this.teamIds.size} unique teams.`); } async fetchPlayerList(teamId) { try { const response = await this.requestQueue.add(() => fetch(`https://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`)); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for team ${teamId}`); const text = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, "text/xml"); const parserError = xmlDoc.querySelector('parsererror'); if (parserError) throw new Error(`XML parsing error for team ${teamId}: ${parserError.textContent}`); const teamPlayers = xmlDoc.querySelector('TeamPlayers'); if (!teamPlayers) return; const teamName = teamPlayers.getAttribute('teamName') || `Team ${teamId}`; const actualTeamId = teamPlayers.getAttribute('teamId') || teamId; const players = xmlDoc.getElementsByTagName('Player'); const targetCountry = this.searchValues.country.toLowerCase(); Array.from(players).forEach(player => { const age = parseInt(player.getAttribute('age')); const countryCode = (player.getAttribute('countryShortname') || '').toLowerCase(); if (age >= this.searchValues.minAge && age <= this.searchValues.maxAge && countryCode === targetCountry) { const playerId = player.getAttribute('id'); const playerName = player.getAttribute('name'); const value = parseInt(player.getAttribute('value')) || 0; const salary = parseInt(player.getAttribute('salary')) || 0; if (playerId && playerName) { this.playerIds.set(playerId, { id: playerId, name: playerName, teamName, teamId: actualTeamId, age, value, salary }); } } }); } catch (error) { if (this.logger) this.logger.log(`Error fetching players for team ${teamId}: ${error.message}`, 'error'); } } async processTeamBatch(teamIds) { if (!teamIds || teamIds.length === 0) { if (this.logger) this.logger.log("No team IDs to process.", "warn"); return; } const totalTeams = teamIds.length; let processedTeams = 0; if (this.logger) this.logger.log(`Processing ${totalTeams} teams...`); await this.chunkProcessor.process( teamIds, async (teamId) => { await this.fetchPlayerList(teamId); processedTeams++; if (processedTeams % 100 === 0 || processedTeams === totalTeams) { if (this.logger) this.logger.log(`Team processing: ${processedTeams}/${totalTeams}`); } } ); if (this.logger) this.logger.log(`Finished processing teams. Found ${this.playerIds.size} potential players.`); } async searchForPlayers() { if (!this.searchValues.country || !this.searchValues.countryData) { if (this.logger) this.logger.log('Country not selected or country data missing.', 'error'); alert('Please ensure a country is selected.'); return; } this.teamIds = new Set(); this.playerIds = new Map(); this.processedLeagues = 0; this.totalLeagues = 0; this.validPlayers = new Map(); this.requestQueue.reset(); this.currentResultsPage = 1; const countryCode = this.searchValues.country; if (this.logger) this.logger.log(`Starting search for country: ${countryCode}`); try { if (this.searchValues.maxAge > 18) { await this.fetchAllTop100Players(countryCode); if (this.logger) this.logger.log(`Found ${this.playerIds.size} players from Top100.`); } const leagueIds = await this.getLeagueIds(countryCode); this.totalLeagues = leagueIds.length; if (this.totalLeagues === 0 && this.playerIds.size === 0) { if (this.logger) this.logger.log(`No leagues found and no top players matched. Stopping search.`, 'warn'); return; } await this.processLeagueBatch(leagueIds); await this.processTeamBatch(Array.from(this.teamIds)); const isU21Page = window.location.href.includes("type=u21"); const ntid = isU21Page ? this.searchValues.countryData.u21ntid : this.searchValues.countryData.ntid; const cid = this.searchValues.countryData.cid; const ntPlayerParser = new NTPlayerParser(this.searchValues); ntPlayerParser.logger = this.logger; const playerEntries = Array.from(this.playerIds.entries()); if (playerEntries.length === 0) { if (this.logger) this.logger.log('No potential players found after gathering IDs.', 'warn'); return; } if (this.logger) this.logger.log(`Processing skills for ${playerEntries.length} players...`); await this.chunkProcessor.process( playerEntries, async ([playerId, playerData]) => { try { const parsedData = await ntPlayerParser.fetchAndParsePlayer(playerId, ntid, cid); if (parsedData && parsedData.totalBalls >= this.searchValues.totalBalls) { this.validPlayers.set(playerId, new PlayerData(playerId, playerData.name, playerData.teamName, playerData.teamId, playerData.age, playerData.value, playerData.salary, parsedData.totalBalls, parsedData.skills)); } } catch (parseError) { if (this.logger) this.logger.log(`Error processing player ${playerId}: ${parseError.message}`, 'error'); } }, (processed, total) => { if (this.logger) { this.logger.log(`Processing player skills: ${processed}/${total}`); } } ); await new Promise(resolve => setTimeout(resolve, 200)); const finalCount = this.validPlayers.size; if (this.logger) this.logger.log(`Search complete: found ${finalCount} players matching all criteria.`); if (finalCount > 0) { const searchData = { timestamp: new Date().toISOString(), filters: JSON.parse(JSON.stringify(this.searchValues)), results: Array.from(this.validPlayers.values()).map(p => p.toJSON()) }; await this.historyManager.saveSearch(searchData); } const resultsButton = document.querySelector('.nt-search-results-button'); if (resultsButton) resultsButton.style.display = finalCount > 0 ? "inline-block" : "none"; return Array.from(this.validPlayers.keys()); } catch (error) { if (this.logger) this.logger.log(`Search error: ${error.message}`, 'error'); alert(`An error occurred during the search: ${error.message}. Check console for details.`); } finally { if (this.logger) this.logger.flush(); } } async performSearch() { if (this.isSearching) { if (this.logger) this.logger.log("Search already in progress.", "warn"); return; } if (!this.searchValues.country || !this.searchValues.countryData) { alert("Please select a country before searching."); return; } this.isSearching = true; const internalSearchButton = document.querySelector('.nt-search-container .nt-search-button'); if (internalSearchButton) internalSearchButton.disabled = true; const logContainer = document.querySelector('.nt-search-log'); const resultsButton = document.querySelector('.nt-search-results-button'); this.showLoading(); if (logContainer) logContainer.innerHTML = ''; if (resultsButton) resultsButton.style.display = 'none'; if (!this.logger || !this.logger.container) { const logCont = document.querySelector('.nt-search-log'); this.logger = logCont ? new Logger(logCont) : { log: () => {}, flush: () => {} }; } try { await this.searchForPlayers(); } catch (error) { if (this.logger) this.logger.log(`Unhandled search error: ${error.message}`, 'error'); alert(`An unexpected error occurred: ${error.message}. Check console for details.`); } finally { this.isSearching = false; if (internalSearchButton) internalSearchButton.disabled = false; this.hideLoading(); if (this.logger) this.logger.flush(); } } getFiltersAppliedText(filters = this.searchValues) { const applied = []; const countryName = this.countries.find(c => c.code === filters.country)?.name || filters.country; if (countryName) applied.push(`Country: ${countryName}`); applied.push(`Age: ${filters.minAge} - ${filters.maxAge}`); applied.push(`Min Total Balls: ${filters.totalBalls}`); ORDERED_SKILL_KEYS.forEach(skill => { if (filters[skill] > 0) { applied.push(`Min ${this.formatSkillName(skill)}: ${filters[skill]}`); } }); return applied.join('; '); } createPaginationControls(page, totalPages) { const container = document.createElement('div'); container.className = 'nt-search-results-pagination'; if (totalPages > 1) { const prevBtn = document.createElement('button'); prevBtn.className = 'nt-search-pagination-button'; prevBtn.textContent = 'Previous'; prevBtn.disabled = page === 1; prevBtn.dataset.action = "prev"; const pageInfo = document.createElement('span'); pageInfo.className = 'nt-search-pagination-info'; pageInfo.textContent = `Page ${page} of ${totalPages}`; const nextBtn = document.createElement('button'); nextBtn.className = 'nt-search-pagination-button'; nextBtn.textContent = 'Next'; nextBtn.disabled = page === totalPages; nextBtn.dataset.action = "next"; container.appendChild(prevBtn); container.appendChild(pageInfo); container.appendChild(nextBtn); } return container; } renderResultsPage(players, page) { const playersContainer = document.querySelector('.nt-search-players-container'); const paginationTopContainer = document.querySelector('.nt-search-results-pagination.top'); const paginationBottomContainer = document.querySelector('.nt-search-results-pagination.bottom'); const modalContent = document.querySelector('.nt-search-results-content'); if (!playersContainer || !paginationTopContainer || !paginationBottomContainer || !modalContent) return; this.currentResultsPage = page; playersContainer.textContent = ''; paginationTopContainer.textContent = ''; paginationBottomContainer.textContent = ''; const playersArray = players.sort((a, b) => b.totalBalls - a.totalBalls); const totalPages = Math.ceil(playersArray.length / PLAYERS_PER_PAGE); const startIndex = (page - 1) * PLAYERS_PER_PAGE; const pagePlayers = playersArray.slice(startIndex, startIndex + PLAYERS_PER_PAGE); this.removePaginationListeners(); this.resultsListeners.prev = () => { if (this.currentResultsPage > 1) { this.renderResultsPage(players, this.currentResultsPage - 1); if (modalContent) modalContent.scrollTop = 0; } }; this.resultsListeners.next = () => { if (this.currentResultsPage < totalPages) { this.renderResultsPage(players, this.currentResultsPage + 1); if (modalContent) modalContent.scrollTop = 0; } }; const topControls = this.createPaginationControls(page, totalPages); const bottomControls = this.createPaginationControls(page, totalPages); paginationTopContainer.appendChild(topControls); paginationBottomContainer.appendChild(bottomControls); this.addPaginationListeners(paginationTopContainer); this.addPaginationListeners(paginationBottomContainer); const fragment = document.createDocumentFragment(); pagePlayers.forEach(player => { let skillsHTML = ORDERED_SKILL_KEYS.map(skillKey => { const value = player.skills[skillKey] || 0; const skillName = this.formatSkillName(skillKey); return `<div class="nt-search-skill-row" title="${skillName}: ${value}"><span class="nt-search-skill-name">${skillName}</span><div class="nt-search-skill-value"><img src="/img/soccer/wlevel_${value}.gif" alt="${value}"><span class="nt-search-skill-value-text">(${value})</span></div></div>`; }).join(''); const skillsContainerHTML = `<div class="nt-search-skills-list">${skillsHTML}</div>`; const playerCard = document.createElement('div'); playerCard.className = 'nt-search-player-card'; playerCard.innerHTML = ` <div class="nt-search-player-summary"> <h3 class="nt-search-player-name"><a href="https://www.managerzone.com/?p=players&pid=${player.id}" target="_blank" title="View player profile (ID: ${player.id})">${player.name}</a></h3> <div class="nt-search-player-total-balls">Total Balls: <strong>${player.totalBalls}</strong></div> <div class="nt-search-player-details"> <div>Team: ${player.teamId ? `<a href="https://www.managerzone.com/?p=team&tid=${player.teamId}" target="_blank" title="View team profile">${player.teamName}</a>` : player.teamName}</div> <div>Age: ${player.age}</div> <div>Value: ${new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(player.value)}</div> </div> </div> ${skillsContainerHTML}`; fragment.appendChild(playerCard); }); playersContainer.appendChild(fragment); } addPaginationListeners(container) { const prevBtn = container.querySelector('[data-action="prev"]'); const nextBtn = container.querySelector('[data-action="next"]'); if (prevBtn && this.resultsListeners.prev) prevBtn.addEventListener('click', this.resultsListeners.prev); if (nextBtn && this.resultsListeners.next) nextBtn.addEventListener('click', this.resultsListeners.next); } removePaginationListeners() { const containers = document.querySelectorAll('.nt-search-results-pagination'); containers.forEach(container => { const prevBtn = container.querySelector('[data-action="prev"]'); const nextBtn = container.querySelector('[data-action="next"]'); if (prevBtn && this.resultsListeners.prev) prevBtn.removeEventListener('click', this.resultsListeners.prev); if (nextBtn && this.resultsListeners.next) nextBtn.removeEventListener('click', this.resultsListeners.next); }); } showResults(players, filters = this.searchValues) { if (players.length === 0) { alert("No valid players to display."); return; } const existingModal = document.querySelector('.nt-search-results-modal'); if (existingModal) existingModal.remove(); this.removePaginationListeners(); if (this.resultsListeners.esc) document.removeEventListener('keydown', this.resultsListeners.esc); const modal = document.createElement('div'); modal.className = 'nt-search-results-modal'; const modalHeader = document.createElement('div'); modalHeader.className = 'nt-search-results-header'; const headerControls = document.createElement('div'); headerControls.className = 'nt-search-header-controls'; const exportButtonGroup = document.createElement('div'); exportButtonGroup.className = 'nt-search-export-button-group'; const exportButton = document.createElement('button'); exportButton.className = 'nt-search-export-button'; exportButton.textContent = 'Export / Share'; exportButton.title = 'Export or share results'; const exportOptions = document.createElement('div'); exportOptions.className = 'nt-search-export-options'; exportOptions.innerHTML = ` <button data-export-type="excel">Export to Excel (.xlsx)</button> <button data-export-type="html">Save as Web Page (.html)</button> <button data-export-type="gist">Share as Link (Gist)</button>`; exportButtonGroup.appendChild(exportButton); exportButtonGroup.appendChild(exportOptions); const closeButton = document.createElement('button'); closeButton.className = 'nt-search-results-close'; closeButton.innerHTML = '×'; closeButton.title = 'Close Results (Esc)'; headerControls.appendChild(exportButtonGroup); headerControls.appendChild(closeButton); modalHeader.innerHTML = ` <div> <h2 class="nt-search-results-title">Search Results (${players.length})</h2> <div class="nt-search-results-filters" style="font-size: 0.8rem; color: #bbb; margin-top: 0.5rem; max-width: 700px; line-height: 1.4;"> <strong style="color: #ff9966;">Filters:</strong> ${this.getFiltersAppliedText(filters)} </div> </div>`; modalHeader.appendChild(headerControls); const modalContent = document.createElement('div'); modalContent.className = 'nt-search-results-content'; modalContent.innerHTML = ` <div class="nt-search-results-pagination top"></div> <div class="nt-search-players-container"></div> <div class="nt-search-results-pagination bottom"></div>`; modal.appendChild(modalHeader); modal.appendChild(modalContent); document.body.appendChild(modal); this.renderResultsPage(players, 1); const closeModal = () => { this.removePaginationListeners(); if (this.resultsListeners.esc) document.removeEventListener('keydown', this.resultsListeners.esc); modal.remove(); }; closeButton.addEventListener('click', closeModal); this.resultsListeners.esc = (e) => { if (e.key === 'Escape') closeModal(); }; document.addEventListener('keydown', this.resultsListeners.esc); exportButton.addEventListener('click', (e) => { e.stopPropagation(); exportOptions.classList.toggle('show'); }); document.addEventListener('click', () => exportOptions.classList.remove('show')); exportOptions.addEventListener('click', (e) => { const target = e.target.closest('button'); if (!target) return; const type = target.dataset.exportType; if (type === 'excel') this.exportToExcel(players); else if (type === 'html') this.exportToHtml(players, filters); else if (type === 'gist') this.shareAsGist(players, filters); exportOptions.classList.remove('show'); }); } formatSkillName(skill) { const names = { speed: 'Speed', stamina: 'Stamina', playIntelligence: 'Play Int', passing: 'Passing', shooting: 'Shooting', heading: 'Heading', keeping: 'Keeping', ballControl: 'Ball Ctrl', tackling: 'Tackling', aerialPassing: 'Aerial Pass', setPlays: 'Set Plays', experience: 'Experience' }; return names[skill] || skill.charAt(0).toUpperCase() + skill.slice(1); } exportToExcel(players) { if (players.length === 0) { alert("No players to export."); return; } try { const dataToExport = players.map(player => player instanceof PlayerData ? player.toExcelRow() : new PlayerData(player.id, player.name, player.teamName, player.teamId, player.age, player.value, player.salary, player.totalBalls, player.skills).toExcelRow()); if (dataToExport.length === 0) { alert("No data formatted for export."); return; } const worksheet = XLSX.utils.json_to_sheet(dataToExport); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Players"); const date = new Date().toISOString().slice(0, 10); const countryCode = this.searchValues.country || 'export'; XLSX.writeFile(workbook, `MZ_NT_Search_${countryCode}_${date}.xlsx`); if (this.logger) this.logger.log(`Exported ${dataToExport.length} players to Excel.`); } catch (error) { if (this.logger) this.logger.log(`Excel export failed: ${error.message}`, 'error'); alert(`Excel export failed: ${error.message}. Check console for details.`); } } async exportHistoryToExcel() { if (this.logger) this.logger.log('Exporting search history...'); const history = await this.historyManager.getHistory(); if (history.length === 0) { alert("No search history found to export."); if (this.logger) this.logger.log('No history found.', 'warn'); return; } try { const workbook = XLSX.utils.book_new(); history.forEach((searchEntry, index) => { const sheetName = new Date(searchEntry.timestamp).toISOString() .slice(0, 19).replace('T', '_').replace(/:/g, '-'); const players = searchEntry.results.map(p => new PlayerData(p.id, p.name, p.teamName, p.teamId, p.age, p.value, p.salary, p.totalBalls, p.skills)); const filtersText = this.getFiltersAppliedText(searchEntry.filters); const dataForSheet = players.map(p => p.toExcelRow()); const ws = XLSX.utils.json_to_sheet(dataForSheet, { skipHeader: false }); XLSX.utils.sheet_add_aoa(ws, [[`Filters: ${filtersText}`]], { origin: "A1" }); XLSX.utils.sheet_add_aoa(ws, [], { origin: "A2" }); const finalWs = XLSX.utils.json_to_sheet(dataForSheet); XLSX.utils.book_append_sheet(workbook, finalWs, sheetName.slice(0, 31)); }); const date = new Date().toISOString().slice(0, 10); XLSX.writeFile(workbook, `MZ_NT_Search_History_${date}.xlsx`); if (this.logger) this.logger.log(`Successfully exported ${history.length} searches to Excel.`); } catch (error) { if (this.logger) this.logger.log(`History export failed: ${error.message}`, 'error'); alert(`History export failed: ${error.message}.`); } } exportToHtml(players, filters) { const filtersText = this.getFiltersAppliedText(filters).replace(/;/g, '<br>'); const rows = players.map(p => { const skills = ORDERED_SKILL_KEYS.map(key => `<td>${p.skills[key] || 0}</td>`).join(''); return `<tr><td><a href="https://www.managerzone.com/?p=players&pid=${p.id}" target="_blank">${p.name}</a></td><td>${p.age}</td><td>${p.totalBalls}</td>${skills}</tr>`; }).join(''); const headerCells = ORDERED_SKILL_KEYS.map(key => `<th>${this.formatSkillName(key)}</th>`).join(''); const htmlContent = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>MZ Player Search Results</title><style>body{font-family:sans-serif;background-color:#1a1a2e;color:#f0f0f0;padding:2rem}h1,h2{color:violet}a{color:#ff9966}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{padding:8px 12px;border:1px solid #333}th{background-color:#2a2a4e}tr:nth-child(even){background-color:#222}</style></head><body><h1>MZ Player Search Results</h1><h2>Filters Applied:</h2><p>${filtersText}</p><table><thead><tr><th>Name</th><th>Age</th><th>Total Balls</th>${headerCells}</tr></thead><tbody>${rows}</tbody></table></body></html>`; const blob = new Blob([htmlContent], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const date = new Date().toISOString().slice(0, 10); const countryCode = this.searchValues.country || 'export'; a.href = url; a.download = `MZ_NT_Search_${countryCode}_${date}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); if (this.logger) this.logger.log(`Exported ${players.length} players to HTML.`); } async shareAsGist(players, filters) { const filtersText = this.getFiltersAppliedText(filters).replace(/; /g, '\n- '); const header = `| Name | Age | Total Balls | ${ORDERED_SKILL_KEYS.map(k => this.formatSkillName(k)).join(' | ')} |`; const separator = `|---|---|---|${ORDERED_SKILL_KEYS.map(() => '---').join('|')}|`; const rows = players.map(p => { const skills = ORDERED_SKILL_KEYS.map(key => p.skills[key] || 0).join(' | '); return `| [${p.name}](https://www.managerzone.com/?p=players&pid=${p.id}) | ${p.age} | ${p.totalBalls} | ${skills} |`; }).join('\n'); const markdownContent = `## MZ Player Search Results\n\n### Filters:\n- ${filtersText}\n\n${header}\n${separator}\n${rows}`; this.showLoading(); if (this.logger) this.logger.log('Creating Gist...'); try { const gistOnlyPat = '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'; const countryCode = filters.country || 'export'; const date = new Date().toISOString().slice(0, 10); const fileName = `MZ-Search-${countryCode}-${date}.md`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://api.github.com/gists', headers: { 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.v3+json', 'Authorization': `token ${gistOnlyPat}` }, data: JSON.stringify({ description: `MZ NT Player Search Results - ${new Date().toLocaleString()}`, public: true, files: { [fileName]: { content: markdownContent } } }), onload: res => resolve(res), onerror: err => reject(err), ontimeout: () => reject(new Error('Gist creation timed out.')) }); }); if (response.status >= 200 && response.status < 300) { const gist = JSON.parse(response.responseText); prompt('Shareable link:', gist.html_url); if (this.logger) this.logger.log('Gist created successfully.'); } else { let errorDetails = response.responseText; try { const errorJson = JSON.parse(response.responseText); errorDetails = `(${response.status}) ${errorJson.message || 'Unknown error'}. See documentation link for details.`; } catch (e) { errorDetails = `(${response.status}) ${response.statusText}`; } throw new Error(errorDetails); } } catch (error) { if (this.logger) this.logger.log(`Gist creation failed: ${error.message}`, 'error'); alert(`Gist creation failed. This may be due to an invalid token or GitHub's rate limits. Please try again later. Error: ${error.message}`); } finally { this.hideLoading(); } } appendSearchTab() { const existingFab = document.querySelector('.nt-search-fab'); if (existingFab) existingFab.remove(); const existingContainer = document.querySelector('.nt-search-container'); if (existingContainer) existingContainer.remove(); this.floatingButton = document.createElement('div'); this.floatingButton.className = 'nt-search-fab'; this.floatingButton.innerHTML = '<i class="fa fa-search"></i>'; this.floatingButton.title = 'Open NT Player Search'; document.body.appendChild(this.floatingButton); const searchContainer = document.createElement('div'); searchContainer.className = 'nt-search-container'; const goText = 'Search'; const skillsHTML = ORDERED_SKILL_KEYS.map(key => { const label = this.formatSkillName(key); return ` <div class="nt-search-field"> <label title="Minimum ${label}">${label}</label> <select name="${key}" title="Select minimum ${label}" disabled> ${this.generateOptions(10, 0, key)} </select> </div>`; }).join(''); searchContainer.innerHTML = ` <div class="nt-search-header"> <h2>NT Player Search</h2> <div class="nt-search-history-controls"> <button class="nt-search-history-button" title="View search history">History</button> <button class="nt-search-results-close" title="Close Panel (Esc)" style="font-size: 1.2rem; padding: 0.3rem 0.6rem;">×</button> </div> </div> <div class="nt-search-grid"> ${skillsHTML} <div class="nt-search-field"><label title="Minimum Total Balls">Total Balls</label><select name="totalBalls" title="Select minimum Total Balls" disabled>${this.generateOptions(110, 9, 'totalBalls')}</select></div> <div class="nt-search-field"><label title="Minimum Age">Min Age</label><select name="minAge" title="Select minimum Age" disabled>${this.generateOptions(96, 16, 'minAge')}</select></div> <div class="nt-search-field"><label title="Maximum Age">Max Age</label><select name="maxAge" title="Select maximum Age" disabled>${this.generateOptions(96, 16, 'maxAge')}</select></div> <div class="nt-search-country-select nt-search-field"><label>Country</label><select name="country" required title="Select country (only your country is enabled)" disabled><option value="" disabled selected>Loading countries...</option></select></div> </div> <div class="nt-search-buttons"> <button class="nt-search-button" title="Start searching" disabled>${goText}</button> <button class="nt-search-results-button" style="display: none;" title="Show found players">Show Results</button> </div> <div class="nt-search-log" title="Search process log"></div>`; document.body.appendChild(searchContainer); this.logger = new Logger(searchContainer.querySelector('.nt-search-log')); } generateCountryOptions() { if (!this.countries || this.countries.length === 0) return `<option value="" disabled selected>Error loading countries</option>`; const placeholder = `<option value="" disabled ${!this.userCountry ? 'selected' : ''}>Select your country</option>`; return placeholder + this.countries.sort((a, b) => a.name.localeCompare(b.name)).map(country => { const isUserCountry = country.code === this.userCountry; const displayName = country.name === 'Czech Republic' ? 'Czechia' : country.name === 'Macedonia' ? 'North Macedonia' : country.name; return `<option value="${country.code}" data-ntid="${country.ntid}" data-u21ntid="${country.u21ntid}" data-cid="${country.cid}" ${isUserCountry ? 'selected' : ''} ${!isUserCountry ? 'disabled' : ''}>${displayName}</option>`; }).join(''); } generateOptions(max, min = 0, name) { let optionsHTML = ''; const defaultValue = this.searchValues[name]; for (let i = min; i <= max; i++) { optionsHTML += `<option value="${i}" ${defaultValue === i ? 'selected' : ''}>${i}</option>`; } return optionsHTML; } handleSelectChange(e) { const select = e.target; const value = select.value; if (select.name === 'country') { const option = select.selectedOptions[0]; if (option && option.value) { this.searchValues.country = value; this.searchValues.countryData = { ntid: option.dataset.ntid, u21ntid: option.dataset.u21ntid, cid: option.dataset.cid }; if (this.logger) this.logger.log(`Country set to: ${option.textContent.trim()}`); } else { this.searchValues.country = ''; this.searchValues.countryData = null; if (this.logger) this.logger.log(`Country selection cleared.`, 'warn'); } } else { const numValue = parseInt(value); if (!isNaN(numValue)) { this.searchValues[select.name] = numValue; if (select.name === 'minAge' && numValue > this.searchValues.maxAge) { this.searchValues.maxAge = numValue; const maxAgeSelect = document.querySelector('select[name="maxAge"]'); if (maxAgeSelect) maxAgeSelect.value = numValue; } else if (select.name === 'maxAge' && numValue < this.searchValues.minAge) { this.searchValues.minAge = numValue; const minAgeSelect = document.querySelector('select[name="minAge"]'); if (minAgeSelect) minAgeSelect.value = numValue; } } } } async showHistoryModal() { const history = await this.historyManager.getHistory(); const existingModal = document.querySelector('.nt-search-history-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.className = 'nt-search-history-modal'; let listContentHTML; if (history.length === 0) { listContentHTML = '<div class="nt-search-history-empty">No search history found.</div>'; } else { listContentHTML = `<ul class="nt-search-history-list">` + history.map(entry => { const date = new Date(entry.timestamp); const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; const filtersText = this.getFiltersAppliedText(entry.filters); return ` <li class="nt-search-history-item" data-timestamp="${entry.timestamp}"> <div class="nt-search-history-item-info"> <span class="nt-search-history-item-timestamp">${formattedDate}</span> <span class="nt-search-history-item-filters" title="${filtersText}">${filtersText}</span> </div> <div class="nt-search-history-item-actions"> <button class="nt-search-button" data-action="view-history" title="View these results">View</button> <button class="nt-search-button delete" data-action="delete-history" title="Delete this entry">Delete</button> </div> </li> `; }).join('') + '</ul>'; } modal.innerHTML = ` <div class="nt-search-history-header"> <h2 class="nt-search-history-title">Search History</h2> <div class="nt-search-history-header-actions"> <button class="nt-search-history-button" id="history-export-btn" title="Export all saved searches to Excel">Export History</button> <button class="nt-search-results-close" title="Close History (Esc)">×</button> </div> </div> <div class="nt-search-history-content"> ${listContentHTML} </div> `; document.body.appendChild(modal); const closeModal = () => { modal.remove(); document.removeEventListener('keydown', keydownHandler); }; const keydownHandler = (e) => { if (e.key === 'Escape') closeModal(); }; modal.querySelector('.nt-search-results-close').addEventListener('click', closeModal); document.addEventListener('keydown', keydownHandler); modal.querySelector('#history-export-btn')?.addEventListener('click', () => { this.exportHistoryToExcel(); }); modal.querySelector('.nt-search-history-list')?.addEventListener('click', async (e) => { const target = e.target; const action = target.dataset.action; const item = target.closest('.nt-search-history-item'); if (!action || !item) return; const timestamp = item.dataset.timestamp; const historyEntry = history.find(entry => entry.timestamp === timestamp); if (action === 'view-history') { if (!historyEntry) return; const players = historyEntry.results.map(p => new PlayerData(p.id, p.name, p.teamName, p.teamId, p.age, p.value, p.salary, p.totalBalls, p.skills)); this.showResults(players, historyEntry.filters); closeModal(); } else if (action === 'delete-history') { await this.historyManager.deleteSearch(timestamp); item.remove(); if (modal.querySelectorAll('.nt-search-history-item').length === 0) { modal.querySelector('.nt-search-history-content').innerHTML = '<div class="nt-search-history-empty">No search history found.</div>'; } } }); } setUpEvents() { const searchContainer = document.querySelector('.nt-search-container'); if (!searchContainer || !this.floatingButton) return; const internalSearchButton = searchContainer.querySelector('.nt-search-button'); const resultsButton = searchContainer.querySelector('.nt-search-results-button'); const selects = searchContainer.querySelectorAll('select'); const closeButton = searchContainer.querySelector('.nt-search-results-close'); const historyButton = searchContainer.querySelector('.nt-search-history-button'); this.floatingButton.addEventListener('click', (e) => { e.preventDefault(); searchContainer.classList.toggle('visible'); if (searchContainer.classList.contains('visible') && !this.isInitialized && !this.isInitializing) { this.firstThingsFirst(); } }); if (closeButton) closeButton.addEventListener('click', () => searchContainer.classList.remove('visible')); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && searchContainer.classList.contains('visible') && !document.querySelector('.nt-search-history-modal') && !document.querySelector('.nt-search-results-modal')) { searchContainer.classList.remove('visible'); } }); selects.forEach(select => select.addEventListener('change', (e) => this.handleSelectChange(e))); if (historyButton) historyButton.addEventListener('click', () => this.showHistoryModal()); if (internalSearchButton) internalSearchButton.addEventListener('click', () => this.performSearch()); if (resultsButton) resultsButton.addEventListener('click', () => this.showResults(Array.from(this.validPlayers.values()))); } } try { const searcher = new NTPlayerSearcher(); if (document.readyState === 'interactive' || document.readyState === 'complete') { searcher.initialize(); } else { document.addEventListener('DOMContentLoaded', () => searcher.initialize()); } } catch (e) { alert("Failed to initialize NTPlayerSearch. Check the console for details."); } })();