Greasy Fork

ylOppTactsPreview (Modified)

Shows latest 10 tactics used by an opponent from the scheduled matches page

目前为 2024-12-16 提交的版本。查看 最新版本

// ==UserScript==
// @name         ylOppTactsPreview (Modified)
// @namespace    douglaskampl
// @version      2.0
// @description  Shows latest 10 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
// ==/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: 420px;
        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;
    }

    .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 = `
          <div class="title-main">${opponent ? opponent : ''} – ${matchType.toUpperCase()}</div>
          <div class="title-subtitle">
            (Opponent's tactics are represented by black dots with white outlines)
            <span style="display:inline-block;width:8px;height:8px;background:#000;border:1px solid #fff;margin-left:5px;vertical-align:middle;"></span>
          </div>
        `;
        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 + 5;

            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() {
        if (!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 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 = createCanvas(150, 200);
        const context = canvas.getContext('2d');
        const image = new Image();

        image.crossOrigin = 'Anonymous';

        image.onload = function () {
            context.drawImage(image, 0, 0, canvas.width, canvas.height);

            const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;

            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 ? isYellow : isBlack) {
                    data[i] = 0;
                    data[i+1] = 0;
                    data[i+2] = 0;
                } else if (opponentIsHome ? isBlack : isYellow) {
                    const nearbyGreen = context.getImageData(Math.floor(i/4) % canvas.width, Math.floor(i/4/canvas.width), 1, 1).data;
                    data[i] = nearbyGreen[0];
                    data[i+1] = nearbyGreen[1];
                    data[i+2] = nearbyGreen[2];
                }
            }

            const tempData = new Uint8ClampedArray(data);

            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);
        };

        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);
            }
        }
    });
})();