Greasy Fork

MZ - Ongoing Match Results

Tracks ongoing matches results and updates standings

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

// ==UserScript==
// @name         MZ - Ongoing Match Results
// @namespace    douglaskampl
// @version      2.0
// @description  Tracks ongoing matches results and updates standings
// @author       Douglas Vieira
// @match        https://www.managerzone.com/?p=league*
// @match        https://www.managerzone.com/?p=friendlyseries*
// @match        https://www.managerzone.com/?p=private_cup*
// @match        https://www.managerzone.com/?p=cup*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        #nprogress {
            pointer-events: none;
        }
        #nprogress .bar {
            background: linear-gradient(to right, #ff6600, #ff9966);
            position: fixed;
            z-index: 1031;
            top: 0;
            left: 0;
            width: 100%;
            height: 2px;
            box-shadow: 0 1px 3px rgba(255, 102, 0, 0.2);
        }
        .status-message {
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0, 0, 0, 0.8);
            color: #ff6600;
            padding: 8px 15px;
            border-radius: 4px;
            font-size: 14px;
            z-index: 1000;
            animation: fadeInOut 0.3s ease;
        }
        @keyframes fadeInOut {
            0% { opacity: 0; transform: translateY(-10px); }
            100% { opacity: 1; transform: translateY(0); }
        }
        .mz-modal {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0, 0, 0, 0.9);
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(255, 102, 0, 0.2);
            z-index: 1100;
            max-width: 300px;
            max-height: 400px;
            overflow-y: auto;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            opacity: 0;
            transform: translateY(20px);
            color: #ff6600;
            border: 1px solid #ff6600;
        }
        .mz-modal.show {
            opacity: 1;
            transform: translateY(0);
        }
        .mz-modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #ff6600;
        }
        .mz-modal-close {
            cursor: pointer;
            padding: 5px 10px;
            background: #ff6600;
            color: black;
            border: none;
            border-radius: 4px;
            transition: all 0.2s ease;
        }
        .mz-modal-close:hover {
            background: #ff9966;
        }
        .mz-modal-content {
            margin-bottom: 15px;
            font-size: 14px;
        }
        .mz-match-result {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px;
            border-bottom: 1px solid rgba(255, 102, 0, 0.3);
        }
        .mz-match-result:last-child {
            border-bottom: none;
        }
    `;
    document.head.appendChild(style);

    const UI = {
        BUTTON_STYLES: {
            backgroundColor: '#1a1a1a',
            color: '#ff6600',
            border: '1px solid #ff6600',
            marginLeft: '10px',
            cursor: 'pointer',
            padding: '5px 10px',
            borderRadius: '4px',
            transition: 'all 0.2s ease'
        },
        BUTTON_STATES: {
            READY: 'Get match results',
            FETCHING: 'Processing...',
            DONE: 'Results updated'
        },
        LOADING_MESSAGES: {
            MATCHES: 'Fetching matches...',
            RESULTS: 'Processing results...',
            UPDATING: 'Updating standings...'
        }
    };

    const SELECTORS = {
        TRACK_BUTTONS: [
            '[id^="trackButton_u18_world_series_"]',
            '[id^="trackButton_u18_series_"]',
            '[id^="trackButton_friendlyseries_"]',
            '[id^="trackButton_privatecup_"]',
            '[id^="trackButton_cup_"]'
        ],
        MATCHES_TABLE: 'table.hitlist',
        STANDINGS_TABLE: 'table.nice_table'
    };

    class MatchTracker {
        constructor() {
            this.matchResults = new Map();
            this.isFriendlySeries = window.location.href.includes('friendlyseries');
            this.isPrivateCup = window.location.href.includes('private_cup');
            this.isCup = window.location.href.includes('p=cup');
            this.hasRun = false;
            this.init();
        }

        showStatusMessage(message) {
            const existingMessage = document.querySelector('.status-message');
            if (existingMessage) {
                existingMessage.remove();
            }

            const messageElement = document.createElement('div');
            messageElement.className = 'status-message';
            messageElement.textContent = message;
            document.body.appendChild(messageElement);

            setTimeout(() => {
                messageElement.style.opacity = '0';
                setTimeout(() => messageElement.remove(), 300);
            }, 2000);
        }

        async init() {
            const matches = await this.getMatches();

            if (!matches || !matches.length) {
                console.log('No ongoing matches found');
                return;
            }

            this.setupUI(matches);
        }

        async getMatches() {
            if (this.isFriendlySeries) {
                return await this.getFriendlySeriesMatches();
            } else if (this.isPrivateCup || this.isCup) {
                return this.getCupMatches();
            }
            return this.getLeagueMatches();
        }

        getCupMatches() {
            const groupStages = document.querySelector('#group-stages');
            if (!groupStages) return [];

            return Array.from(groupStages.querySelectorAll('table.hitlist tr'))
                .filter(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    if (!link) return false;
                    const score = link.textContent.trim();
                    return !score.match(/^\d+\s*-\s*\d+$/) && !score.match(/^X\s*-\s*X$/);
                })
                .map(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    const homeTeam = row.querySelector('td:first-child').textContent.trim();
                    const awayTeam = row.querySelector('td:last-child').textContent.trim();
                    const params = new URLSearchParams(link.href);
                    return {
                        mid: params.get('mid'),
                        homeTeam,
                        awayTeam
                    };
                });
        }

        setupUI(matches) {
            const trackButton = this.findTrackButton();
            if (!trackButton) {
                console.error('Track button not found');
                return;
            }

            const fetchButton = this.createFetchButton();
            trackButton.parentNode.insertBefore(fetchButton, trackButton.nextSibling);

            fetchButton.addEventListener('mouseenter', () => {
                if (!this.hasRun) {
                    fetchButton.style.backgroundColor = '#333';
                }
            });

            fetchButton.addEventListener('mouseleave', () => {
                if (!this.hasRun) {
                    fetchButton.style.backgroundColor = '#1a1a1a';
                }
            });

            fetchButton.addEventListener('click', () => {
                if (!this.hasRun) {
                    this.handleFetchClick(fetchButton, matches);
                }
            });
        }

        findTrackButton() {
            return SELECTORS.TRACK_BUTTONS.reduce((found, selector) =>
                found || document.querySelector(selector), null);
        }

        createFetchButton() {
            const button = document.createElement('button');
            Object.assign(button.style, UI.BUTTON_STYLES);
            button.textContent = UI.BUTTON_STATES.READY;
            return button;
        }

        showLoadingOverlay(message) {
            let overlay = document.querySelector('.status-overlay');
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'status-overlay';
                document.body.appendChild(overlay);
            }
            overlay.textContent = message;
        }

        hideLoadingOverlay() {
            const overlay = document.querySelector('.status-overlay');
            if (overlay) {
                overlay.remove();
            }
        }

        showResultsModal(results) {
            const modal = document.createElement('div');
            modal.className = 'mz-modal';

            const header = document.createElement('div');
            header.className = 'mz-modal-header';
            header.innerHTML = `
                <h3>Match Results</h3>
                <button class="mz-modal-close">Close</button>
            `;

            const content = document.createElement('div');
            content.className = 'mz-modal-content';

            results.forEach(result => {
                const matchDiv = document.createElement('div');
                matchDiv.className = 'mz-match-result';
                matchDiv.textContent = `${result.homeTeam} ${result.score} ${result.awayTeam}`;
                content.appendChild(matchDiv);
            });

            modal.appendChild(header);
            modal.appendChild(content);
            document.body.appendChild(modal);

            setTimeout(() => modal.classList.add('show'), 10);

            modal.querySelector('.mz-modal-close').addEventListener('click', () => {
                modal.classList.remove('show');
                setTimeout(() => modal.remove(), 300);
            });
        }

        async handleFetchClick(button, matches) {
            if (this.hasRun) return;

            this.hasRun = true;
            NProgress.configure({ showSpinner: false });
            NProgress.start();
            this.showStatusMessage(UI.LOADING_MESSAGES.MATCHES);

            if (!matches.length) {
                NProgress.done();
                return;
            }

            button.disabled = true;
            button.textContent = UI.BUTTON_STATES.FETCHING;
            button.style.opacity = '0.7';
            button.style.cursor = 'not-allowed';

            this.showStatusMessage(UI.LOADING_MESSAGES.RESULTS);
            const results = await this.processMatches(matches);

            if (this.isFriendlySeries || this.isPrivateCup || this.isCup) {
                this.showResultsModal(results);
            }

            this.showStatusMessage(UI.LOADING_MESSAGES.UPDATING);
            this.updateAllTeamStats();

            button.textContent = UI.BUTTON_STATES.DONE;
            button.style.backgroundColor = '#333';
            button.style.borderColor = '#666';
            button.style.color = '#999';

            NProgress.done();
            this.showStatusMessage('All updates complete');
        }

        getLeagueMatches() {
            const matchesTable = document.querySelector(SELECTORS.MATCHES_TABLE);
            if (!matchesTable) return [];

            return Array.from(matchesTable.querySelectorAll('tr'))
                .filter(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    if (!link) return false;
                    const score = link.textContent.trim();
                    return !score.match(/^\d+\s*-\s*\d+$/) && !score.match(/^X\s*-\s*X$/);
                })
                .map(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    const homeTeam = row.querySelector('td:first-child').textContent.trim();
                    const awayTeam = row.querySelector('td:last-child').textContent.trim();
                    const params = new URLSearchParams(link.href);
                    return {
                        mid: params.get('mid'),
                        homeTeam,
                        awayTeam
                    };
                });
        }

        async getFriendlySeriesMatches() {
            const fsidMatch = window.location.href.match(/fsid=(\d+)/);
            if (!fsidMatch) return [];

            try {
                const response = await fetch(
                    `https://www.managerzone.com/ajax.php?p=friendlySeries&sub=matches&fsid=${fsidMatch[1]}&sport=soccer`
                );
                const text = await response.text();
                const doc = new DOMParser().parseFromString(text, "text/html");
                const matchRows = Array.from(doc.querySelectorAll('table.hitlist tr'));

                return matchRows
                    .filter(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    if (!link) return false;
                    const score = link.textContent.trim();
                    return !score.match(/^\d+\s*-\s*\d+$/) && !score.match(/^X\s*-\s*X$/);
                })
                    .map(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    const homeTeam = row.querySelector('td:first-child').textContent.trim();
                    const awayTeam = row.querySelector('td:last-child').textContent.trim();
                    const params = new URLSearchParams(link.href);
                    return {
                        mid: params.get('mid'),
                        homeTeam,
                        awayTeam
                    };
                });
            } catch (error) {
                console.error('Error fetching friendly series matches:', error);
                return [];
            }
        }

        async processMatches(matches) {
            const results = [];
            const total = matches.length;

            for (let i = 0; i < matches.length; i++) {
                const match = matches[i];
                try {
                    const response = await fetch(
                        `https://www.managerzone.com/xml/match_info.php?sport_id=1&match_id=${match.mid}`
                    );
                    const text = await response.text();
                    const matchData = this.parseMatchResponse({ responseText: text });

                    if (matchData) {
                        this.matchResults.set(match.mid, matchData);
                        this.updateMatchDisplay(match.mid, matchData);
                        results.push({
                            ...match,
                            score: `${matchData.homeGoals}-${matchData.awayGoals}`
                        });
                    }
                } catch (error) {
                    console.error(`Error processing match ${match.mid}:`, error);
                }

                NProgress.set((i + 1) / total);
            }

            return results;
        }

        parseMatchResponse(response) {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(response.responseText, "application/xml");
            const matchNode = xmlDoc.querySelector('Match');
            if (!matchNode) return null;

            const homeTeam = matchNode.querySelector('Team[field="home"]');
            const awayTeam = matchNode.querySelector('Team[field="away"]');
            if (!homeTeam || !awayTeam) return null;

            return {
                homeTid: homeTeam.getAttribute('id'),
                awayTid: awayTeam.getAttribute('id'),
                homeGoals: parseInt(homeTeam.getAttribute('goals'), 10) || 0,
                awayGoals: parseInt(awayTeam.getAttribute('goals'), 10) || 0
            };
        }

        updateMatchDisplay(mid, matchData) {
            const link = Array.from(document.links)
                .find(link => link.href.includes(`mid=${mid}`));
            if (link) {
                link.textContent = `${matchData.homeGoals}-${matchData.awayGoals}`;
            }
        }

        calculateMatchResult(matchData) {
            if (matchData.homeGoals > matchData.awayGoals) {
                return {
                    home: { points: 3, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 0, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            } else if (matchData.homeGoals < matchData.awayGoals) {
                return {
                    home: { points: 0, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 3, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            } else {
                return {
                    home: { points: 1, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 1, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            }
        }

        findTeamRows(tid) {
            const teamLinks = Array.from(document.querySelectorAll(`a[href*="tid=${tid}"]`));
            const rowsSet = new Set();

            teamLinks.forEach(link => {
                const row = link.closest('tr');
                if (row) rowsSet.add(row);
            });

            const highlightedRows = Array.from(document.querySelectorAll('.highlight_row'))
                .filter(row => row.querySelector(`a[href*="tid=${tid}"]`));
            highlightedRows.forEach(row => rowsSet.add(row));

            return Array.from(rowsSet);
        }

        updateAllTeamStats() {
            this.matchResults.forEach((matchData) => {
                const result = this.calculateMatchResult(matchData);
                this.updateTeamRow(matchData.homeTid, result.home);
                this.updateTeamRow(matchData.awayTid, result.away);
            });
        }

        updateTeamRow(tid, result) {
            const teamRows = this.findTeamRows(tid);
            if (!teamRows.length) return;

            teamRows.forEach(row => {
                const cells = row.querySelectorAll('td');
                if (cells.length < 10) return;

                const parseCell = cell => parseInt(cell.textContent, 10) || 0;

                cells[2].textContent = parseCell(cells[2]) + 1;

                if (result.points === 3) {
                    cells[3].textContent = parseCell(cells[3]) + 1;
                } else if (result.points === 1) {
                    cells[4].textContent = parseCell(cells[4]) + 1;
                } else {
                    cells[5].textContent = parseCell(cells[5]) + 1;
                }

                cells[6].textContent = parseCell(cells[6]) + result.goalsFor;
                cells[7].textContent = parseCell(cells[7]) + result.goalsAgainst;

                const goalDiff = parseCell(cells[6]) - parseCell(cells[7]);
                const goalDiffElem = cells[8].querySelector('nobr');
                if (goalDiffElem) {
                    goalDiffElem.textContent = goalDiff;
                }

                cells[9].textContent = parseCell(cells[9]) + result.points;
            });

            this.sortTableByPoints();
        }

        sortTableByPoints() {
            const table = document.querySelector(SELECTORS.STANDINGS_TABLE);
            if (!table) return;

            const tbody = table.querySelector('tbody');
            if (!tbody) return;

            const columnHeaders = Array.from(table.querySelectorAll('thead tr th'));
            const columnCount = columnHeaders.length;

            const dataRows = Array.from(tbody.querySelectorAll('tr')).filter(row => {
                const cells = row.querySelectorAll('td');
                return cells.length >= 10 && cells[2] && !isNaN(parseInt(cells[2].textContent));
            });

            dataRows.sort((a, b) => {
                const getCellValue = (row, index) => {
                    const cell = row.querySelectorAll('td')[index];
                    return parseInt(cell.textContent.trim(), 10) || 0;
                };

                const pointsA = getCellValue(a, 9);
                const pointsB = getCellValue(b, 9);
                if (pointsB !== pointsA) return pointsB - pointsA;

                const goalDiffA = getCellValue(a, 6) - getCellValue(a, 7);
                const goalDiffB = getCellValue(b, 6) - getCellValue(b, 7);
                if (goalDiffB !== goalDiffA) return goalDiffB - goalDiffA;

                return getCellValue(b, 6) - getCellValue(a, 6);
            });

            const nonDataRows = Array.from(tbody.children).filter(row => !dataRows.includes(row));

            const tempContainer = document.createElement('tbody');

            dataRows.forEach((row, index) => {
                const positionCell = row.querySelector('td:first-child span');
                if (positionCell) {
                    positionCell.textContent = (index + 1).toString();
                }

                row.className = index % 2 === 0 ? '' : 'highlight_row';

                row.style.borderBottom = '';
                if (index === 0) {
                    row.style.borderBottom = '2px solid green';
                } else if (index === 1) {
                    row.style.borderBottom = '2px dashed #556B2F';
                } else if (index === 7) {
                    row.style.borderBottom = '2px solid red';
                }

                tempContainer.appendChild(row);
            });

            nonDataRows.forEach(row => tempContainer.appendChild(row));

            tbody.parentNode.replaceChild(tempContainer, tbody);
        }
    }

    setTimeout(() => new MatchTracker(), 3333);
})();