Autodarts Plus

Userscript for Autodarts

目前为 2024-02-28 提交的版本。查看 最新版本

// ==UserScript==
// @id           autodarts-plus@https://github.com/sebudde/autodarts-plus
// @name         Autodarts Plus
// @namespace    https://github.com/sebudde/autodarts-plus
// @version      0.1.0
// @description  Userscript for Autodarts
// @author       sebudde
// @match        https://play.autodarts.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=autodarts.io
// @license      MIT
// @grant        GM.getValue
// @grant        GM.setValue
// ==/UserScript==

(async function() {
    'use strict';

    const CONFIG = {
        match: {
            inactiveSmall: 1,
            caller: 1,
            showThrowSumAtLegFinish: 1,
            showNextLegAfter: 1
        }
    };

    //////////////// CONFIG END ////////////////////

    const readyClasses = {
        play: 'css-1lua7td',
        lobbies: 'css-1q0rlnk',
        table: 'css-p3eaf1', // matches & boards
        match: 'css-ul22ge',
        matchHistory: 'css-10z204m'
    };

    let firstLoad = true;

    let configPathName = '/config';
    const pageContainer = document.createElement('div');

    const observeDOM = (function() {
        const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

        return function(obj, callback) {
            if (!obj || obj.nodeType !== 1) return;
            const mutationObserver = new MutationObserver(callback);
            mutationObserver.observe(obj, {
                attributes: true,
                childList: true,
                subtree: true
            });
            return mutationObserver;
        };
    })();

    //////////////// CSS classes start ////////////////////
    const adp_style = document.createElement('style');
    adp_style.type = 'text/css';
    adp_style.innerHTML = `
        .adp_points-small { font-size: 3em!important; }
    `;
    document.getElementsByTagName('head')[0].appendChild(adp_style);

    let callerData = {};

    ////////////////   ////////////////////

    const setCallerData = async (name, value) => {
        const gmData = await GM.getValue('adp');
        const gmCallerData = gmData?.callerData || {};

        const data = name.split('-');

        const gmCallerValue = gmCallerData[data[0]] || {};
        const newCallerValue = {[data[1]]: value};

        callerData = {
            ...gmCallerData,
            [data[0]]: {...gmCallerValue, ...newCallerValue}
        };

        await GM.setValue('adp', {
            callerData: callerData
        });
    };

    const callerObj = {
        caller1: {
            folder: '0',
            name: 'Caller OFF'
        },
        caller2: {
            folder: '1_male_eng',
            name: 'Male eng',
            server: 'https://autodarts.x10.mx',
            fileExt: '.mp3'
        },
        caller3: {
            folder: 'google_eng',
            name: 'Google eng'
        },
        caller4: {
            folder: 'google_de',
            name: 'Google de'
        }
    };

    const onDOMready = async () => {
        if (firstLoad) {
            firstLoad = false;

            //////////////// caller data  ////////////////////

            const gmData = await GM.getValue('adp');
            const gmCallerData = gmData?.callerData || {};

            callerData = {
                ...gmCallerData, ...callerObj
            };

            await GM.setValue('adp', {
                callerData: callerData
            });

            const callerObjLength = Object.keys(callerObj).length;

            //////////////// add config page  ////////////////////

            pageContainer.classList.add('css-gmuwbf');
            const configContainer = document.createElement('div');
            configContainer.classList.add('css-10z204m');
            pageContainer.appendChild(configContainer);
            configContainer.innerHTML = `
                    <h2 style="font-size: 1.5em; font-weight: 700; align-self: flex-start;">Config</h2>
                    <h3 style="font-size: 1.2em; font-weight: 700; align-self: flex-start;">Caller</h3>
                    `;

            for (let callerCount = callerObjLength + 1; callerCount <= callerObjLength + 5; callerCount++) {
                const callerContainer = document.createElement('div');
                callerContainer.classList.add('css-1p4eqnd');
                callerContainer.style.gap = '2rem';
                const callerServer = callerData[`caller${callerCount}`]?.server || '';
                const callerName = callerData[`caller${callerCount}`]?.name || '';
                const callerFolder = callerData[`caller${callerCount}`]?.folder || '';
                const callerFileExt = callerData[`caller${callerCount}`]?.fileExt || '';

                callerContainer.innerHTML = `
                        <div class="css-1igwmid" style="margin-right: 2em">
                            <b>Caller ${callerCount - callerObjLength}</b>
                        </div>
                        <div class="css-1igwmid">
                            <div class="css-u4ybgy" style="width: 240px"><div class="css-1igwmid"><input placeholder="Server" class="adp_caller--server css-1ndqqtl" name="caller${callerCount}-server" value="${callerServer}"></div></div>
                        </div>
                        <div class="css-1igwmid">
                            <div class="css-u4ybgy"><div class="css-1igwmid"><input placeholder="Name" class="adp_caller--name css-1ndqqtl" name="caller${callerCount}-name" value="${callerName}"></div></div>
                        </div>
                        <div class="css-1igwmid">
                            <div class="css-u4ybgy" style="width: 180px"><div class="css-1igwmid"><input placeholder="Folder"  class="adp_caller--folder css-1ndqqtl" name="caller${callerCount}-folder" value="${callerFolder}"></div></div>
                        </div>
                        <div class="css-1igwmid">
                            <div class="css-u4ybgy" style="width: 100px"><div class="css-1igwmid"><input placeholder="File ext"  class="adp_caller--folder css-1ndqqtl" name="caller${callerCount}-fileExt" value="${callerFileExt}"></div></div>
                        </div>`;
                configContainer.appendChild(callerContainer);
            }

            const input = configContainer.querySelectorAll('input');
            [...input].forEach((el) => (el.addEventListener('blur', (e) => {
                setCallerData(e.target.name, e.target.value);
            })));

            document.getElementById('root').appendChild(pageContainer);

            //////////////// add menu  ////////////////////
            const menuBtn = document.createElement('a');
            menuBtn.classList.add('css-1nlwyv4');
            menuBtn.classList.add('adp_menu-btn');
            menuBtn.innerText = 'Config';
            menuBtn.style.cursor = 'pointer';
            const menuContainer = document.querySelector('.css-1igwmid');
            menuContainer.appendChild(menuBtn);

            [...document.querySelectorAll('.css-1nlwyv4')].forEach((el) => (el.addEventListener('click', async (event) => {
                document.querySelector('#root > div:nth-of-type(2)').style.display = 'flex';
                pageContainer.style.display = 'none';
                if (event.target.classList.contains('adp_menu-btn')) {
                    // switch to page "Matches History" because we need its CSS
                    menuContainer.querySelector('a:nth-of-type(4)').click();
                    window.history.pushState(null, '', configPathName);
                }

            }, false)));

        }

        console.log('DOM ready:');
    };

    const showConfig = () => {
        console.log('showConfig');
    };

    ////////////////  end ////////////////////

    const handleConfigPage = () => {
        console.log('config ready');
        document.querySelector('#root > div:nth-of-type(2)').style.display = 'none';
        pageContainer.style.display = 'flex';
    };

    const handlePlay = () => {
        console.log('play ready');
    };

    const handleMatchHistory = () => {
        console.log('matchHistory ready');
        if (location.pathname === configPathName) {
            handleConfigPage();
        }
    };

    const handleMatches = () => {
        console.log('matches ready');
    };

    const handleMatch = async () => {
        console.log('match ready!');

        // iOS fix
        // https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari
        const soundEffect1 = new Audio();
        soundEffect1.autoplay = true;
        const soundEffect2 = new Audio();
        soundEffect2.autoplay = true;

        const matchMenuRow = document.createElement('div');
        matchMenuRow.classList.add('css-k008qs');
        matchMenuRow.style.marginTop = 'calc(var(--chakra-space-2) * -1 - 4px)';

        const matchMenuContainer = document.createElement('div');
        matchMenuContainer.classList.add('css-a6m3v9');

        matchMenuRow.appendChild(matchMenuContainer);

        document.querySelector('.css-k008qs').after(matchMenuRow);

        // PR font-size larger
        [...document.querySelectorAll('.css-1n5vwgq .css-qqfgvy')].forEach((el) => (el.style.fontSize = 'var(--chakra-fontSizes-xl)'));
        [...document.querySelectorAll('.css-1memit')].forEach((el) => (el.style.marginTop = '-4px'));
        [...document.querySelectorAll('.css-x3m75h')].forEach((el) => (el.style.lineHeight = '148px'));

        const matchVariant = document.querySelector('.css-1xbroe7').innerText;
        if (matchVariant !== 'X01') return;
        let matchId = '';

        const counterContainer = document.querySelector('.css-oyptjf');

        const uuidLength = 36;
        const pathName = location.pathname;
        if (pathName.indexOf('matches/') >= 0) {
            matchId = pathName.split('matches/')[1];
            if (matchId) matchId = matchId.substring(0, uuidLength);
        }

        let callerActive = (await GM.getValue('callerActive')) || '0';
        let triplesound = (await GM.getValue('triplesound')) || '0';
        let boosound = (await GM.getValue('boosound')) || false;
        let nextLegAfterSec = (await GM.getValue('nextLegAfterSec')) || 'OFF';

        if (CONFIG.match.caller === 1) {
            const onSelectChange = (event) => {
                (async () => {
                    eval(event.target.id + ' = event.target.value');
                    await GM.setValue(event.target.id, event.target.value);
                })();
            };

            const setActiveAttr = (el, isActive) => {
                if (isActive) {
                    el.setAttribute('data-active', '');
                } else {
                    el.removeAttribute('data-active');
                }
            };

            const tripleSoundArr = [
                {
                    value: '0',
                    name: 'Triple OFF'
                }, {
                    value: '1',
                    name: 'Beep'
                }, {
                    value: '2',
                    name: 'Löwen'
                }];

            const nextLegSecArr = [
                {
                    value: 'OFF'
                }, {
                    value: '0'
                }, {
                    value: '5'
                }, {
                    value: '10'
                }, {
                    value: '20'
                }];

            const callerSelect = document.createElement('select');
            callerSelect.id = 'callerActive';
            callerSelect.classList.add('css-1xbroe7');
            callerSelect.style.padding = '0 5px';
            callerSelect.onchange = onSelectChange;

            matchMenuContainer.appendChild(callerSelect);

            for (const [caller, data] of Object.entries(callerData)) {
                if (!data.folder) continue;
                const optionEl = document.createElement('option');
                optionEl.value = caller;
                optionEl.text = data.name || data.folder;
                optionEl.style.backgroundColor = '#353d47';
                if (callerActive === caller) optionEl.setAttribute('selected', 'selected');
                callerSelect.appendChild(optionEl);
            }

            const tripleSoundSelect = document.createElement('select');
            tripleSoundSelect.id = 'triplesound';
            tripleSoundSelect.classList.add('css-1xbroe7');
            tripleSoundSelect.style.padding = '0 5px';
            tripleSoundSelect.onchange = onSelectChange;

            matchMenuContainer.appendChild(tripleSoundSelect);

            tripleSoundArr.forEach((triple) => {
                const optionEl = document.createElement('option');
                optionEl.value = triple.value;
                optionEl.text = triple.name;
                optionEl.style.backgroundColor = '#353d47';
                if (triplesound === triple.value) optionEl.setAttribute('selected', 'selected');
                tripleSoundSelect.appendChild(optionEl);
            });

            const booBtn = document.createElement('button');
            booBtn.id = 'boosound';
            booBtn.innerText = 'BOO';
            booBtn.classList.add('css-1xbmrf2');
            booBtn.style.height = 'var(--chakra-sizes-8)';
            booBtn.style.padding = '0 var(--chakra-space-4)';
            booBtn.style.margin = '0';
            setActiveAttr(booBtn, boosound);
            matchMenuContainer.appendChild(booBtn);

            booBtn.addEventListener('click', async (event) => {
                const isActive = event.target.hasAttribute('data-active');
                setActiveAttr(booBtn, !isActive);
                boosound = !isActive;
                await GM.setValue('boosound', !isActive);
            }, false);

            const nextLegSecSelect = document.createElement('select');
            nextLegSecSelect.id = 'nextLegAfterSec';
            nextLegSecSelect.classList.add('css-1xbroe7');
            nextLegSecSelect.style.padding = '0 5px';
            nextLegSecSelect.onchange = onSelectChange;

            matchMenuContainer.appendChild(nextLegSecSelect);

            nextLegSecArr.forEach((sec) => {
                const optionEl = document.createElement('option');
                optionEl.value = sec.value;
                optionEl.text = `Next Leg ${sec.value}s`;
                optionEl.style.backgroundColor = '#353d47';
                if (nextLegAfterSec === sec.value) optionEl.setAttribute('selected', 'selected');
                nextLegSecSelect.appendChild(optionEl);
            });

            const hideHeaderBtn = document.createElement('button');
            hideHeaderBtn.id = 'hideHeader';
            hideHeaderBtn.innerText = 'Header';
            hideHeaderBtn.classList.add('css-1xbmrf2');
            hideHeaderBtn.style.height = 'var(--chakra-sizes-8)';
            hideHeaderBtn.style.padding = '0 var(--chakra-space-4)';
            hideHeaderBtn.style.margin = 0;

            let hideHeaderGM = await GM.getValue('hideHeader');

            const headerEl = document.querySelector('.css-gmuwbf');
            const mainContainerEl = document.querySelector('.css-1lua7td');

            if (hideHeaderGM) {
                headerEl.style.display = 'none';
                mainContainerEl.style.height = '100vh';
            }

            setActiveAttr(hideHeaderBtn, !hideHeaderGM);

            matchMenuContainer.appendChild(hideHeaderBtn);

            hideHeaderBtn.addEventListener('click', async (event) => {
                const isActive = event.target.hasAttribute('data-active');
                setActiveAttr(hideHeaderBtn, !isActive);
                headerEl.style.display = isActive ? 'none' : 'flex';
                mainContainerEl.style.height = isActive ? '100vh' : 'calc(100vh - 72px)';

                await GM.setValue('hideHeader', isActive);
            }, false);

            // ######### start iOS fix #########
            // https://stackoverflow.com/questions/31776548/why-cant-javascript-play-audio-files-on-iphone-safari

            const isiOS = [
                    'iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(navigator.platform) || // iPad on iOS 13 detection
                (navigator.userAgent.includes('Mac') && 'ontouchend' in document);

            if (isiOS) {
                const startBtnContainer = document.createElement('div');
                startBtnContainer.style.position = 'absolute';
                startBtnContainer.style.height = '100%';
                startBtnContainer.style.width = '100%';
                startBtnContainer.style.top = '0';
                startBtnContainer.style.left = '0';
                startBtnContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
                startBtnContainer.style.display = 'flex';
                startBtnContainer.style.justifyContent = 'center';
                startBtnContainer.style.alignItems = 'center';

                document.querySelector('#root').appendChild(startBtnContainer);

                const startBtn = document.createElement('button');
                startBtn.id = 'startBtn';
                startBtn.innerText = 'START';
                startBtn.classList.add('css-1xbmrf2');
                startBtn.style.background = '#ffffff';
                startBtn.style.color = '#646464';
                startBtn.style.fontSize = '36px';
                startBtn.style.padding = '36px 24px';
                startBtnContainer.appendChild(startBtn);

                startBtn.addEventListener('click', async (event) => {
                    soundEffect1.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA';
                    startBtnContainer.remove();
                }, false);

                // ######### end iOS fix #########
            }
        }

        function playSound1(fileName) {
            // console.log('fileName1', fileName);
            soundEffect1.src = fileName;
        }

        function playSound2(fileName) {
            // console.log('fileName2', fileName);
            soundEffect2.src = fileName;
        }

        const caller = async () => {
            const soundServerUrl = 'https://autodarts-plus.x10.mx';

            const callerFolder = callerData[callerActive]?.folder || '';
            const callerServerUrl = callerData[callerActive]?.server || '';
            const fileExt = callerData[callerActive]?.fileExt || '.mp3';

            const turnPoints = counterContainer.firstChild.innerText.trim();
            const throwPointsArr = [...counterContainer.querySelectorAll('.css-dfewu8, .css-rzdgh7')].map((el) => el.innerText);

            const curThrowPointsName = throwPointsArr.slice(-1)[0];

            const winnerContainer = document.querySelector('.css-e9w8hh');

            let curThrowPointsNumber = 0;
            let curThrowPointsBed = '';
            let curThrowPointsMultiplier = 1;

            if (curThrowPointsName) {
                if (curThrowPointsName.startsWith('M')) {
                    curThrowPointsBed = 'Outside';
                } else if (curThrowPointsName === 'Bull') {
                    curThrowPointsNumber = 25;
                    curThrowPointsBed = 'D';
                } else if (curThrowPointsName === '25') {
                    curThrowPointsNumber = 25;
                    curThrowPointsBed = 'S';
                } else {
                    curThrowPointsNumber = curThrowPointsName.slice(1);
                    curThrowPointsBed = curThrowPointsName.charAt(0);
                }

                if (curThrowPointsBed === 'D') curThrowPointsMultiplier = 2;
                if (curThrowPointsBed === 'T') curThrowPointsMultiplier = 3;
            }

            if (turnPoints === 'BUST') {
                if (callerFolder.length && callerServerUrl.length) playSound1(callerServerUrl + '/' + callerFolder + '/' + '0' + fileExt);
            } else {
                if (curThrowPointsName === 'BULL') {
                    if (triplesound === '1') {
                        playSound1(soundServerUrl + '/' + 'beep_1.mp3');
                    }
                    if (triplesound === '2') {
                        playSound1(soundServerUrl + '/' + 'beep_2_bullseye.mp3');
                    }
                } else if (curThrowPointsBed === 'Outside') {
                    if (boosound === true) {
                        const randomMissCount = Math.floor(Math.random() * 3) + 1;
                        playSound1(soundServerUrl + '/' + 'miss_' + randomMissCount + '.mp3');
                    }
                } else {
                    if (curThrowPointsMultiplier === 3) {
                        if (triplesound === '1') {
                            playSound1(soundServerUrl + '/' + 'beep_1.mp3');
                        }
                        if (triplesound === '2' && curThrowPointsNumber >= 17) {
                            playSound1(soundServerUrl + '/' + 'beep_2_' + curThrowPointsNumber + '.wav');
                        }
                    }
                }

                if (throwPointsArr.length === 3 && callerFolder.length) {
                    if (callerFolder.startsWith('google')) {
                        playSound1('https://autodarts.de.cool/mp3_helper.php?language=' + callerFolder.substring(7, 9) + '&text=' + turnPoints);
                    } else {
                        if (callerFolder.length && callerServerUrl.length) playSound1(callerServerUrl + '/' + callerFolder + '/' + turnPoints + '.mp3');
                    }
                }

                if (winnerContainer) {
                    const waitForSumCalling = throwPointsArr.length === 3 ? 2500 : 0;

                    setTimeout(() => {
                        const buttons = [...document.querySelectorAll('button.css-1x1xjw8, button.css-1vfwxw0')];
                        buttons.forEach((button) => {
                            // --- Leg finished ---
                            if (button.innerText === 'Next Leg') {
                                if (callerFolder.length && callerServerUrl.length) playSound1(callerServerUrl + '/' + callerFolder + '/' + 'gameshot.mp3');
                            }
                            // --- Match finished ---
                            if (button.innerText === 'Finish') {
                                console.log('finish');
                                if (callerFolder.length && callerServerUrl.length) playSound1(callerServerUrl + '/' + callerFolder + '/' + 'gameshot and the match.mp3');
                                setTimeout(() => {
                                    playSound2(soundServerUrl + '/' + 'chase_the_sun.mp3');
                                }, 1000);
                            }
                        });
                    }, waitForSumCalling);
                }
            }
        };

        const onCounterChange = async () => {
            if (CONFIG.match.caller === 1) caller();

            if (CONFIG.match.inactiveSmall === 1) {
                [...document.querySelectorAll('.css-x3m75h')].forEach((el) => (el.classList.remove('adp_points-small')));
                [...document.querySelectorAll('.css-1a28glk .css-x3m75h')].forEach((el) => (el.classList.add('adp_points-small')));
            }

            if (CONFIG.match.showThrowSumAtLegFinish || CONFIG.match.showNextLegAfter) {
                const winnerContainer = document.querySelector('.css-e9w8hh');

                if (winnerContainer) {
                    // --- Leg finished ---
                    console.log('Leg finished');

                    if (CONFIG.match.showThrowSumAtLegFinish) {
                        const throwRound = document.querySelector('.css-1tw9fat')?.innerText?.split('/')[0]?.substring(1);
                        const throwThisRound = document.querySelectorAll('.css-1chp9v4, .css-ucdbhl').length;

                        const throwsSum = (throwRound - 1) * 3 + throwThisRound;

                        const throwsSumEl = document.createElement('span');
                        throwsSumEl.style.fontSize = '0.5em';
                        throwsSumEl.innerHTML = throwsSum + ' Darts';

                        const sumContainerEl = winnerContainer.querySelector('.css-x3m75h');
                        sumContainerEl.replaceChildren(throwsSumEl);
                    }

                    if (CONFIG.match.showNextLegAfter) {
                        const buttons = [...document.querySelectorAll('button.css-1vfwxw0')];
                        buttons.forEach((button) => {
                            if (button.innerText === 'Next Leg') {
                                if (!parseInt(nextLegAfterSec)) return;
                                setTimeout(() => {
                                    button.click();
                                }, parseInt(nextLegAfterSec) * 1000);
                            }
                        });
                    }
                }
            }
        };

        if (!matchId) {
            console.error('matchId could not be extracted from URL');
        } else {
            onCounterChange();

            observeDOM(counterContainer, async function(m) {
                onCounterChange();
            });
        }
    };

    const handleBoards = () => {
        console.log('boards ready');
    };

    const readyClassesValues = Object.values(readyClasses);

    observeDOM(document.getElementById('root'), function(mutationrecords) {
        mutationrecords.some((record) => {
            if (record.addedNodes.length && record.addedNodes[0].classList?.length) {
                const elemetClassList = [...record.addedNodes[0].classList];
                return elemetClassList.some((className) => {
                    if (className.startsWith('css-')) {
                        // console.log('className', className);
                        if (!readyClassesValues.includes(className)) return false;
                        const key = Object.keys(readyClasses).find((key) => readyClasses[key] === className);
                        if (key) {
                            onDOMready();
                            switch (key) {
                                case 'play':
                                    handlePlay();
                                    break;
                                case 'table':
                                    document.querySelector('.' + className).children[0].classList.contains('css-12pccnb') && handleBoards();
                                    document.querySelector('.' + className).children[0].classList.contains('css-5605sr') && handleMatches();
                                    break;
                                case 'match':
                                    handleMatch();
                                    break;
                                case 'boards':
                                    handleBoards();
                                    break;
                                case 'matchHistory':
                                    handleMatchHistory();
                                    break;
                            }
                            return true;
                        }
                    }
                });
            }
        });
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址