MZ - NT Challenges

Sends NT challenges and notifies about incoming challenges

  1. // ==UserScript==
  2. // @name MZ - NT Challenges
  3. // @namespace douglaskampl
  4. // @version 2.0
  5. // @description Sends NT challenges and notifies about incoming challenges
  6. // @author Douglas
  7. // @match https://www.managerzone.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
  9. // @connect pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev
  10. // @connect www.managerzone.com
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_addStyle
  14. // @grant GM_getResourceText
  15. // @grant GM_xmlhttpRequest
  16. // @require https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js
  17. // @resource TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.css
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23.  
  24. const SCRIPT_PREFIX = '[NT-CHALLENGER]';
  25.  
  26. const dataManager = {
  27. cachedCountries: null,
  28. endpoints: {
  29. countries: 'https://pub-02de1c06eac643f992bb26daeae5c7a0.r2.dev/json/countries.json',
  30. userData: u => `https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${u}`,
  31. nationalTeam: (nt, cid, type) => `https://www.managerzone.com/ajax.php?p=nationalTeams&sub=team&ntid=${nt}&cid=${cid}&type=${type}&sport=soccer`,
  32. challenges: 'https://www.managerzone.com/ajax.php?p=nationalTeams&sub=challenge',
  33. createChallenge: 'https://www.managerzone.com/ajax.php?p=challenge&sub=handle-challenge&action=create'
  34. },
  35. storageKeys: {
  36. countriesCache: 'mz_nt_challenger_countries_cache',
  37. defaultCoached: 'mz_nt_default_coached_list',
  38. defaultUncoached: 'mz_nt_default_uncoached_list',
  39. exclusionList: 'mz_nt_challenge_exclusion_list'
  40. },
  41.  
  42. initializeDefaultLists() {
  43. if (!GM_getValue(this.storageKeys.defaultCoached, null)) {
  44. const defaultCoached = [
  45. "Albania", "Argentina", "Australia", "Austria", "Belgium", "Brazil",
  46. "Canada", "Chile", "China", "Colombia", "Croatia", "Denmark",
  47. "England", "Finland", "France", "Germany", "Greece", "Hungary",
  48. "Iceland", "Italy", "Mexico", "Netherlands", "Poland", "Portugal",
  49. "Russia", "Scotland", "Spain", "Sweden", "Switzerland", "Turkey",
  50. "United States", "Uruguay", "Wales"
  51. ];
  52. GM_setValue(this.storageKeys.defaultCoached, defaultCoached);
  53. }
  54. if (!GM_getValue(this.storageKeys.defaultUncoached, null)) {
  55. const defaultUncoached = [
  56. "Algeria", "Faroe Islands", "Iran", "Jordan", "Kenya",
  57. "Kuwait", "Northern Ireland", "Norway", "Pakistan",
  58. "Saudi Arabia", "Senegal", "Vietnam"
  59. ];
  60. GM_setValue(this.storageKeys.defaultUncoached, defaultUncoached);
  61. }
  62. },
  63.  
  64. async fetch(details) {
  65. return new Promise((resolve, reject) => {
  66. const requestDetails = {
  67. ...details,
  68. timeout: 20000,
  69. onload: response => resolve(response),
  70. onerror: (err) => {
  71. console.error(`${SCRIPT_PREFIX} GM_xmlhttpRequest error:`, err);
  72. reject(new Error('Network Error'));
  73. },
  74. ontimeout: () => {
  75. console.error(`${SCRIPT_PREFIX} GM_xmlhttpRequest timeout for URL: ${details.url}`);
  76. reject(new Error('Request Timed Out'));
  77. },
  78. };
  79. GM_xmlhttpRequest(requestDetails);
  80. });
  81. },
  82.  
  83. async getCountries(force = false) {
  84. if (force) {
  85. this.cachedCountries = null;
  86. GM_setValue(this.storageKeys.countriesCache, null);
  87. console.log(`${SCRIPT_PREFIX} Force refresh: Cleared countries cache.`);
  88. }
  89.  
  90. if (this.cachedCountries) {
  91. return this.cachedCountries;
  92. }
  93.  
  94. const persistentCache = GM_getValue(this.storageKeys.countriesCache, null);
  95. if (persistentCache) {
  96. this.cachedCountries = JSON.parse(persistentCache);
  97. return this.cachedCountries;
  98. }
  99.  
  100. try {
  101. const response = await this.fetch({
  102. method: 'GET',
  103. url: this.endpoints.countries
  104. });
  105. this.cachedCountries = JSON.parse(response.responseText);
  106. GM_setValue(this.storageKeys.countriesCache, response.responseText);
  107. return this.cachedCountries;
  108. } catch (error) {
  109. console.error(`${SCRIPT_PREFIX} Failed to fetch and parse countries list.`, error);
  110. return null;
  111. }
  112. },
  113.  
  114. async getTargetableNTs() {
  115. const allTeams = await this.getCountries();
  116. if (!allTeams) {
  117. throw new Error("Failed to fetch base country list.");
  118. }
  119.  
  120. const defaultExclusions = GM_getValue(this.storageKeys.defaultUncoached, []);
  121. const excludedTeams = GM_getValue(this.storageKeys.exclusionList, defaultExclusions);
  122.  
  123. const targetable = allTeams.filter(team => !excludedTeams.includes(team.name));
  124. return targetable;
  125. }
  126. };
  127.  
  128. const uiManager = {
  129. triggerId: 'mz-challenger',
  130. modalId: 'mz-master-modal',
  131. logPanelId: 'mz-master-log-panel',
  132. progressTrackerId: 'mz-master-progress-tracker',
  133. teamTypeSelectorName: 'team-type-selector',
  134.  
  135. injectStyles() {
  136. GM_addStyle(`
  137. @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
  138. :root {
  139. --mz-master-navy: #0D253F; --mz-master-black: #010101; --mz-master-pink: #EC407A;
  140. --mz-master-text: #E0E0E0; --mz-master-text-dark: #a0a0a0; --mz-master-bg: #151821;
  141. --mz-master-bg-dark: #111;
  142. }
  143. .mz-master-radio-group { display: flex; justify-content: center; gap: 10px; margin: 25px 0; }
  144. .mz-master-radio-group input { display: none; }
  145. .mz-master-radio-group label {
  146. font-size: 14px; font-weight: 500; color: var(--mz-master-text-dark); background-color: #2c2c2c;
  147. padding: 10px 20px; border-radius: 8px; border: 2px solid #444; cursor: pointer; transition: all 0.2s ease;
  148. }
  149. .mz-master-radio-group input:checked + label {
  150. color: var(--mz-master-text); border-color: var(--mz-master-pink); box-shadow: 0 0 10px rgba(236, 64, 122, 0.5);
  151. }
  152. .mz-master-modal-button, #${this.triggerId} {
  153. font-family: 'Inter', sans-serif; font-weight: 500; color: var(--mz-master-text);
  154. background-image: linear-gradient(to right, var(--mz-master-navy), var(--mz-master-black));
  155. border-radius: 6px; border: none; cursor: pointer; transition: all 0.2s ease;
  156. box-shadow: 0 2px 5px rgba(236, 64, 122, 0.3);
  157. }
  158. #${this.triggerId} { font-size: 12px; padding: 6px 12px; margin-left: 20px; }
  159. .mz-master-modal-button { font-size: 13px; padding: 10px 20px; }
  160. .mz-master-modal-button.cancel { background-image: linear-gradient(to right, #555, #333); }
  161. .mz-master-modal-button.close-btn { margin-top: 20px; }
  162. .mz-master-modal-button:hover, #${this.triggerId}:hover {
  163. transform: scale(1.05); box-shadow: 0 4px 10px rgba(236, 64, 122, 0.5);
  164. }
  165. .mz-master-modal-button.cancel:hover { box-shadow: 0 4px 10px rgba(255, 255, 255, 0.2); }
  166. #${this.triggerId}:disabled {
  167. cursor: not-allowed; background-image: linear-gradient(to right, #333, #222);
  168. color: #666; transform: none; box-shadow: none;
  169. }
  170. .${this.modalId}-overlay {
  171. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  172. background-color: rgba(0, 0, 0, 0.7); z-index: 9998; display: flex; justify-content: center; align-items: center;
  173. opacity: 0; transition: opacity 0.3s ease;
  174. }
  175. .${this.modalId}-content {
  176. position: relative; font-family: 'Inter', sans-serif; background-color: var(--mz-master-navy); color: var(--mz-master-text);
  177. padding: 25px; border-radius: 8px; width: 90%; max-width: 600px; box-shadow: 0 0 20px rgba(236, 64, 122, 0.2);
  178. border: 1px solid var(--mz-master-pink); transform: scale(0.9); transition: transform 0.3s ease; text-align: center;
  179. }
  180. .${this.modalId}-content h2 { margin: 0 25px 15px; color: var(--mz-master-pink); font-weight: 700; }
  181. .modal-close-btn {
  182. position: absolute; top: 10px; right: 15px; background: none; border: none; font-size: 24px;
  183. color: var(--mz-master-text-dark); cursor: pointer; transition: color 0.2s, transform 0.2s;
  184. }
  185. .modal-close-btn:hover { color: var(--mz-master-text); transform: scale(1.2); }
  186. .modal-settings-btn {
  187. position: absolute; top: 12px; left: 15px; background: none; border: none; font-size: 18px;
  188. color: var(--mz-master-text-dark); cursor: pointer; transition: color 0.2s, transform 0.2s;
  189. }
  190. .modal-settings-btn:hover { color: var(--mz-master-text); transform: rotate(45deg); }
  191. .modal-button-container { display: flex; justify-content: center; gap: 15px; margin-top: 25px; }
  192. .${this.modalId}-spinner {
  193. border: 4px solid #444; border-top: 4px solid var(--mz-master-pink); border-radius: 50%; width: 40px; height: 40px;
  194. animation: spin 1s linear infinite; margin: 20px auto;
  195. }
  196. .modal-plan-info {
  197. font-size: 13px; color: var(--mz-master-text-dark); margin-bottom: 15px;
  198. border-bottom: 1px solid #444; padding-bottom: 15px;
  199. }
  200. .settings-search-input {
  201. width: 100%; padding: 10px; margin-bottom: 15px; box-sizing: border-box; background-color: var(--mz-master-bg-dark);
  202. border: 1px solid #444; border-radius: 4px; color: var(--mz-master-text); font-family: 'Inter', sans-serif;
  203. }
  204. #${this.progressTrackerId} {
  205. font-size: 14px; color: var(--mz-master-text); margin: 20px 0; font-weight: 500;
  206. }
  207. #${this.logPanelId}, .modal-plan-details, .settings-list {
  208. height: 250px; overflow-y: auto; background-color: var(--mz-master-bg-dark); border: 1px solid #333;
  209. border-radius: 4px; margin-top: 20px; padding: 10px; text-align: left;
  210. font-size: 12px; line-height: 1.6; font-family: 'Menlo', 'Consolas', monospace;
  211. }
  212. .settings-list-item { display: block; margin-bottom: 5px; cursor: pointer; padding: 2px 5px; }
  213. .log-entry { margin-bottom: 3px; }
  214. .log-entry-ok { color: #81C784; }
  215. .log-entry-error { color: #E57373; font-weight: bold; }
  216. .log-entry-info { color: #90A4AE; }
  217. @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
  218. `);
  219. },
  220.  
  221. appendToLog(message, type = 'info') {
  222. const logPanel = document.getElementById(this.logPanelId);
  223. if (!logPanel) {
  224. return;
  225. }
  226. const entry = document.createElement('div');
  227. entry.textContent = message;
  228. entry.className = `log-entry log-entry-${type}`;
  229. logPanel.appendChild(entry);
  230. logPanel.scrollTop = logPanel.scrollHeight;
  231. },
  232.  
  233. updateProgress(sent, total) {
  234. const tracker = document.getElementById(this.progressTrackerId);
  235. if (tracker) {
  236. tracker.textContent = `Processing challenge ${sent} of ${total}...`;
  237. }
  238. },
  239.  
  240. showModal(content, options = {}) {
  241. this.closeModal(true);
  242. const { title, showSettings, closeOnClickOutside } = options;
  243. const overlay = document.createElement('div');
  244. overlay.className = `${this.modalId}-overlay`;
  245. if (closeOnClickOutside) {
  246. overlay.addEventListener('click', (e) => {
  247. if (e.target === overlay) {
  248. this.closeModal();
  249. }
  250. });
  251. }
  252.  
  253. const contentDiv = document.createElement('div');
  254. contentDiv.className = `${this.modalId}-content`;
  255. contentDiv.innerHTML = `
  256. <button title="Close" class="modal-close-btn">×</button>
  257. ${showSettings ? `<button title="Settings" class="modal-settings-btn"><i class="fa fa-cog"></i></button>` : ''}
  258. ${title ? `<h2>${title}</h2>` : ''}
  259. ${content}
  260. `;
  261.  
  262. overlay.appendChild(contentDiv);
  263. document.body.appendChild(overlay);
  264.  
  265. contentDiv.querySelector('.modal-close-btn').onclick = () => this.closeModal();
  266. if (showSettings) {
  267. const settingsBtn = contentDiv.querySelector('.modal-settings-btn');
  268. if (settingsBtn) {
  269. settingsBtn.onclick = () => massChallengeSender.showSettings();
  270. }
  271. }
  272.  
  273. setTimeout(() => {
  274. overlay.style.opacity = '1';
  275. contentDiv.style.transform = 'scale(1)';
  276. }, 10);
  277. return contentDiv;
  278. },
  279.  
  280. updateModal(content, options = {}) {
  281. const contentDiv = document.querySelector(`.${this.modalId}-content`);
  282. if (contentDiv) {
  283. const { title, showSettings } = options;
  284. const closeBtnHTML = contentDiv.querySelector('.modal-close-btn')?.outerHTML || '';
  285. const titleHTML = title ? `<h2>${title}</h2>` : '';
  286. const settingsBtnHTML = showSettings ? `<button title="Settings" class="modal-settings-btn"><i class="fa fa-cog"></i></button>` : '';
  287.  
  288. contentDiv.innerHTML = `${closeBtnHTML}${settingsBtnHTML}${titleHTML}${content}`;
  289.  
  290. contentDiv.querySelector('.modal-close-btn').onclick = () => this.closeModal();
  291. if (showSettings) {
  292. const settingsBtn = contentDiv.querySelector('.modal-settings-btn');
  293. if (settingsBtn) {
  294. settingsBtn.onclick = () => massChallengeSender.showSettings();
  295. }
  296. }
  297. }
  298. },
  299.  
  300. closeModal(isSilent = false) {
  301. const overlay = document.querySelector(`.${this.modalId}-overlay`);
  302. if (!overlay) {
  303. return;
  304. }
  305. if (isSilent) {
  306. overlay.remove();
  307. } else {
  308. overlay.style.opacity = '0';
  309. overlay.querySelector(`.${this.modalId}-content`).style.transform = 'scale(0.9)';
  310. setTimeout(() => {
  311. overlay.remove();
  312. this.setButtonState('idle');
  313. }, 300);
  314. }
  315. },
  316.  
  317. renderInitialUI(container) {
  318. if (document.getElementById(this.triggerId)) {
  319. return null;
  320. }
  321. const button = document.createElement('button');
  322. button.id = this.triggerId;
  323. button.textContent = 'Send Lots of Challenges';
  324. const parent = container.parentElement;
  325. parent.style.display = 'flex';
  326. parent.style.alignItems = 'center';
  327. parent.appendChild(button);
  328. return button;
  329. },
  330.  
  331. setButtonState(state) {
  332. const button = document.getElementById(this.triggerId);
  333. if (!button) {
  334. return;
  335. }
  336. if (state === 'processing') {
  337. button.disabled = true;
  338. button.textContent = 'Processing...';
  339. } else {
  340. button.disabled = false;
  341. button.textContent = 'Send Challenges to Everyone';
  342. }
  343. }
  344. };
  345.  
  346. const challengeScheduler = {
  347. CHALLENGE_WINDOW_DAYS: 12,
  348.  
  349. generateChallengeDates() {
  350. const dates = [];
  351. const now = new Date();
  352.  
  353. for (let i = 1; i <= this.CHALLENGE_WINDOW_DAYS; i++) {
  354. const futureDate = new Date();
  355. futureDate.setDate(now.getDate() + i);
  356. futureDate.setUTCHours(0, 0, 0, 0);
  357. const timestamp = futureDate.getTime() / 1000;
  358. dates.push(timestamp);
  359. }
  360. return dates;
  361. },
  362.  
  363. shuffleArray(array) {
  364. let currentIndex = array.length,
  365. randomIndex;
  366. while (currentIndex !== 0) {
  367. randomIndex = Math.floor(Math.random() * currentIndex);
  368. currentIndex--;
  369. [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
  370. }
  371. return array;
  372. },
  373.  
  374. createTeamSubgroups(teams, groupCount) {
  375. const groups = [];
  376. if (groupCount === 0 || teams.length === 0) {
  377. return groups;
  378. }
  379. let startIndex = 0;
  380. const baseSize = Math.floor(teams.length / groupCount);
  381. const remainder = teams.length % groupCount;
  382.  
  383. for (let i = 0; i < groupCount; i++) {
  384. const size = baseSize + (i < remainder ? 1 : 0);
  385. groups.push(teams.slice(startIndex, startIndex + size));
  386. startIndex += size;
  387. }
  388. return groups.filter(g => g.length > 0);
  389. },
  390.  
  391. prepareChallengeMatrix(teams, dates, teamType) {
  392. const matrix = [];
  393. const GROUPS_PER_DATE_BATCH = 6;
  394. const challengeTypes = [];
  395. if (teamType === 'senior' || teamType === 'both') {
  396. challengeTypes.push('senior');
  397. }
  398. if (teamType === 'u21' || teamType === 'both') {
  399. challengeTypes.push('u21');
  400. }
  401. if (challengeTypes.length === 0) {
  402. return [];
  403. }
  404. const shuffledDates = this.shuffleArray([...dates]);
  405. for (let i = 0; i < shuffledDates.length; i += GROUPS_PER_DATE_BATCH) {
  406. const dateBatch = shuffledDates.slice(i, i + GROUPS_PER_DATE_BATCH);
  407. const shuffledTeams = this.shuffleArray([...teams]);
  408. const teamGroups = this.createTeamSubgroups(shuffledTeams, dateBatch.length);
  409. dateBatch.forEach((date, j) => {
  410. const currentGroup = teamGroups[j];
  411. if (!currentGroup) {
  412. return;
  413. }
  414. currentGroup.forEach(team => {
  415. challengeTypes.forEach(type => {
  416. matrix.push({
  417. team,
  418. type,
  419. date,
  420. hour: 13,
  421. home_away: Math.random() < 0.5 ? 'home' : 'away'
  422. });
  423. });
  424. });
  425. });
  426. }
  427. return this.shuffleArray(matrix);
  428. },
  429.  
  430. async runRealChallenges(matrix) {
  431. const modalContent = uiManager.showModal(
  432. `<div class="${uiManager.modalId}-spinner"></div><div id="${uiManager.progressTrackerId}"></div><div id="${uiManager.logPanelId}"></div>`, {
  433. closeOnClickOutside: false
  434. }
  435. );
  436.  
  437. let sentCount = 0;
  438. const totalChallenges = matrix.length;
  439. uiManager.updateProgress(sentCount, totalChallenges);
  440.  
  441. const challengePromises = matrix.map(challenge => {
  442. const { team, type, date, hour, home_away } = challenge;
  443. const formattedDate = new Date(date * 1000).toLocaleDateString("en-CA", {
  444. timeZone: 'UTC'
  445. });
  446. const formData = new URLSearchParams();
  447. formData.append(home_away === 'home' ? 'date_home' : 'date_away', `${date},${hour}`);
  448. formData.append('tactic_home', 'a');
  449. formData.append('tactic_away', 'a');
  450. const url = new URL(dataManager.endpoints.createChallenge);
  451. const teamId = type === 'senior' ? team.ntid : team.u21ntid;
  452. url.searchParams.append('tid', teamId);
  453. url.searchParams.append('national', type);
  454. url.searchParams.append('type', type);
  455. url.searchParams.append('sport', 'soccer');
  456.  
  457. return dataManager.fetch({
  458. method: 'POST',
  459. url: url.toString(),
  460. data: formData.toString(),
  461. headers: {
  462. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  463. }
  464. }).then(response => {
  465. if (response.status >= 200 && response.status < 300) {
  466. uiManager.appendToLog(`[SUCCESS] Sent [${type.toUpperCase()}] challenge to ${team.name} on ${formattedDate}.`, 'ok');
  467. return { status: 'fulfilled' };
  468. } else {
  469. throw new Error(`Status ${response.status}`);
  470. }
  471. }).catch(error => {
  472. uiManager.appendToLog(`[FAILED] Sending [${type.toUpperCase()}] challenge to ${team.name}. Reason: ${error.message || 'Network Error'}`, 'error');
  473. return { status: 'rejected' };
  474. }).finally(() => {
  475. sentCount++;
  476. uiManager.updateProgress(sentCount, totalChallenges);
  477. });
  478. });
  479.  
  480. await Promise.all(challengePromises);
  481.  
  482. modalContent.querySelector(`.${uiManager.modalId}-spinner`).style.display = 'none';
  483. uiManager.appendToLog('Done.', 'info');
  484. const closeButton = document.createElement('button');
  485. closeButton.textContent = 'Close';
  486. closeButton.className = 'mz-master-modal-button close-btn';
  487. closeButton.onclick = () => location.reload();
  488. modalContent.appendChild(closeButton);
  489. }
  490. };
  491.  
  492. const massChallengeSender = {
  493. challengeTabSelector: 'a[href*="sub=challenge"]',
  494. dropdownSelector: 'select[name="tid"]',
  495.  
  496. showSettings() {
  497. const coachedTeams = GM_getValue(dataManager.storageKeys.defaultCoached, []);
  498. const uncoachedTeams = GM_getValue(dataManager.storageKeys.defaultUncoached, []);
  499. const allKnownTeams = [...coachedTeams, ...uncoachedTeams];
  500. const excludedTeams = GM_getValue(dataManager.storageKeys.exclusionList, uncoachedTeams);
  501.  
  502. const searchInputHTML = `<input type="text" id="settings-search" class="settings-search-input" placeholder="Filter...">`;
  503. const settingsListHTML = `<div class="settings-list">${allKnownTeams.sort().map(teamName => `
  504. <label class="settings-list-item">
  505. <input type="checkbox" name="nt-exclusion" value="${teamName}" ${!excludedTeams.includes(teamName) ? 'checked' : ''}>
  506. ${teamName}
  507. </label>
  508. `).join('')}</div>`;
  509. const buttonsHTML = `
  510. <div class="modal-button-container">
  511. <button id="modal-save-settings-btn" class="mz-master-modal-button">Save</button>
  512. </div>
  513. `;
  514. uiManager.showModal(`${searchInputHTML}${settingsListHTML}${buttonsHTML}`, {
  515. title: "Manage Targetable Countries (Uncheck Undesirable Countries)",
  516. closeOnClickOutside: true,
  517. showSettings: true
  518. });
  519.  
  520. const searchInput = document.getElementById('settings-search');
  521. searchInput.oninput = () => {
  522. const filter = searchInput.value.toLowerCase();
  523. document.querySelectorAll('.settings-list-item').forEach(item => {
  524. const text = item.textContent.toLowerCase();
  525. item.style.display = text.includes(filter) ? '' : 'none';
  526. });
  527. };
  528.  
  529. document.getElementById('modal-save-settings-btn').onclick = () => {
  530. const newExcluded = [...document.querySelectorAll('input[name="nt-exclusion"]:not(:checked)')].map(cb => cb.value);
  531. GM_setValue(dataManager.storageKeys.exclusionList, newExcluded);
  532. this.startW();
  533. };
  534. },
  535.  
  536. async generateAndShowChallengePlan(teamType) {
  537. uiManager.setButtonState('processing');
  538. uiManager.showModal(`<div class="${uiManager.modalId}-spinner"></div><p>Please wait...</p>`, {
  539. showSettings: true
  540. });
  541. try {
  542. const loadingDelay = new Promise(resolve => setTimeout(resolve, 1000));
  543. const dataFetch = dataManager.getTargetableNTs();
  544. const [_, teams] = await Promise.all([loadingDelay, dataFetch]);
  545. const dates = challengeScheduler.generateChallengeDates();
  546.  
  547. if (!teams || teams.length === 0) {
  548. throw new Error("No targetable NTs found. Check settings.");
  549. }
  550. if (!dates || dates.length === 0) {
  551. throw new Error("No available challenge dates found.");
  552. }
  553.  
  554. const nowString = new Date().toLocaleString('en-GB', {
  555. timeZone: 'Europe/Stockholm',
  556. year: 'numeric',
  557. month: 'long',
  558. day: 'numeric',
  559. hour: '2-digit',
  560. minute: '2-digit',
  561. second: '2-digit'
  562. }).replace(' at', ',');
  563.  
  564. const matrix = challengeScheduler.prepareChallengeMatrix(teams, dates, teamType);
  565. const summary = `Reference Time (Sweden): <b>${nowString}</b>.<br><b>${matrix.length} challenges</b> will be sent to <b>${teams.length}</b> targetable NTs.`;
  566. const planDetails = `<div class="modal-plan-details">${matrix.map(c => `<div>[${c.type.toUpperCase()}] to <b>${c.team.name}</b> on ${new Date(c.date*1000).toLocaleDateString("en-CA",{timeZone: 'UTC'})} at ${c.hour}:00</div>`).join('')}</div>`;
  567. const buttonsHTML = `
  568. <div class="modal-button-container">
  569. <button id="modal-cancel-btn" class="mz-master-modal-button cancel">Cancel</button>
  570. <button id="modal-confirm-btn" class="mz-master-modal-button">Confirm & Send</button>
  571. </div>
  572. `;
  573. uiManager.updateModal(`<div class="modal-plan-info">${summary}</div>${planDetails}${buttonsHTML}`, {
  574. title: "",
  575. showSettings: true
  576. });
  577.  
  578. document.getElementById('modal-cancel-btn').onclick = () => uiManager.closeModal();
  579. document.getElementById('modal-confirm-btn').onclick = () => challengeScheduler.runRealChallenges(matrix);
  580. } catch (error) {
  581. console.error(`${SCRIPT_PREFIX} Error during plan generation:`, error);
  582. uiManager.showModal(`<p>${error.message}</p>`, {
  583. title: 'Error'
  584. });
  585. uiManager.setButtonState('idle');
  586. }
  587. },
  588.  
  589. startW() {
  590. const types = [{
  591. value: 'senior',
  592. text: 'Senior'
  593. }, {
  594. value: 'u21',
  595. text: 'U21'
  596. }, {
  597. value: 'both',
  598. text: 'Both'
  599. }];
  600. const radioHTML = types.map((type) => `
  601. <input type="radio" id="radio-${type.value}" name="${uiManager.teamTypeSelectorName}" value="${type.value}" ${type.value === 'senior' ? 'checked' : ''}>
  602. <label for="radio-${type.value}">${type.text}</label>
  603. `).join('');
  604. const modalContent = `<div class="mz-master-radio-group">${radioHTML}</div><button id="modal-continue-btn" class="mz-master-modal-button">Continue</button>`;
  605. uiManager.showModal(modalContent, {
  606. showSettings: true,
  607. closeOnClickOutside: true,
  608. title: 'Select Type'
  609. });
  610. document.getElementById('modal-continue-btn').addEventListener('click', () => {
  611. const selectedType = document.querySelector(`input[name="${uiManager.teamTypeSelectorName}"]:checked`).value;
  612. this.generateAndShowChallengePlan(selectedType);
  613. });
  614. },
  615.  
  616. watchTab() {
  617. const persistentObserver = new MutationObserver(() => {
  618. const dropdown = document.querySelector(this.dropdownSelector);
  619. if (dropdown && dropdown.parentElement && !document.getElementById(uiManager.triggerId)) {
  620. const button = uiManager.renderInitialUI(dropdown.parentElement);
  621. if (button) {
  622. button.addEventListener('click', () => this.startW());
  623. }
  624. }
  625. });
  626. persistentObserver.observe(document.body, {
  627. childList: true,
  628. subtree: true
  629. });
  630. },
  631.  
  632. initialize() {
  633. console.log(`${SCRIPT_PREFIX} Initializing...`);
  634. dataManager.initializeDefaultLists();
  635. uiManager.injectStyles();
  636. this.watchTab();
  637. }
  638. };
  639.  
  640. const incomingChallengeNotifier = {
  641. CONFIG: {
  642. USERNAME: '',
  643. COUNTRY_NAME: '',
  644. COUNTRY_ID: null,
  645. SENIOR_NT_ID: null,
  646. U21_NT_ID: null
  647. },
  648. NOTIFICATION_SETTINGS: {
  649. duration: 10000,
  650. close: true,
  651. gravity: "bottom",
  652. position: "left",
  653. style: {
  654. background: "#0D0D0D",
  655. color: "#00FF41",
  656. border: "1px solid #00FF41",
  657. borderRadius: "0px",
  658. boxShadow: "0 0 15px #00FF41",
  659. fontFamily: "monospace"
  660. }
  661. },
  662.  
  663. async initializeConfig() {
  664. const u = this.CONFIG.USERNAME || this.getCurrentUsername();
  665. if (!u) {
  666. return false;
  667. }
  668. try {
  669. const userResponse = await dataManager.fetch({
  670. method: 'GET',
  671. url: dataManager.endpoints.userData(u)
  672. });
  673. const xml = new DOMParser().parseFromString(userResponse.responseText, 'text/xml');
  674. const countryCode = xml.querySelector('UserData')?.getAttribute('countryShortname');
  675. if (!countryCode) {
  676. return false;
  677. }
  678. const countries = await dataManager.getCountries();
  679. if (!countries) {
  680. return false;
  681. }
  682. const countryData = countries.find(c => c.code === countryCode);
  683. if (!countryData) {
  684. return false;
  685. }
  686. Object.assign(this.CONFIG, {
  687. USERNAME: u,
  688. COUNTRY_NAME: countryData.name,
  689. COUNTRY_ID: countryData.cid,
  690. SENIOR_NT_ID: countryData.ntid,
  691. U21_NT_ID: countryData.u21ntid
  692. });
  693. return true;
  694. } catch (e) {
  695. return false;
  696. }
  697. },
  698. getCurrentUsername() {
  699. const el = document.querySelector('#header-username');
  700. return el ? el.textContent.trim() : '';
  701. },
  702. async verifyNCStatus() {
  703. try {
  704. const url = dataManager.endpoints.nationalTeam(this.CONFIG.SENIOR_NT_ID, this.CONFIG.COUNTRY_ID, 'national_team');
  705. const response = await dataManager.fetch({
  706. method: 'GET',
  707. url: url
  708. });
  709. const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
  710. return Array.from(doc.querySelectorAll('a[href*="/?p=profile"]')).some(link => link.textContent.trim() === this.CONFIG.USERNAME);
  711. } catch (e) {
  712. return false;
  713. }
  714. },
  715. async processIncomingChallenges(cat, tid) {
  716. const p = new URLSearchParams({
  717. ntid: tid,
  718. cid: this.CONFIG.COUNTRY_ID,
  719. type: cat === 'U21' ? 'national_team_u21' : 'national_team',
  720. sport: 'soccer'
  721. });
  722. try {
  723. const response = await dataManager.fetch({
  724. method: 'GET',
  725. url: `${dataManager.endpoints.challenges}&${p.toString()}`
  726. });
  727. const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
  728. const tab = doc.querySelector('#matches_in');
  729. if (!tab) {
  730. return;
  731. }
  732. tab.querySelectorAll('tbody tr').forEach(row => {
  733. const opp = row.querySelector('td:nth-child(1) a');
  734. const tim = row.querySelector('td:nth-child(3)');
  735. if (!opp || !tim) {
  736. return;
  737. }
  738. const oppName = opp.textContent.trim();
  739. const timeText = tim.textContent.trim();
  740. const sk = `challenge_${oppName}_${cat}_${timeText}`;
  741. if (!GM_getValue(sk, false)) {
  742. Toastify({
  743. text: `Incoming NT challenge from ${oppName} (${cat})!`,
  744. ...this.NOTIFICATION_SETTINGS
  745. }).showToast();
  746. GM_setValue(sk, true);
  747. }
  748. });
  749. } catch (e) {
  750. return;
  751. }
  752. },
  753. async initialize() {
  754. GM_addStyle(GM_getResourceText('TOASTIFY_CSS'));
  755.  
  756. const ok = await this.initializeConfig();
  757. if (!ok) {
  758. return;
  759. }
  760. const isNC = await this.verifyNCStatus();
  761. if (!isNC) {
  762. return;
  763. }
  764. if (this.CONFIG.SENIOR_NT_ID) {
  765. this.processIncomingChallenges('SENIOR', this.CONFIG.SENIOR_NT_ID);
  766. }
  767. if (this.CONFIG.U21_NT_ID) {
  768. this.processIncomingChallenges('U21', this.CONFIG.U21_NT_ID);
  769. }
  770. }
  771. };
  772.  
  773. massChallengeSender.initialize();
  774. incomingChallengeNotifier.initialize();
  775. })();