// ==UserScript== // @name ylOppTactsPreview (Modified) // @namespace douglaskampl // @version 2.4 // @description Shows the latest tactics used by an opponent from the scheduled matches page // @author Douglas // @match https://www.managerzone.com/?p=match&sub=scheduled // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com // @grant GM_addStyle // @license MIT // @downloadURL none // ==/UserScript== GM_addStyle(` .fade-in { animation: fade-in 0.2s ease forwards; } .fade-out { animation: fade-out 0.2s ease forwards; } @keyframes fade-in { from {opacity:0; transform:translateY(-5px);} to {opacity:1; transform:translateY(0);} } @keyframes fade-out { from {opacity:1; transform:translateY(0);} to {opacity:0; transform:translateY(-5px);} } .magnifier-icon { cursor: pointer !important; font-size: 14px !important; margin-left: 5px !important; z-index: 100 !important; pointer-events: auto !important; color: #444; transition: transform 0.2s ease, opacity 0.2s ease; } .magnifier-icon:hover { transform: scale(1.2); opacity: 0.8; } .tactics-container { position: absolute; top: 150px; left: 50%; transform: translateX(-50%); background: #fafafa; border: 1px solid #ccc; border-radius: 6px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 500px; max-height: 80vh; overflow: hidden; font-family: sans-serif; color: #333; font-size: 13px; display: flex; flex-direction: column; z-index: 9999; } .tactics-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; background: #cdd; border-bottom: 1px solid #ccc; font-size: 12px; } .tactics-header .match-info-text { margin: 0; font-weight: normal; font-size: 12px; color: #333; } .tactics-header .close-button { background: none; border: none; cursor: pointer; font-size: 14px; color: #333; line-height: 1; padding: 0; margin: 0; transition: transform 0.2s ease, color 0.2s ease; } .tactics-header .close-button:hover { transform: scale(1.1); color: #000; } .tactics-header .title-main { font-weight: 600; color: #333; margin-bottom: 2px; } .tactics-header .title-subtitle { font-size: 11px; color: #666; font-style: italic; } .tactics-list { padding: 10px; overflow-y: auto; flex: 1; display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; align-content: flex-start; background: #fff; scrollbar-width: thin; scrollbar-color: rgba(136, 136, 136, 0.5) transparent; } .tactics-list::-webkit-scrollbar { width: 8px; } .tactics-list::-webkit-scrollbar-track { background: transparent; border-radius: 4px; } .tactics-list::-webkit-scrollbar-thumb { background-color: rgba(136, 136, 136, 0.5); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; } .tactics-list::-webkit-scrollbar-thumb:hover { background-color: rgba(136, 136, 136, 0.8); } .tactic-item { background: #fff; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; padding: 6px; font-size: 11px; display: flex; flex-direction: column; align-items: center; transition: transform 0.2s ease, box-shadow 0.2s ease; } .tactic-item:hover { transform: scale(1.03); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .tactic-item p { margin: 5px 0 0 0; color: #333; } .tactics-container canvas { border-radius: 4px; transition: transform 0.2s ease, box-shadow 0.2s ease; margin-bottom: 5px; background: #f9f9f9; border: 1px solid #ddd; } .tactics-container canvas:hover { transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } #match-type-modal { position: absolute; top: 180px; left: 50%; transform: translateX(-50%); background: #fafafa; border: 1px solid #ccc; border-radius: 6px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 15px; font-family: sans-serif; font-size: 13px; color: #333; z-index: 10000; width: 220px; } #match-type-modal label { display: block; margin-bottom: 8px; font-weight: bold; font-size: 13px; } #match-type-modal select { padding: 5px; font-size: 13px; width: 100%; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; background: #fff; } #match-type-modal .btn-group { display: flex; gap: 8px; justify-content: flex-end; } #match-type-modal button { padding: 5px 12px; font-size: 12px; cursor: pointer; background: #e0e0e0; border: 1px solid #aaa; border-radius: 4px; transition: background 0.2s ease; } #match-type-modal button:hover { background: #d0d0d0; } `); (function () { "use strict"; const CONSTANTS = { MAX_TACTICS: 10, SELECTORS: { FIXTURES_LIST: '#fixtures-results-list-wrapper', STATS_XENTE: '#legendDiv', ELO_SCHEDULED: '#eloScheduledSelect', HOME_TEAM: '.home-team-column.flex-grow-1', SELECT_WRAPPER: 'dd.set-default-wrapper' }, MATCH_TYPES: ['u18', 'u21', 'u23', 'no_restriction'] }; let ourTeamName = null; let selectedMatchTypeG = ''; let currentTidValue = ''; let currentOpponent = ''; let lastMagnifierRect = null; const observer = new MutationObserver(() => { insertIconsAndListeners(); }); function startObserving() { const fixturesList = document.querySelector(CONSTANTS.SELECTORS.FIXTURES_LIST); if (fixturesList) { observer.observe(fixturesList, { childList: true, subtree: true }); } } async function fetchLatestTactics(tidValue, opponent, matchType) { selectedMatchTypeG = matchType; currentTidValue = tidValue; currentOpponent = opponent; try { const response = await fetch( "https://www.managerzone.com/ajax.php?p=matches&sub=list&sport=soccer", { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: `type=played&hidescore=false&tid1=${tidValue}&offset=&selectType=${matchType}&limit=default`, credentials: 'include' } ); if (!response.ok) throw new Error('Network response was not ok'); const data = await response.json(); processTacticsData(data); } catch (_error) {} } function processTacticsData(data) { const parser = new DOMParser(); const htmlDocument = parser.parseFromString(data.list, 'text/html'); const scoreShownLinks = htmlDocument.querySelectorAll('a.score-shown'); const container = createTacticsContainer(selectedMatchTypeG, currentOpponent); document.body.appendChild(container); const listWrapper = container.querySelector('.tactics-list'); if (scoreShownLinks.length === 0) { const message = document.createElement('div'); message.style.textAlign = 'center'; message.style.color = '#555'; message.style.fontSize = '12px'; message.style.padding = '10px'; message.textContent = "No recent tactics found for the selected match type. Your opponent clearly doesn't care."; listWrapper.appendChild(message); container.classList.add('fade-in'); return; } scoreShownLinks.forEach((link, index) => { if (index >= CONSTANTS.MAX_TACTICS) return; const dl = link.closest('dl'); const theScore = link.textContent.trim(); const homeTeamName = dl.querySelector('.home-team-column .full-name')?.textContent.trim() || 'Home'; const awayTeamName = dl.querySelector('.away-team-column .full-name')?.textContent.trim() || 'Away'; const homeTeamLink = dl.querySelector('.home-team-column a.clippable'); const awayTeamLink = dl.querySelector('.away-team-column a.clippable'); let homeTid = null, awayTid = null; if (homeTeamLink) { homeTid = new URLSearchParams(new URL(homeTeamLink.href, location.href).search).get('tid'); } if (awayTeamLink) { awayTid = new URLSearchParams(new URL(awayTeamLink.href, location.href).search).get('tid'); } let homeGoals = 0; let awayGoals = 0; if (theScore.includes('-')) { const parts = theScore.split('-').map(x => x.trim()); if (parts.length === 2) { homeGoals = parseInt(parts[0]) || 0; awayGoals = parseInt(parts[1]) || 0; } } const mid = extractMidFromUrl(link.href); const tacticUrl = `https://www.managerzone.com/dynimg/pitch.php?match_id=${mid}`; const resultUrl = `https://www.managerzone.com/?p=match&sub=result&mid=${mid}`; const opponentIsHome = (homeTid === currentTidValue); const canvas = createCanvasWithReplacedColors(tacticUrl, opponentIsHome); const item = document.createElement('div'); item.className = 'tactic-item'; let opponentGoals = opponentIsHome ? homeGoals : awayGoals; let otherGoals = opponentIsHome ? awayGoals : homeGoals; if (opponentGoals > otherGoals) { item.style.backgroundColor = '#daf8da'; } else if (opponentGoals < otherGoals) { item.style.backgroundColor = '#f8dada'; } else { item.style.backgroundColor = '#f0f0f0'; } const linkA = document.createElement('a'); linkA.href = resultUrl; linkA.target = '_blank'; linkA.className = 'tactic-link'; linkA.style.color = '#333'; linkA.style.textDecoration = 'none'; linkA.appendChild(canvas); const scoreP = document.createElement('p'); scoreP.textContent = `${homeTeamName} ${theScore} ${awayTeamName}`; linkA.appendChild(scoreP); item.appendChild(linkA); listWrapper.appendChild(item); }); container.classList.add('fade-in'); } function showMatchTypeModal(tidValue, opponent, event) { const existingModal = document.getElementById('match-type-modal'); if (existingModal) { fadeOutAndRemove(existingModal); } const modal = document.createElement('div'); modal.id = 'match-type-modal'; modal.classList.add('fade-in'); const label = document.createElement('label'); label.textContent = 'Select match type:'; modal.appendChild(label); const select = document.createElement('select'); CONSTANTS.MATCH_TYPES.forEach(type => { const option = document.createElement('option'); option.value = type; option.textContent = type.replace('_', ' ').toUpperCase(); select.appendChild(option); }); modal.appendChild(select); const btnGroup = document.createElement('div'); btnGroup.className = 'btn-group'; const okButton = document.createElement('button'); okButton.textContent = 'OK'; okButton.onclick = () => { fadeOutAndRemove(modal); fetchLatestTactics(tidValue, opponent, select.value); }; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.onclick = () => fadeOutAndRemove(modal); btnGroup.append(okButton, cancelButton); modal.appendChild(btnGroup); document.body.appendChild(modal); const rect = event.target.getBoundingClientRect(); lastMagnifierRect = { left: window.scrollX + rect.left, top: window.scrollY + rect.top, bottom: window.scrollY + rect.bottom, width: rect.width, height: rect.height }; modal.style.position = 'absolute'; modal.style.top = (lastMagnifierRect.bottom + 5) + 'px'; modal.style.left = (lastMagnifierRect.left) + 'px'; } function createTacticsContainer(matchType, opponent) { const existingContainer = document.getElementById('tactics-container'); if (existingContainer) { fadeOutAndRemove(existingContainer); } const container = document.createElement('div'); container.id = 'tactics-container'; container.className = 'tactics-container'; const header = document.createElement('div'); header.className = 'tactics-header'; const title = document.createElement('div'); title.className = 'match-info-text'; title.innerHTML = `
${opponent ? opponent : ''} – ${matchType.toUpperCase()}
${opponent}'s tactics are represented by black dots with white outlines:
`; header.appendChild(title); const closeButton = document.createElement('button'); closeButton.className = 'close-button'; closeButton.textContent = '×'; closeButton.onclick = () => fadeOutAndRemove(container); header.appendChild(closeButton); container.appendChild(header); const listWrapper = document.createElement('div'); listWrapper.className = 'tactics-list'; container.appendChild(listWrapper); document.body.appendChild(container); if (lastMagnifierRect) { const modalWidth = 420; const leftPos = lastMagnifierRect.left + (lastMagnifierRect.width / 2) - (modalWidth / 2); const topPos = lastMagnifierRect.bottom - 350; container.style.position = 'absolute'; container.style.top = topPos + 'px'; container.style.left = leftPos + 'px'; container.style.transform = 'none'; } return container; } function fadeOutAndRemove(el) { el.classList.remove('fade-in'); el.classList.add('fade-out'); setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 200); } function identifyUserTeamName() { const ddRows = document.querySelectorAll('dd.odd'); const countMap = new Map(); let totalMatches = 0; ddRows.forEach(dd => { const homeName = dd.querySelector('.home-team-column .full-name')?.textContent.trim(); const awayName = dd.querySelector('.away-team-column .full-name')?.textContent.trim(); if (homeName && awayName) { totalMatches++; countMap.set(homeName, (countMap.get(homeName) || 0) + 1); countMap.set(awayName, (countMap.get(awayName) || 0) + 1); } }); for (const [name, count] of countMap.entries()) { if (count === totalMatches) { return name; } } return null; } function insertIconsAndListeners() { ourTeamName = ourTeamName || identifyUserTeamName(); if (!ourTeamName) return; document.querySelectorAll('dd.odd').forEach(dd => { const selectWrapper = dd.querySelector(CONSTANTS.SELECTORS.SELECT_WRAPPER); if (selectWrapper) { const select = selectWrapper.querySelector('select'); if (select && !selectWrapper.querySelector('.magnifier-icon')) { const homeTeamName = dd.querySelector('.home-team-column .full-name')?.textContent.trim(); const awayTeamName = dd.querySelector('.away-team-column .full-name')?.textContent.trim(); let opponentName = null; let opponentTid = null; const homeTeamLink = dd.querySelector('.home-team-column a.clippable'); const awayTeamLink = dd.querySelector('.away-team-column a.clippable'); let homeTid = null, awayTid = null; if (homeTeamLink) { homeTid = new URLSearchParams(new URL(homeTeamLink.href, location.href).search).get('tid'); } if (awayTeamLink) { awayTid = new URLSearchParams(new URL(awayTeamLink.href, location.href).search).get('tid'); } if (homeTeamName === ourTeamName && awayTeamName && awayTid) { opponentName = awayTeamName; opponentTid = awayTid; } else if (awayTeamName === ourTeamName && homeTeamName && homeTid) { opponentName = homeTeamName; opponentTid = homeTid; } else { return; } if (!opponentTid) return; const icon = document.createElement('span'); icon.className = 'magnifier-icon'; icon.dataset.tid = opponentTid; icon.dataset.opponent = opponentName; icon.textContent = '🔍'; select.insertAdjacentElement('afterend', icon); } } }); } function extractMidFromUrl(url) { return new URLSearchParams(new URL(url, location.href).search).get('mid'); } function processImage(context, canvas, image, opponentIsHome) { if (opponentIsHome) { context.translate(canvas.width / 2, canvas.height / 2); context.rotate(Math.PI); // π/180° context.translate(-canvas.width / 2, -canvas.height / 2); } context.drawImage(image, 0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const darkGreen = { r: 0, g: 100, b: 0 }; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const isBlack = (r < 30 && g < 30 && b < 30); const isYellow = (r > 200 && g > 200 && b < 100); if (opponentIsHome) { if (isYellow) { data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; } else if (isBlack) { data[i] = darkGreen.r; data[i + 1] = darkGreen.g; data[i + 2] = darkGreen.b; } } else { if (isBlack) { data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; } else if (isYellow) { data[i] = darkGreen.r; data[i + 1] = darkGreen.g; data[i + 2] = darkGreen.b; } } } const tempData = new Uint8ClampedArray(data); // Outline branco nos pixels pretos (oponente) for (let y = 0; y < canvas.height; y++) { for (let x = 0; x < canvas.width; x++) { const i = (y * canvas.width + x) * 4; if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) { for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const nx = x + dx; const ny = y + dy; if (nx >= 0 && nx < canvas.width && ny >= 0 && ny < canvas.height) { const ni = (ny * canvas.width + nx) * 4; if (!(data[ni] === 0 && data[ni + 1] === 0 && data[ni + 2] === 0)) { tempData[ni] = 255; tempData[ni + 1] = 255; tempData[ni + 2] = 255; } } } } } } } context.putImageData(new ImageData(tempData, canvas.width, canvas.height), 0, 0); } function createCanvas(width, height) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.style.pointerEvents = 'auto'; return canvas; } function createCanvasWithReplacedColors(imageUrl, opponentIsHome) { const canvas = document.createElement('canvas'); canvas.width = 150; canvas.height = 200; canvas.style.pointerEvents = 'auto'; const context = canvas.getContext('2d'); const image = new Image(); image.crossOrigin = 'Anonymous'; image.onload = () => processImage(context, canvas, image, opponentIsHome); image.src = imageUrl; return canvas; } function waitForEloValues() { const interval = setInterval(() => { const elements = document.querySelectorAll(CONSTANTS.SELECTORS.HOME_TEAM); if (elements.length > 0 && elements[elements.length - 1]?.innerHTML.includes('br')) { clearInterval(interval); insertIconsAndListeners(); } }, 100); setTimeout(() => { clearInterval(interval); insertIconsAndListeners(); }, 1500); } function initialize() { const statsXenteRunning = document.querySelector(CONSTANTS.SELECTORS.STATS_XENTE); const eloScheduledSelected = document.querySelector(CONSTANTS.SELECTORS.ELO_SCHEDULED)?.checked; statsXenteRunning && eloScheduledSelected ? waitForEloValues() : insertIconsAndListeners(); startObserving(); } setTimeout(initialize, 500); document.body.addEventListener('click', (e) => { if (e.target?.classList.contains('magnifier-icon')) { e.preventDefault(); e.stopPropagation(); const tidValue = e.target.dataset.tid; const opponent = e.target.dataset.opponent; if (!tidValue) return; showMatchTypeModal(tidValue, opponent, e); } else { const tacticsContainer = document.getElementById('tactics-container'); const matchTypeModal = document.getElementById('match-type-modal'); if (tacticsContainer && !tacticsContainer.contains(e.target) && !e.target.classList.contains('magnifier-icon')) { fadeOutAndRemove(tacticsContainer); } if (matchTypeModal && !matchTypeModal.contains(e.target)) { fadeOutAndRemove(matchTypeModal); } } }); })();