[satology] Auto Claim Multiple Faucets with Monitor UI

Automatic rolls and claims for 50+ crypto faucets/PTC/miners (Freebitco.in BTC, auto promo code for 16 CryptosFaucet, FaucetPay, StormGain, etc)

目前为 2023-05-24 提交的版本。查看 最新版本

// ==UserScript==
// @name         [satology] Auto Claim Multiple Faucets with Monitor UI
// @description  Automatic rolls and claims for 50+ crypto faucets/PTC/miners (Freebitco.in BTC, auto promo code for 16 CryptosFaucet, FaucetPay, StormGain, etc)
// @description  Claim free ADA, BNB, BCH, BTC, DASH, DGB, DOGE, ETH, FEY, LINK, LTC, NEO, SHIB, STEAM, TRX, USDC, USDT, XEM, XRP, ZEC, ETC
// @version      3.0.13
// @author       satology
// @namespace    satology.onrender.com
// @homepage     https://criptologico.com/tools/cc

// @grant        GM_info
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        window.close
// @grant        GM_openInTab
// @grant        window.onurlchange
// @connect      criptologico.com


// @note         IMPORTANT
// @note         - To start the script you need to navigate to https://criptologico.com/tools/cc
// @note         - Each schedule will open it's own tab to allow multiclaiming

// @icon         https://www.google.com/s2/favicons?domain=criptologico.com
// @match        https://app.stormgain.com/crypto-miner/
// @match        https://freecardano.com/*
// @match        https://freebinancecoin.com/*
// @match        https://freebitcoin.io/*
// @match        https://freedash.io/*
// @match        https://free-doge.com/*
// @match        https://freeethereum.com/*
// @match        https://freecryptom.com/*
// @match        https://free-ltc.com/*
// @match        https://freeneo.io/*
// @match        https://freesteam.io/*
// @match        https://free-tron.com/*
// @match        https://freeusdcoin.com/*
// @match        https://freetether.com/*
// @match        https://freenem.com/*
// @match        https://freeshibainu.com/*
// @match        https://coinfaucet.io/*
// @match        https://freebitco.in/*
// @match        https://faucetpay.io/*
// @match        https://bigbtc.win/*
// @match        https://www.bestchange.com/*
// @match        https://faucetok.net/*
// @match        https://betfury.io/boxes/all*
// @match        https://www.free-doge.io/
// @match        https://www.free-doge.io/free/
// @match        https://autofaucet.dutchycorp.space/login.php*
// @match        https://autofaucet.dutchycorp.space/roll.php*
// @match        https://autofaucet.dutchycorp.space/coin_roll.php*
// @match        https://express.dutchycorp.space/index.php*
// @match        https://express.dutchycorp.space/roll.php*
// @match        https://express.dutchycorp.space/coin_roll.php*
// @match        https://faucetcrypto.com/dashboard
// @match        https://faucetcrypto.com/task/faucet-claim
// @match        https://faucetcrypto.com/ptc/*
// @match        https://faucetcrypto.com/task/ptc-advertisement/*
// @match        https://faupig-bit.online/page/dashboard*
// @match        https://faupig-bit.online/account/login/not-logged-in
// @match        https://freepancake.com/*
// @match        https://freegridco.in/*
// @match        https://freematic.com/*
// @match        https://freebch.fun/page/dashboard*
// @match        https://freebch.fun/account/login/not-logged-in
// @match        https://james-trussy.com/*
// @match        https://www.only1024.com/f*
// @match        https://criptologico.com/tools/cc*
// @match        https://freebittorrent.com/*
// @match        https://freebfg.com/*
// @match        https://yescoiner.com/*
// @match        https://coindiversity.io/*
// @match        https://bscads.com/*
// @match        https://ltcfaucet.top/*
// @match        https://bnbfaucet.top/*
// @match        https://dogecoinfaucet.top/*
// @match        https://tronfaucet.top/*
// @match        https://tronfaucet.top/*
// @match        https://ethfaucet.top/*
// ==/UserScript==

(function() {
    'use strict';
    const localeConfig = {
        setToEnglish: true, // will set the faucets to English
        stringSearches: {
            promoCodeAccepted: 'roll',
            promoCodeUsed: 'already used',
            promoCodeInvalid: ['not found', 'only alphanumeric'],
            promoCodeExpired: ['ended']
        }
    };

    const STATUS = {
        INITIALIZING: 0,
        IDLE: 1,
        CLAIMING: 2
    };

    const K = Object.freeze({
        WebType: {
            UNDEFINED: 0,
            CRYPTOSFAUCETS: 1,
            STORMGAIN: 2,
            FREEBITCOIN: 3,
            FAUCETPAY: 4,
            FREELITECOIN: 5,
            FREEETHEREUMIO: 6,
            BAGIKERAN: 7,
            OKFAUCET: 8,
            BIGBTC: 9,
            BESTCHANGE: 10,
            KINGBIZ: 11,
            BFBOX: 13,
            FREEDOGEIO: 14,
            DUTCHYROLL: 15,
            FCRYPTO: 16,
            CPU: 17,
            CBG: 18,
            FPB: 19,
            G8: 20,
            FREEGRC: 21,
            HELI: 22,
            VIE: 23,
            O24: 24,
            YCOIN: 25,
            CDIVERSITY: 26,
            BSCADS: 27,
            CTOP: 28
        },
        CF: {
            UrlType: {
                HOME: 0,
                FREE: 1,
                CONTACTTWITTER: 2,
                PROMOTION: 3,
                STATS: 4,
                SETTINGS: 5,
                FREEROLLS: 6,
                IGNORE: 99
            },
            PromoStatus: {
                NOCODE: 0,
                PENDING: 1,
                ACCEPTED: 2,
                USEDBEFORE: 3,
                INVALID: 4,
                UNKNOWNERROR: 5,
                EXPIRED: 6
            },
            ReusableCodeSuggestions: ['q5rlm6ot3r', '55khv20st4', 'ykxlvmg9ja', 'vmuph8j0c6', 'd8fmqxjlma', 'rjnmzjs673', 'ki2r0jq5r0', '4obq1i3idd']
        },
        RandomInteractionLevel: {
            NONE: 0,
            LOW: 1,
            MEDIUM: 2,
            HIGH: 3
        },
        Integers: {
            HS_26_IN_MILLISECONDS: 93600000, //Using 26 hs instead of 24hs
            HS_2_IN_MILLISECONDS: 7200000 //and 2hs gap retry when code is flagged as USEDBEFORE
        },
        WalletType: {
            FP_MAIL: 100,
            FP_BTC: 101,
            FP_BNB: 102,
            FP_BCH: 103,
            FP_DASH: 104,
            FP_DGB: 105,
            FP_DOGE: 106,
            FP_ETH: 107,
            FP_FEY: 108,
            FP_LTC: 109,
            FP_TRX: 110,
            FP_USDT: 111,
            FP_ZEC: 112,
            FP_SOL: 113,
            EC: 200,
            BTC: 1,
            LTC: 2
        },
        ErrorType: {
            ERROR: 0,
            TIMEOUT: 1,
            NEED_TO_LOGIN: 2,
            ROLL_ERROR: 3,
            CLICK_ROLL_ERROR: 4,
            LOGIN_ERROR: 5,
            CLAIM_ERROR: 6,
            ADDRESS_ERROR: 7,
            MIN_WITHDRAW_ERROR: 8,
            IP_BAN: 9,
            IP_RESTRICTED: 10,
            IP_ERROR: 11,
            FORCE_CLOSED: 12,
            NO_FUNDS: 13,
            VERIFY_EMAIL: 14,
            NO_ADDRESS: 15,
            FAUCET_EMPTY: 16
        },
        CMC: {
            MULT: '-1',
            BTC: '1',
            LTC: '2',
            XRP: '52',
            DOGE: '74',
            DGB: '109',
            DASH: '131',
            USDT: '825',
            XEM: '873',
            ETH: '1027',
            STEEM: '1230',
            NEO: '1376',
            ZEC: '1437',
            BCH: '1831',
            BNB: '1839',
            TRX: '1958',
            LINK: '1975',
            ADA: '2010',
            USDC: '3408',
            SOL: '5426',
            SHIB: '5994',
            FEY: '10361',
            BFG: '11038',
            CAKE: '7186',
            GRC: '833',
            MATIC: '3890',
            BABY: '10334',
            BTT: '16086',
            BSW: '10746',
        },
        LOCATION: {
            UNKNOWN: 0,
            MANAGER: 1,
            SITE: 2
        }
    });

    let persistence, shared, manager, ui, CFPromotions, interactions, CFHistory, SiteProcessor, eventer;
    let uiRenderer;

    Element.prototype.isVisible = function() {
        return !!(this.offsetWidth||this.offsetHeight||this.getClientRects().length);
    };
    Element.prototype.isUserFriendly = function(selector) {
        let e = selector ? this.querySelector(selector) : this;
        return e && e.isVisible()  ? e : null;
    };
    Document.prototype.isUserFriendly = Element.prototype.isUserFriendly;

    Number.prototype.toDate = function() {
        return new Date(this);
    };
    Number.prototype.msToCountdown = function() {
        const remainingSeconds = Math.ceil(this / 1000);
        const hours = Math.floor(remainingSeconds / 3600).toString().padStart(2, '0');
        const minutes = Math.floor((remainingSeconds % 3600) / 60).toString().padStart(2, '0');
        const seconds = (remainingSeconds % 60).toString().padStart(2, '0');
        return `${hours}:${minutes}:${seconds}`;
    };

    String.prototype.clean = function() {
        let output = "";
        for (let i = 0; i < this.length; i++) {
            if (this.charCodeAt(i) <= 127) {
                output += this.charAt(i);
            }
        }
        return output;
    };
    Array.prototype.shuffle = function () {
        let currentIndex = this.length, temporaryValue, randomIndex;

        while (0 !== currentIndex) {
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex -= 1;
            temporaryValue = this[currentIndex];
            this[currentIndex] = this[randomIndex];
            this[randomIndex] = temporaryValue;
        }

        return this;
    };

    let helpers = {
        getTdPrintableTime: function (date = new Date()) {
            if (date != null) {
                return ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
            }
            return '';
        },
        getPrintableTime: function (date = new Date()) {
            if (date == null) {
                return '';
            }
            return ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2);
        },
        getPrintableDateTime: function (date) {
            if (date != null) {
                return ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
            } else {
                return '';
            }
        },
        getEnumText: function (enm, value) {
            return Object.keys(enm).find(key => enm[key] === value) || '_ERR';
        },
        randomMs: function (a, b){
            return a + (b - a) * Math.random();
        },
        addMinutes: function(mins, date = new Date()) {
            return date.setMinutes(date.getMinutes() + +mins);
        },
        addSeconds: function(secs, date = new Date()) {
            return date.setSeconds(date.getSeconds() + +secs);
        },
        randomHexColor: function() {
            const hexChars = '0123456789abcdef';
            let color = '';
            for (let i = 0; i < 6; i++) {
              color += hexChars[Math.floor(Math.random() * hexChars.length)];
            }
            return color;
        },
        randomString: function(length) {
            let str = '';
            const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
            const charactersLength = characters.length;

            for (let i = 0; i < length; i++) {
                str += characters.charAt(Math.floor(Math.random() * charactersLength));
            }

            return str;
        },
        randomInt: function(min, max) {
            min = Math.ceil(min);
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min + 1)) + min;
        },
        addMs: function(ms, date = new Date()) {
            return date.setMilliseconds(date.getMilliseconds() + ms);
        },
        getRandomMs: function(minute, rangeDiffInPercentage) { // Now will be a random value between minute and minute + rangeDiffPercentage%; Example if minute = 30 and rangeDiffPercentage = 5 => random in the range [30, 31.5]
            let msMin = minute * 60 * 1000;
            let msMax = msMin + rangeDiffInPercentage/100 * msMin;
            return helpers.randomMs(msMin, msMax);
        },
        hsToMs: function(hours) {
            return hours * 60 * 60 * 1000;
        },
        minToMs: function(min) {
            return min * 60 * 1000;
        },
        getEmojiForPromoStatus: function(promoStatus) {
            switch (promoStatus) {
                case K.CF.PromoStatus.NOCODE:
                    return '⚪';
                case K.CF.PromoStatus.PENDING:
                    return '⏳';
                case K.CF.PromoStatus.ACCEPTED:
                    return '✔️';
                case K.CF.PromoStatus.USEDBEFORE:
                    return '🕙';
                case K.CF.PromoStatus.INVALID:
                    return '❌';
                case K.CF.PromoStatus.EXPIRED:
                    return '📅';
                case K.CF.PromoStatus.UNKNOWNERROR:
                    return '❗';
            }
        },
        getHost: function(url, withHttps = false) {
            if (url.includes('//')) {
                url = url.split('//')[1];
            }
            url = url.split('/')[0];
            return withHttps ? ('https://' + url) : url;
        },
        cf: {
            getUrlType: function(url) {
                if (url.endsWith('/free-rolls')) {
                    return K.CF.UrlType.FREEROLLS;
                }
                if (url.split('?')[0].endsWith('/free')) {
                    return K.CF.UrlType.FREE;
                }
                if (url.includes('/promotion/')) {
                    return K.CF.UrlType.PROMOTION;
                }
                if (url.endsWith('/contact-twitter')) {
                    return K.CF.UrlType.CONTACTTWITTER;
                }
                if (url.endsWith('/settings')) {
                    return K.CF.UrlType.SETTINGS;
                }
                if (url.endsWith('/stats')) {
                    return K.CF.UrlType.STATS;
                }
                if (url.endsWith('/')) {
                    url = url.slice(0, -1);
                    if (url == helpers.getHost(url, true)) {
                        return K.CF.UrlType.HOME;
                    }
                }

                return K.CF.UrlType.IGNORE;
            }
        },
        triggerMouseEvent: function (elm, eventType) {
            let clickEvent = document.createEvent('MouseEvents');
            clickEvent.initEvent (eventType, true, true);
            elm.dispatchEvent (clickEvent);
        },
        alternativeClick: function (elm) {
            helpers.triggerMouseEvent (elm, "mouseover");
            helpers.triggerMouseEvent (elm, "mousedown");
            helpers.triggerMouseEvent (elm, "mouseup");
            helpers.triggerMouseEvent (elm, "click");
        }
    }

    class Persistence {
        constructor(prefix = 'autoWeb_') {
            this.prefix = prefix;
        }
        save(key, value, parseIt = false) {
            GM_setValue(this.prefix + key, parseIt ? JSON.stringify(value) : value);
        }
        load(key, parseIt = false) {
            let value = GM_getValue(this.prefix + key);
            if(value && parseIt) {
                value = JSON.parse(value);
            }
            return value;

        }
    }

    let objectGenerator = {
        createShared: function() {
            let config = {};
            function initializeConfig() {
                config = {
                    'devlog.enabled': false,
                    'devlog.maxLines': 200,
                    'defaults.extraInterval': true,
                    'defaults.timeout': 4,
                    'defaults.postponeMinutes': 65,
                    'defaults.postponeMinutes.min': 65,
                    'defaults.postponeMinutes.max': 65,
                    'defaults.workInBackground': true,
                    'defaults.nextRun.useCountdown': true,
                    'defaults.nextRun': 60,
                    'defaults.nextRun.min': 60,
                    'defaults.nextRun.max': 60,
                    'defaults.sleepMode': false,
                    'defaults.sleepMode.min': "00:00",
                    'defaults.sleepMode.max': "01:00",
                    'cf.tryGetCodes': false,
                    'cf.rollOnce': false,
                    'cf.autologin': false,
                    'cf.credentials.mode': 1,
                    'cf.credentials.email': '[email protected]',
                    'cf.credentials.password': 'YOURPASSWORD',
                    'cf.sleepHoursIfIpBan': 8,
                    'fp.maxTimeInMinutes': 15,
                    'fp.randomPtcOrder': true,
                    'dutchy.useBoosted': false,
                    'bk.withdrawMode': "0",
                    'bk.hoursBetweenWithdraws': 4,
                    'bk.sleepMinutesIfIpBan': 75,
                    'bestchange.address': '101',
                    'ui.runtime': 0,
                    'fpb.credentials.mode': 2,
                    'fpb.credentials.username': 'YOUR_USERNAME',
                    'fpb.credentials.password': 'YOURPASSWORD',
                    'bigbtc.postponeMinutes': '0',
                    'fbch.credentials.mode': 2,
                    'fbch.credentials.username': 'YOUR_USERNAME',
                    'fbch.credentials.password': 'YOURPASSWORD',
                    'jtfey.credentials.mode': 2,
                    'jtfey.credentials.username': 'YOUR_USERNAME',
                    'jtfey.credentials.password': 'YOURPASSWORD',
                    'shost.credentials.mode': 2,
                    'shost.credentials.username': 'YOUR_USERNAME',
                    'shost.credentials.password': 'YOURPASSWORD',
                    'ycoin.credentials.mode': 2,
                    'ycoin.credentials.username': 'YOUR_ACCOUNT_NUMBER',
                    'ycoin.credentials.password': 'YOURPASSWORD',
                    'bkclass.coin': 'LTC',
                    'bkclass.bcoin': 'LTC',
                    'bscads.credentials.mode': 2,
                    'bscads.credentials.username': 'YOUR_USERNAME',
                    'bscads.credentials.password': 'YOURPASSWORD',
                    'migrations': [
                        {version: '00200799', applied: false} // migration to change pcodes status from error to usable due to ui changes
                    ]
                };

                let storedData = persistence.load('config', true);
                if(storedData) {
                    for (const prop in config) {
                        if(storedData.hasOwnProperty(prop)) {
                            config[prop] = storedData[prop];
                        }
                    }
                }

                config.version = GM_info.script.version;
            };
            function getConfig() {
                return config;
            };
            function updateConfig(items) {
                items.forEach( function (item) {
                    config[item.prop] = item.value;
                });
                persistence.save('config', config, true);
            };
            function migrationApplied(migrationVersion) {
                try {
                    let mig = config.migrations.find(x => x.version == migrationVersion);
                    mig.applied = true;
                    persistence.save('config', config, true);
                } catch (err) {
                    console.warn('Error saving migration as applied');
                    console.error(err);
                }
            };
            function devlog(msg, elapsed = false, reset = false) {
                if(!config['devlog.enabled']) {
                    return;
                }

                let log;
                if(reset) {
                    log = [`${helpers.getPrintableTime()}|Log cleared`];
                } else {
                    log = persistence.load('devlog', true);
                    log = log ?? [];
                }

                if(msg) {
                    msg = scheduleUuid ? `[${scheduleUuid}] ${msg}` : msg;
                    let previous;
                    try {
                        previous = log[log.length - 1].split('|')[1];
                    } catch {}
                    if(elapsed && (previous == msg)) {
                        log[log.length - 1] = `${helpers.getPrintableTime()}|${msg}|[Elapsed time: ${elapsed} seconds]`;
                    } else {
                        log.push(`${helpers.getPrintableTime()}|${msg}`);
                    }
                }

                if(log.length > 200) {
                    log.splice(0, log.length - 200);
                }

                persistence.save('devlog', log, true);
            };
            function getDevLog() {
                let log;
                log = persistence.load('devlog', true);
                if(log) {
                    return log;
                }
            };

            let runningSites = {}
            let scheduleUuid = null;
            function isOpenedByManager() {
                loadFlowControl();
                if(!runningSites || runningSites == {}) {
                    return false;
                }
                let uuid = null;
                for (const sch in runningSites) {
                    if ( (runningSites[sch].host && runningSites[sch].host == window.location.host) ||
                         (runningSites[sch].params && runningSites[sch].params.trackUrl && window.location.href.includes(runningSites[sch].params.trackUrl))
                       ) {
                        uuid = sch;
                        break;
                    }
                }
                if (!uuid) {
                    return false;
                }

                if (runningSites[uuid].runStatus == 'COMPLETED') {
                    return false;
                } else {
                    scheduleUuid = uuid;
                    return true;
                }
            };
            function loadFlowControl() {
                runningSites = persistence.load('runningSites', true) || {};
            };
            function setFlowControl(schedule, id, url, webType, params = null) {
                runningSites[schedule] = {
                    id: id,
                    changedAt: Date.now(),
                    url: url,
                    host: url.host,
                    type: webType,
                    opened: false,
                    error: false,
                    result: {}
                };

                if(params) {
                    runningSites[schedule].params = params;
                } else {
                    runningSites[schedule].params = {};
                }
                saveFlowControl(schedule);
            };
            function isCompleted(expectedId) {
                loadFlowControl();
                for(const sch in runningSites) {
                    if (runningSites[sch].id == expectedId) {
                        if (runningSites[sch].runStatus == 'COMPLETED') {
                            return true;
                        } else {
                            return false;
                        }
                    }
                }
                return false;
            };
            function hasErrors(expectedId) {
                for(const sch in runningSites) {
                    if (runningSites[sch].id == expectedId && runningSites[sch].error) {
                        return true;
                    }
                }
                return false;
            };
            function getResult(schedule) {
                if (schedule) {
                    return runningSites.hasOwnProperty(schedule) ? runningSites[schedule].result : {};
                }
                return runningSites.hasOwnProperty(scheduleUuid) ? runningSites[scheduleUuid].result : {};
            };
            function getCurrent(schedule) {
                if (schedule) {
                    return runningSites.hasOwnProperty(schedule) ? runningSites[schedule] : {};
                }
                return runningSites.hasOwnProperty(scheduleUuid) ? runningSites[scheduleUuid] : {};
            };
            function saveAndclose(runDetails, delay = 0) {
                markAsVisited(runDetails);
                if(delay) {
                    setTimeout(window.close, delay);
                } else {
                    setTimeout(window.close, 1000);
                }
            };
            function purgeFlowControlSchedules(validSchedules) {
                loadFlowControl();
                let deletables = [];
                for (var sch in runningSites) {
                    if (!validSchedules.includes(sch)) {
                        deletables.push(sch);
                    }
                }
                deletables.forEach(x => {
                    delete runningSites[sch];
                });
                persistence.save('runningSites', runningSites, true);
            };
            function saveFlowControl(schedule) {
                schedule = schedule ? schedule : scheduleUuid;
                if (!schedule) {
                    persistence.save('runningSites', runningSites, true);
                    return;
                }
                let tempFlow = persistence.load('runningSites', true);
                tempFlow[schedule] = runningSites[schedule];
                persistence.save('runningSites', tempFlow, true);
            };
            function markAsVisited(runDetails, runStatus = 'COMPLETED') {
                if (!scheduleUuid) {
                    return;
                }
                runningSites[scheduleUuid].opened = true;
                runningSites[scheduleUuid].runStatus = runStatus;
                runningSites[scheduleUuid].result = runDetails ? runDetails : runningSites[scheduleUuid].result;

                saveFlowControl(scheduleUuid);
            };
            function addError(errorType, errorMessage, schedule) {
                if (schedule) {
                    runningSites[schedule].error = true;
                    runningSites[schedule].result.errorType = errorType;
                    runningSites[schedule].result.errorMessage = errorMessage;
                } else {
                    runningSites[scheduleUuid].error = true;
                    runningSites[scheduleUuid].result.errorType = errorType;
                    runningSites[scheduleUuid].result.errorMessage = errorMessage;
                }

                saveFlowControl(schedule ? schedule : scheduleUuid);
            };
            function closeWithError(errorType, errorMessage) {
                addError(errorType, errorMessage);
                window.close();
            };
            function clearFlowControl(schedule) {
                if (schedule) {
                    runningSites[schedule] = {};
                    saveFlowControl(schedule);
                }
            };
            function clearRetries() {
                loadFlowControl();
                runningSites[scheduleUuid].retrying = false;
                saveFlowControl(scheduleUuid);
                return false;
            };
            function isRetrying() {
                if(runningSites[scheduleUuid].retrying) {
                    return true;
                }
                runningSites[scheduleUuid].retrying = true;
                saveFlowControl(scheduleUuid);
                return false;
            };
            function setProp(key, val) {
                runningSites[scheduleUuid][key] = val;
                saveFlowControl(scheduleUuid);
            };
            function getProp(key) {
                return runningSites[scheduleUuid][key];
            };
            function getParam(key) {
                try {
                } catch {}
                return runningSites[scheduleUuid].params[key];
            };
            initializeConfig();
            return {
                devlog: devlog,
                getDevLog: getDevLog,
                setFlowControl: setFlowControl,
                isCompleted: isCompleted,
                isOpenedByManager: isOpenedByManager,
                saveFlowControl: saveFlowControl,
                getCurrent: getCurrent,
                getResult: getResult,
                addError: addError,
                closeWindow: saveAndclose,
                closeWithError: closeWithError,
                updateWithoutClosing: markAsVisited,
                hasErrors: hasErrors,
                clearFlowControl: clearFlowControl,
                getConfig: getConfig,
                updateConfig: updateConfig,
                clearRetries: clearRetries,
                isRetrying: isRetrying,
                setProp: setProp,
                getProp: getProp,
                getParam: getParam,
                migrationApplied: migrationApplied,
                purgeFlowControlSchedules: purgeFlowControlSchedules
            };
        },
        createManager: function() {
            let timestamp = null;
            let intervalUiUpdate;
            let getFeedInterval;

            let userWallet = [];

            const sites = [
                { id: '1', name: 'CF ADA', cmc: '2010', coinRef: 'ADA', url: new URL('https://freecardano.com/free'), rf: '?ref=335463', type: K.WebType.CRYPTOSFAUCETS, clId: 45 },
                { id: '2', name: 'CF BNB', cmc: '1839', coinRef: 'BNB', url: new URL('https://freebinancecoin.com/free'), rf: '?ref=161127', type: K.WebType.CRYPTOSFAUCETS, clId: 42 },
                { id: '3', name: 'CF BTC', cmc: '1', coinRef: 'BTC', url: new URL('https://freebitcoin.io/free'), rf: '?ref=490252', type: K.WebType.CRYPTOSFAUCETS, clId: 40 },
                { id: '4', name: 'CF DASH', cmc: '131', coinRef: 'DASH', url: new URL('https://freedash.io/free'), rf: '?ref=124083', type: K.WebType.CRYPTOSFAUCETS, clId: 156 },
                { id: '5', name: 'CF ETH', cmc: '1027', coinRef: 'ETH', url: new URL('https://freeethereum.com/free'), rf: '?ref=204076', type: K.WebType.CRYPTOSFAUCETS, clId: 44 },
                { id: '6', name: 'CF LINK', cmc: '1975', coinRef: 'LINK', url: new URL('https://freecryptom.com/free'), rf: '?ref=78652', type: K.WebType.CRYPTOSFAUCETS, clId: 157 },
                { id: '7', name: 'CF LTC', cmc: '2', coinRef: 'LTC', url: new URL('https://free-ltc.com/free'), rf: '?ref=117042', type: K.WebType.CRYPTOSFAUCETS, clId: 47 },
                { id: '8', name: 'CF NEO', cmc: '1376', coinRef: 'NEO', url: new URL('https://freeneo.io/free'), rf: '?ref=100529', type: K.WebType.CRYPTOSFAUCETS, clId: 158 },
                { id: '9', name: 'CF STEAM', cmc: '825', coinRef: 'STEEM', url: new URL('https://freesteam.io/free'), rf: '?ref=117686', type: K.WebType.CRYPTOSFAUCETS, clId: 49 },
                { id: '10', name: 'CF TRX', cmc: '1958', coinRef: 'TRX', url: new URL('https://free-tron.com/free'), rf: '?ref=145047', type: K.WebType.CRYPTOSFAUCETS, clId: 41 },
                { id: '11', name: 'CF USDC', cmc: '3408', coinRef: 'USDC', url: new URL('https://freeusdcoin.com/free'), rf: '?ref=100434', type: K.WebType.CRYPTOSFAUCETS, clId: 51 },
                { id: '12', name: 'CF USDT', cmc: '825', coinRef: 'USDT', url: new URL('https://freetether.com/free'), rf: '?ref=181230', type: K.WebType.CRYPTOSFAUCETS, clId: 43 },
                { id: '13', name: 'CF XEM', cmc: '873', coinRef: 'XEM', url: new URL('https://freenem.com/free'), rf: '?ref=295274', type: K.WebType.CRYPTOSFAUCETS, clId: 46 },
                { id: '14', name: 'CF XRP', cmc: '52', coinRef: 'XRP', url: new URL('https://coinfaucet.io/free'), rf: '?ref=808298', type: K.WebType.CRYPTOSFAUCETS, clId: 48 },
                { id: '15', name: 'StormGain', cmc: '1', url: new URL('https://app.stormgain.com/crypto-miner/'), rf: 'friend/BNS27140552', type: K.WebType.STORMGAIN, clId: 35 },
                { id: '16', name: 'CF DOGE', cmc: '74', coinRef: 'DOGE', url: new URL('https://free-doge.com/free'), rf: '?ref=97166', type: K.WebType.CRYPTOSFAUCETS, clId: 50 },
                { id: '17', name: 'FreeBitco.in', cmc: '1', url: new URL('https://freebitco.in/'), rf: '?r=41092365', type: K.WebType.FREEBITCOIN, clId: 36 },
                { id: '18', name: 'FaucetPay PTC', cmc: '1', url: new URL('https://faucetpay.io/ptc'), rf: '?r=41092365', type: K.WebType.FAUCETPAY, clId: 159 },
                { id: '52', name: 'BigBtc', cmc: '1', wallet: K.WalletType.FP_BTC, url: new URL('https://bigbtc.win/'), rf: '?id=39255652', type: K.WebType.BIGBTC, clId: 200 },
                { id: '53', name: 'BestChange', cmc: '1', wallet: K.WalletType.FP_BTC, url: new URL('https://www.bestchange.com/'), rf: ['index.php?nt=bonus&p=1QCD6cWJNVH4Cdnz85SQ2qtTkAwGr9fvUk'], type: K.WebType.BESTCHANGE, clId: 163 },
                { id: '58', name: 'BF BTC', cmc: '1', url: new URL('https://betfury.io/boxes/all'), rf: ['?r=608c5cfcd91e762043540fd9'], type: K.WebType.BFBOX, clId: 1 },
                { id: '59', name: 'BF BNB', cmc: '1839', url: new URL('https://betfury.io/boxes/all'), rf: ['?r=608c5cfcd91e762043540fd9'], type: K.WebType.BFBOX, clId: 1 },
                { id: '61', name: 'Dutchy', cmc: '-1', url: new URL('https://autofaucet.dutchycorp.space/roll.php'), rf: '?r=corecrafting', type: K.WebType.DUTCHYROLL, clId: 141 },
                { id: '62', name: 'Dutchy Monthly Coin', cmc: '-1', url: new URL('https://autofaucet.dutchycorp.space/coin_roll.php'), rf: '?r=corecrafting', type: K.WebType.DUTCHYROLL, clId: 141 },
                { id: '65', name: 'FCrypto Roll', cmc: '-1', url: new URL('https://faucetcrypto.com/dashboard'), rf: 'ref/704060', type: K.WebType.FCRYPTO, clId: 27 },
                { id: '67', name: 'BF BFG', cmc: '11038', url: new URL('https://betfury.io/boxes/all'), rf: ['?r=608c5cfcd91e762043540fd9'], type: K.WebType.BFBOX, clId: 1 },
                { id: '68', name: 'CF SHIBA', cmc: '5994', coinRef: 'SHIBA', url: new URL('https://freeshibainu.com/free'), rf: '?ref=18226', type: K.WebType.CRYPTOSFAUCETS, clId: 167 },
                { id: '77', name: 'FPig', cmc: '825', wallet: K.WalletType.FP_USDT, url: new URL('https://faupig-bit.online/page/dashboard'), rf: [''], type: K.WebType.FPB, clId: 154 },
                { id: '78', name: 'CF Cake', cmc: '7186', coinRef: 'CAKE', url: new URL('https://freepancake.com/free'), rf: '?ref=699', type: K.WebType.CRYPTOSFAUCETS, clId: 197 },
                { id: '80', name: 'FreeGRC', cmc: '833', url: new URL('https://freegridco.in/#free_roll'), rf: '', type: K.WebType.FREEGRC, clId: 207 },
                { id: '81', name: 'CF Matic', cmc: '3890', coinRef: 'MATIC', url: new URL('https://freematic.com/free'), rf: '?ref=6435', type: K.WebType.CRYPTOSFAUCETS, clId: 210 },
                { id: '84', name: 'JTFey', cmc: '-1', url: new URL('https://james-trussy.com/faucet'), rf: ['?r=corecrafting'], type: K.WebType.VIE, clId: 213 },
                { id: '85', name: 'O24', cmc: '1', wallet: K.WalletType.FP_BTC, url: new URL('https://www.only1024.com/f'), rf: ['?r=1QCD6cWJNVH4Cdnz85SQ2qtTkAwGr9fvUk'], type: K.WebType.O24, clId: 97 },
                { id: '86', name: 'BF BABY', cmc: '10334', url: new URL('https://betfury.io/boxes/all'), rf: ['?r=608c5cfcd91e762043540fd9'], type: K.WebType.BFBOX, clId: 1 },
                { id: '87', name: 'CF BTT', cmc: '16086', coinRef: 'BTT', url: new URL('https://freebittorrent.com/free'), rf: '?ref=2050', type: K.WebType.CRYPTOSFAUCETS, clId: 218 },
                { id: '88', name: 'BF BSW', cmc: '10746', url: new URL('https://betfury.io/boxes/all'), rf: ['?r=608c5cfcd91e762043540fd9'], type: K.WebType.BETFURYBOX, clId: 1 },
                { id: '89', name: 'CF BFG', cmc: '11038', coinRef: 'BFG', url: new URL('https://freebfg.com/free'), rf: '?ref=117', type: K.WebType.CRYPTOSFAUCETS, clId: 219 },
                { id: '93', name: 'YCoin', cmc: '1', wallet: K.WalletType.FP_BTC, url: new URL('https://yescoiner.com/faucet'), rf: ['?ref=4729452'], type: K.WebType.YCOIN, clId: 234 },
                { id: '94', name: 'CDiversity', cmc: '-1', wallet: K.WalletType.FP_MAIL, url: new URL('http://coindiversity.io/free-coins'), rf: ['?r=1J3sLBZAvY5Vk9x4RY2qSFyL7UHUszJ4DJ'], type: K.WebType.CDIVERSITY, clId: 235 },
                { id: '95', name: 'BscAds', cmc: '1839', url: new URL('https://bscads.com/'), rf: ['ref/corecrafting'], type: K.WebType.BSCADS, clId: 226 },
                { id: '96', name: 'Top Ltc', cmc: '2', wallet: K.WalletType.FP_LTC, url: new URL('https://ltcfaucet.top/'), rf: ['?r=MWSsGAQTYD7GH5o4oAehC8Et5PyMBfhnKK'], type: K.WebType.CTOP, clId: 239 },
                { id: '97', name: 'Top Bnb', cmc: '1839', wallet: K.WalletType.FP_BNB, url: new URL('https://bnbfaucet.top/'), rf: ['?r=0x1e8CB8A79E347C54aaF21C0502892B58F97CC07A'], type: K.WebType.CTOP, clId: 240 },
                { id: '98', name: 'Top Doge', cmc: '74', wallet: K.WalletType.FP_DOGE, url: new URL('https://dogecoinfaucet.top/'), rf: ['?r=D8Xgghu5gCryukwmxidFpSmw8aAKon2mEQ'], type: K.WebType.CTOP, clId: 241 },
                { id: '99', name: 'Top Trx', cmc: '1958', wallet: K.WalletType.FP_TRX, url: new URL('https://tronfaucet.top/'), rf: ['?r=TK3ofbD3AyXotN2111UvnwCzr2YaW8Qmx7'], type: K.WebType.CTOP, clId: 242 },
                { id: '100', name: 'Top Eth', cmc: '1027', wallet: K.WalletType.FP_ETH, url: new URL('https://ethfaucet.top/'), rf: ['?r=0xC21FD989118b8C0Db6Ac2eC944B53C09F7293CC8'], type: K.WebType.CTOP, clId: 243 },
            ];

            const wallet = [
                { id: '100', name: 'FaucetPay Email', type: K.WalletType.FP_MAIL },
                { id: '101', name: 'FaucetPay BTC (Bitcoin)', type: K.WalletType.FP_BTC },
                { id: '102', name: 'FaucetPay BNB (Binance Coin)', type: K.WalletType.FP_BNB },
                { id: '103', name: 'FaucetPay BCH (Bitcoin Cash)', type: K.WalletType.FP_BCH },
                { id: '104', name: 'FaucetPay DASH (Dash)', type: K.WalletType.FP_DASH },
                { id: '105', name: 'FaucetPay DGB (DigiByte)', type: K.WalletType.FP_DGB },
                { id: '106', name: 'FaucetPay DOGE (Dogecoin)', type: K.WalletType.FP_DOGE },
                { id: '107', name: 'FaucetPay ETH (Ethereum)', type: K.WalletType.FP_ETH },
                { id: '108', name: 'FaucetPay FEY (Feyorra)', type: K.WalletType.FP_FEY },
                { id: '109', name: 'FaucetPay LTC (Litecoin)', type: K.WalletType.FP_LTC },
                { id: '110', name: 'FaucetPay TRX (Tron)', type: K.WalletType.FP_TRX },
                { id: '111', name: 'FaucetPay USDT (Tether TRC20)', type: K.WalletType.FP_USDT },
                { id: '112', name: 'FaucetPay ZEC (Zcash)', type: K.WalletType.FP_ZEC },
                { id: '113', name: 'FaucetPay SOL (Solana)', type: K.WalletType.FP_SOL },
                { id: '200', name: 'ExpressCrypto (EC-UserId-XXXXXX)', type: K.WalletType.EC },
                { id: '1', name: 'BTC Alternative Address', type: K.WalletType.BTC }
            ];

            class Site {
    constructor(params) {
        Object.assign(this, {
            schedule: '4a70e0', // Owner!
            id: null,
            name: null,
            cmc: null, // REVIEW LOCATION
            coinRef: null, // REVIEW LOCATION. Only for CFs?
            url: null, // REVIEW FORMAT. Only one/'start' url? What about complex scripts/rotators/SLs?
            rf: null, // ...
            type: null, // REVIEW DEFAULT. It should be something like 'Crawler' or 'Handler' and the site params should depend on this value
            clId: null,
            wallet: null, // should be part of site parameters/crawler based?
            enabled: false,
            lastClaim: 0,
            aggregate: 0,
            balance: 0,
            stats: {},
            nextRoll: null,
            params: {}, // should have schedule overrides and be called customExecution, scheduleParamaters or something like that
            firstRun: true,
            isExternal: false,
        }, params);

        this.setLegacyConditionalDefaults();

    }

    setLegacyConditionalDefaults() {
        if (this.type == K.WebType.CRYPTOSFAUCETS) {
            this.schedule = '65329c';
        }

        if (this.type == K.WebType.BFBOX) {
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = false;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 21;
            this.params['defaults.nextRun.max'] = 25;
        }

        if (this.type == K.WebType.FREEBITCOIN) {
            this.params['custom.useWofRp'] = 0;
            this.params['custom.useFunRp'] = 0;
        }

        if (this.type == K.WebType.STORMGAIN) {
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = true;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 15;
            this.params['defaults.nextRun.max'] = 20;
        }
        if (this.type == K.WebType.FAUCETPAY) {
            this.params['defaults.workInBackground.override'] = true;
            this.params['defaults.workInBackground'] = false;
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = false;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 300;
            this.params['defaults.nextRun.max'] = 360;
        }
        if (this.type == K.WebType.BIGBTC) {
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = false;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 15;
            this.params['defaults.nextRun.max'] = 40;
        }
        if (this.type == K.WebType.DUTCHYROLL) {
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = true;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 30;
            this.params['defaults.nextRun.max'] = 35;
        }
        if (this.type == K.WebType.FCRYPTO) {
            this.params['defaults.workInBackground.override'] = true;
            this.params['defaults.workInBackground'] = false;
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = false;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 26;
            this.params['defaults.nextRun.max'] = 35;
            this.params['defaults.timeout.override'] = true;
            this.params['defaults.timeout'] = 3;
            this.params['defaults.postponeMinutes.override'] = true;
            this.params['defaults.postponeMinutes'] = 0;
            this.params['defaults.postponeMinutes.min'] = 12;
            this.params['defaults.postponeMinutes.max'] = 18;
        }
        if (this.type == K.WebType.FPB) {
            this.params['defaults.nextRun.override'] = true;
            this.params['defaults.nextRun.useCountdown'] = false;
            this.params['defaults.nextRun'] = 0;
            this.params['defaults.nextRun.min'] = 22;
            this.params['defaults.nextRun.max'] = 45;
        }
    }

    static _sites = [];
    static getAll() {
        return Site._sites;
    }

    static getById(siteId) {
        return Site.getAll().find(x => x.id == siteId) || false;
    }

    static createFromDataArray(newSites) {
        if (!Array.isArray(newSites)) {
            newSites = [...newSites];
        }
        newSites.forEach(s => Site.getAll().push(new Site(s)));
    }

    static add(data) { 
        let newSite = new Site(data);
        Site.getAll().push(newSite);

        let schedule = manager.Schedule.getById(newSite.schedule);

        if (!schedule) {
            try {
                schedule = manager.Schedule.getAll()[0];
            } catch (err) {
                console.warn('No schedules found! Reseting to default schedules');
                let defaultSchedule = new Schedule({ uuid: '4a70e0', name: 'Default' });
                let sampleSchedule = new Schedule({ uuid: '65329c', name: 'CF' });
                if (Schedule.getAll().length == 0) {
                    Schedule.add(defaultSchedule);
                    Schedule.add(sampleSchedule);
                }
                schedule = manager.Schedule.getAll()[0];
            }
        }

        if (!schedule) {
            console.warn('Schedule NOT found');
            console.warn(data);
            return;
        }
        schedule.addSite(newSite);

        eventer.emit('siteAdded', {
            siteId: newSite.id,
            siteName: newSite.name,
            scheduleId: newSite.schedule
        });
    }

    static remove(siteId) {
        let idx = this._sites.findIndex(x => x.id === siteId);
        if (idx > -1 && this._sites[idx].isExternal) {
            let siteName = this._sites[idx].name;
            this._sites = Site.getAll().filter(x => x.id !== siteId);
            manager.Schedule.getAll().forEach(sch => {
                sch.removeSite(siteId);
            });
            eventer.emit('siteRemoved', {
                siteId: siteId,
                siteName: siteName
            });
        }

    }

    static sortAll() {
        Site.getAll().sort( function(a,b) {
            if (a === b) {
                return 0;
            } else if (a.nextRoll === null && b.nextRoll === null) {
                let aHasLoginError = a.stats?.errors?.errorType == 2;
                let bHasLoginError = b.stats?.errors?.errorType == 2;
                if (aHasLoginError) {
                    return -1;
                } else if (bHasLoginError) {
                    return 1;
                }
                return a.id > b.id ? -1 : 1
            } else if (a.nextRoll === null) {
                return 1;
            } else if (b.nextRoll === null) {
                return -1;
            } else {
                return a.nextRoll.getTime() < b.nextRoll.getTime() ? -1 : 1;
            }
        });
    }

    static setAsRunAsap(siteId) {
        let site = Site.getById(siteId);
        if (!site) return false;

        try {
            let schedule = Schedule.getById(site.schedule);
            if (schedule.status == STATUS.CLAIMING) {
                console.warn(`Setting ASAP as 1st in schedule time + 1`);
                site.nextRoll = new Date(schedule.currentSite.nextRoll.getTime() + 1);
            } else {
                let now = new Date();
                if (!schedule.currentSite?.nextRoll) {
                    console.warn(`Setting ASAP as now()`);
                    site.nextRoll = now;
                } else if (now < schedule.currentSite.nextRoll) {
                    console.warn(`Setting ASAP as now()`);
                    site.nextRoll = now;
                } else {
                    console.warn(`Setting ASAP as 1st in schedule time - 1`);
                    site.nextRoll = new Date(schedule.currentSite.nextRoll.getTime() - 1);
                }
            }
            site.enabled = true;

            console.warn(`[${site.schedule}] ${site.name} updated to run ASAP from Site`);
            eventer.emit('siteUpdated', site);
            return;
        } catch (err) {
            console.error(err);
            ui.log({msg: `Error setting faucet to run ASAP from Site: ${err}`});
        }
    }

    changeSchedule(newScheduleId) {
        let oldScheduleId = null;
        if (this.schedule) {
            oldScheduleId = this.schedule;
            manager.Schedule.getById(this.schedule)?.removeSite(this.id);
        }
        this.schedule = newScheduleId;
        let newSchedule = manager.Schedule.getById(this.schedule);
        newSchedule.addSite(this); // maybe use just the ids...
        eventer.emit('siteChangedSchedule', {
            siteId: this.id,
            scheduleId: this.schedule,
            oldScheduleId: oldScheduleId
        });
    }

    static saveAll() {
        persistence.save('webList', Site._sites.map(x => x.toStorage()), true);
    }

    toStorage() { // Single site
        if (!this.isExternal) {
            return {
                id: this.id,
                isExternal: this.isExternal || false,
                name: this.name,
                schedule:this.schedule,
                lastClaim: this.lastClaim,
                aggregate: this.aggregate,
                balance: this.balance,
                stats: this.stats,
                nextRoll: this.nextRoll,
                enabled: this.enabled,
                params: this.params
            };
        } else {
            return {
                id: this.id,
                url: this.url.href,
                clId: this.clId,
                type: this.type,
                cmc: this.cmc,
                rf: this.rf,
                name: this.name,
                isExternal: this.isExternal || false,
                schedule:this.schedule,
                lastClaim: this.lastClaim,
                aggregate: this.aggregate,
                balance: this.balance,
                stats: this.stats,
                nextRoll: this.nextRoll,
                enabled: this.enabled,
                params: this.params
            };
        }
    }

    update(items) { // this should be for Parameters or Execution (custom)
        this.params = this.params || {};
        items.forEach( item => {
            this.params[item.prop] = item.value;
        });
        ui.log({schedule: this.schedule, siteName: this.name, msg: `Site ${this.name} updated`});
    }

    getSiteParameters() {
        if (this.type == K.WebType.CRYPTOSFAUCETS) {
            this.siteParameters = {
                handler: 'CF',
                fields: [
                    { name: 'try_get_codes', type: 'checkbox', value: 'false', text: 'Auto update promo codes' },
                    { name: 'max_rolls_per_visit', type: 'numberInput', value: 1, min: 0 },
                    { name: 'autologin', type: 'checkbox', value: 'true', text: 'Autologin when necessary' },
                    { name: 'credentials_mode', type: 'credentials_or_autofilled', value: '2' },
                    { name: 'email', type: 'email', value: '' },
                    { name: 'password', type: 'password', value: '' }
                ]
            };
        }
        return this.siteParameters || false;
    }
}

            class Schedule {
    constructor(params) {
        Object.assign(this, {
            uuid: '4a70e0',
            name: 'default_schedule',
            status: STATUS.INITIALIZING,
            currentSite: null,
            sites: [],
            tab: null,
            timer: null, // TBD
            timeWaiting: 0,
            timeUntilNext: null,
            worker: null
        }, params)
        this.timer = new Timer({ isManager: true, delaySeconds: 30, uuid: this.uuid, webType: null });
        this.timer = new Timer(true, 30, this.uuid);
    }

    static schedules = [];

    static getAll() {
        return Schedule.schedules;
    }

    static getById(scheduleId) {
        return Schedule.getAll().find(x => x.uuid == scheduleId) || false;
    }

    static add(newSchedule) {
        Schedule.getAll().push(newSchedule);
    }

    static getAllForCrud() {
        return Schedule.getAll().map(x => {
            return {
                uuid: x.uuid,
                name: x.name,
                hasSites: x.sites && x.sites.length > 0
            };
        });
    }

    static async initialize() {

        Schedule.loadAll();

        let defaultSchedule = new Schedule({ uuid: '4a70e0', name: 'Default' });
        let sampleSchedule = new Schedule({ uuid: '65329c', name: 'CF' });
        if (Schedule.getAll().length == 0) {
            Schedule.add(defaultSchedule);
            Schedule.add(sampleSchedule);
            return;
        }

        let idxDefault = Schedule.getAll().findIndex(x => x.uuid == '4a70e0');
        if (idxDefault == -1) {
            Schedule.add(defaultSchedule);
        }
    };

    static saveAll() {
        persistence.save('schedules', Schedule.schedules.map(x => {
            return {
                uuid: x.uuid,
                name: x.name
            };
        }), true);
    }

    static loadAll() {
        Schedule.schedules = [];
        let schedulesJson = persistence.load('schedules', true) || [];
        schedulesJson.forEach(function(element) {
            Schedule.getAll().push(new Schedule({
                uuid: element.uuid,
                name: element.name,
            }));
        });
    }

    sortSites() {
        this.sites.sort( function(a,b) {
            if (a === b) {
                return 0;
            } else if (a.nextRoll === null && b.nextRoll === null) {
                let aHasLoginError = a.stats?.errors?.errorType == 2;
                let bHasLoginError = b.stats?.errors?.errorType == 2;
                if (aHasLoginError) {
                    return -1;
                } else if (bHasLoginError) {
                    return 1;
                }
                return a.id > b.id ? -1 : 1
            } else if (a.nextRoll === null) {
                return 1;
            } else if (b.nextRoll === null) {
                return -1;
            } else {
                return a.nextRoll.getTime() < b.nextRoll.getTime() ? -1 : 1;
            }
        });
    }

    static crud(data) {
        let isInvalid = false;
        try {
            const orphanSites = [];
            data.forEach(x => {
                if (x.added) {
                    if (Schedule.getById(x.uuid)) {
                        isInvalid = true;
                    } else {
                        let newSchedule = new Schedule({
                            uuid: x.uuid,
                            name: x.name,
                            order: x.order
                        })
                        Schedule.getAll().push(newSchedule);
                        newSchedule.start();
                    }
                } else if (x.removed) {
                    let pos = Schedule.getAll().findIndex(s => s.uuid == x.originals.uuid);
                    orphanSites.push(...Schedule.getAll()[pos].sites);
                    Schedule.getAll().splice(pos, 1);
                } else {
                    let sch = Schedule.getAll().find(s => s.uuid == x.originals.uuid);
                    if (Schedule.getById(x.uuid) && (Schedule.getById(x.uuid) != sch)) {
                        isInvalid = true;
                    } else {
                        sch.uuid = x.uuid;
                    }
                    sch.name = x.name;
                    sch.order = x.order;
                }
            });

            Schedule.getAll().sort((a, b) => a.order - b.order);

            if (orphanSites.length > 0) {
                orphanSites.forEach(x => {
                    x.schedule = Schedule.getAll()[0].uuid;
                });

                Schedule.getAll()[0].sites.push(...orphanSites);
            }
            Schedule.saveAll();
        } catch (err) {
            console.error(err);
            return false;
        }
        if (isInvalid) {
            return false;
        }
        return true;
    }

    addSite(site)     { this.sites.push(site) }
    removeSite(siteId)  {
        if (this.sites.findIndex(x => x.id === siteId) > -1) {
            this.sites = this.sites.filter(x => x.id !== siteId);
            this.setCurrentSite();
        }
    }

    setCurrentSite() {
        this.currentSite = this.sites[0];
    }

    start() {
        this.status = STATUS.IDLE;
        this.worker = setTimeout(() => {
            this.checkNextRoll();
        }, 2000);
    }

    checkNextRoll() {
        if (this.status != STATUS.IDLE) {
            return;
        }
        this.timer.stopCheck();
        clearTimeout(this.worker);
        if(!this.currentSite || this.currentSite.nextRoll == null) {
            document.querySelector(`#wait-times span[data-schedule="${this.uuid}"]`).setAttribute('data-nextroll', 'UNDEFINED');
            this.status = STATUS.IDLE;
            return;
        }

        if(this.currentSite.nextRoll.getTime() < Date.now()) {
            ui.log({ schedule: this.uuid, siteName: this.currentSite.name, msg: `Opening ${this.currentSite.name}`});
            document.querySelector(`#wait-times span[data-schedule="${this.uuid}"]`).setAttribute('data-nextroll', 'RUNNING');
            this.open();
            this.timeUntilNext = null;
            return;
        } else {
            this.timeUntilNext = this.currentSite.nextRoll.getTime() - Date.now() + helpers.randomMs(1000, 2000);

            document.querySelector(`#wait-times span[data-schedule="${this.uuid}"]`).setAttribute('data-nextroll', this.currentSite.nextRoll.getTime());
            this.worker = setTimeout(() => {
                this.checkNextRoll();
            }, this.timeUntilNext);
            this.status = STATUS.IDLE;
        }
    }

    getCustomOrDefaultVal(param, useOverride = false) {
        let val;

        if (useOverride) {
            if (this.currentSite.params && this.currentSite.params.hasOwnProperty(param)) {
                val = this.currentSite.params[param];
                if (val != -1) {
                    return val;
                }
            }
        }

        return shared.getConfig()[param];
    }

    useOverride(param) {
        let overrideFlag = param  + '.override';
        return this.currentSite.params && this.currentSite.params[overrideFlag];
    }

    closeTab() {
        this.tab.close();
    };

    reopenTab() {
        this.tab = GM_openInTab(this.currentSite.url, { active: !this.getCustomOrDefaultVal('defaults.workInBackground', this.useOverride('defaults.workInBackground')) });
    };

    open(promoCodes) {
        this.status = STATUS.CLAIMING;
        let navUrl = this.currentSite.url;
        try {
            let params = this.currentSite.params || {};
            if(promoCodes) {
                navUrl = new URL('promotion/' + promoCodes[0], this.currentSite.url.origin);
                ui.log({ schedule: this.uuid, siteName: this.currentSite.name, msg: `Opening ${this.currentSite.name} with ${promoCodes.length} Promo Codes [${promoCodes.join(',')}]`});
                params.promoCodes = promoCodes;
            }

            if (this.currentSite.firstRun) {
                if(Array.isArray(this.currentSite.rf) && this.currentSite.rf.length > 0) {
                    navUrl = new URL(navUrl.href + this.currentSite.rf[helpers.randomInt(0, this.currentSite.rf.length - 1)]);
                }
            }

            if (this.currentSite.wallet) {
                try {
                    params.address = userWallet.find(x => x.type == this.currentSite.wallet).address;
                } catch {
                    shared.addError(K.ErrorType.NO_ADDRESS, 'You need to add your address to the wallet before claiming this faucet.', this.uuid);
                    ui.log({ schedule: this.uuid, siteName: this.currentSite.name, msg: `Unable to launch ${this.currentSite.name}: Address not detected > add it to the wallet.`});
                    this.moveNextAfterTimeoutOrError();
                    return;
                }
            }
            if(this.currentSite.type == K.WebType.BESTCHANGE) {
                params.address = shared.getConfig()['bestchange.address'] == '1' ? userWallet.find(x => x.type == 1).address : params.address;
            }
            params.timeout = this.getCustomOrDefaultVal('defaults.timeout', this.useOverride('defaults.timeout'));
            params.cmc = this.currentSite.cmc;

            if(this.currentSite.type == K.WebType.FPB) {
                switch(this.currentSite.id) {
                    case '77':
                        params.sitePrefix = 'fpb';
                        break;
                    case '83':
                        params.sitePrefix = 'fbch';
                        break;
                    case '92':
                        params.sitePrefix = 'shost';
                        break;
                }
            }

            if(this.currentSite.type == K.WebType.VIE) {
                params.credentials = {
                    mode: shared.getConfig()['jtfey.credentials.mode'],
                    username: shared.getConfig()['jtfey.credentials.username'],
                    password: shared.getConfig()['jtfey.credentials.password']
                };
            }

            shared.setFlowControl(this.uuid, this.currentSite.id, navUrl, this.currentSite.type, params);
            setTimeout(() => {
                this.waitForResult();
            }, 15000);

            if (this.tab && !this.tab.closed) {
                try {
                    this.tab.close();
                } catch {
                }
            } else {
            }

            this.timer.startCheck(this.currentSite.type);
            let noSignUpList = [ K.WebType.BESTCHANGE, K.WebType.CBG, K.WebType.G8, K.WebType.O24, K.WebType.CDIVERSITY, K.WebType.CTOP ];
            let hrefOpener = navUrl.href;
            if (noSignUpList.includes(this.currentSite.type)) {
                hrefOpener = (new URL(this.currentSite.clId, 'https://criptologico.com/goto/')).href;
            }
            this.tab = GM_openInTab(hrefOpener, { active: !this.getCustomOrDefaultVal('defaults.workInBackground', this.useOverride('defaults.workInBackground')) });
        } catch(err) {
            ui.log({ schedule: this.uuid, msg: `Error opening tab: ${err}`});
        }
    };

    waitForResult() {
        if(isObsolete()) {
            return;
        }

        if(shared.isCompleted(this.currentSite.id)) {
            this.analyzeResult(); // rename to something else...
        } else {
            this.waitOrMoveNext(); // this should just be the error and timeout check
        }
        return;

    };

    analyzeResult() {
        let result = shared.getResult(this.uuid);

        if (result) {
            this.updateWebListItem(result);

            if (result.closeParentWindow) {
                ui.log({ schedule: this.uuid, msg: `Closing working tab per process request` });
                this.closeTab();
            }

            if (this.currentSite.type == K.WebType.CRYPTOSFAUCETS) {
                let promoCode = CFPromotions.hasPromoAvailable(this.currentSite.id);
                if (promoCode) {
                    this.timeWaiting = 0;

                    this.currentSite.nextRoll = new Date(754000 + +this.currentSite.id);
                    update(false);
                    this.open(promoCode);
                    return;
                }
            }
        } else {
            ui.log({ schedule: this.uuid, siteName: this.currentSite.name, msg: `Unable to read last run result, for ID: ${this.currentSite.id} > ${this.currentSite.name}`});
        }

        this.timeWaiting = 0;
        this.status = STATUS.IDLE;
        shared.clearFlowControl(this.uuid);
        update(true);
        readUpdateValues(true);
        return;
    }

    waitOrMoveNext() {
        this.timeWaiting += 15;
        if (!shared.hasErrors(this.currentSite.id) && !this.hasTimedOut()) {
            ui.log({ schedule: this.uuid, 
                siteName: this.currentSite.name,
                elapsed: this.timeWaiting, 
                msg: `Waiting for ${this.currentSite.name} results...`});
            setTimeout(() => {
                this.waitForResult();
            }, 15000);
            return;
        }

        if (shared.hasErrors(this.currentSite.id)) {
            this.currentSite.stats.errors = shared.getResult(this.uuid);
            ui.log({ schedule: this.uuid, siteName: this.currentSite.name, 
                msg: `${this.currentSite.name} closed with error: ${helpers.getEnumText(K.ErrorType,this.currentSite.stats.errors.errorType)} ${this.currentSite.stats.errors.errorMessage}`});

            if(this.sleepIfBan()) {
                return;
            }
        }

        if (this.hasTimedOut()) {
            if (this.currentSite.isExternal) {
                this.currentSite.stats.countTimeouts = 0;
                this.currentSite.stats.errors = null;
                ui.log({ schedule: this.uuid, siteName: this.currentSite.name, 
                    msg: `Closing ${this.currentSite.name}` });
                    try {
                        this.closeTab();
                    } catch (err) { console.error('Unable to close working tab', err); }
                this.moveAfterNormalRun();
                return;
            } else {
                if(this.currentSite.stats.countTimeouts) {
                    this.currentSite.stats.countTimeouts += 1;
                } else {
                    this.currentSite.stats.countTimeouts = 1;
                }

                ui.log({ schedule: this.uuid, siteName: this.currentSite.name, 
                    msg: `Waited too much time for ${this.currentSite.name} results: triggering timeout` });
            }
        }

        this.moveNextAfterTimeoutOrError();
        return;
    }

    hasTimedOut() { // here or on a site level???
        let val = this.getCustomOrDefaultVal('defaults.timeout', this.useOverride('defaults.timeout')) * 60;
        return (this.timeWaiting > val);
    };

    sleepIfBan() { // This should be a SiteType hook
        if( (this.currentSite.stats.errors.errorType == K.ErrorType.IP_BAN && shared.getConfig()['cf.sleepHoursIfIpBan'] > 0)
        || ( (this.currentSite.stats.errors.errorType == K.ErrorType.IP_RESTRICTED || this.currentSite.stats.errors.errorType == K.ErrorType.IP_BAN) && shared.getConfig()['bk.sleepMinutesIfIpBan'] > 0) ) {
            if(this.currentSite.type == K.WebType.CRYPTOSFAUCETS) {
                Site.getAll().filter(x => x.enabled && x.type == K.WebType.CRYPTOSFAUCETS)
                    .forEach( function(el) {
                    el.nextRoll = this.sleepCheck(helpers.addMs(helpers.getRandomMs(shared.getConfig()['cf.sleepHoursIfIpBan'] * 60, 2)).toDate());
                });
            }

            shared.clearFlowControl(this.uuid);
            update(true);
            this.timeWaiting = 0;
            this.status = STATUS.IDLE;
            shared.clearFlowControl(this.uuid);
            readUpdateValues(true);
            return true;
        }
        return false;
    }

    updateWebListItem(result) {
        ui.log({ schedule: this.uuid, 
            msg: `Updating data: ${JSON.stringify(result)}` });
        this.currentSite.stats.countTimeouts = 0;
        this.currentSite.stats.errors = null;

        if (result.claimed) {
            try {
                result.claimed = parseFloat(result.claimed);
            } catch { }
            if(!isNaN(result.claimed)) {
                this.currentSite.lastClaim = result.claimed;
                this.currentSite.aggregate += result.claimed;
            }
        }
        if(result.balance) {
            this.currentSite.balance = result.balance;
        }
        this.currentSite.nextRoll = this.getNextRun(result.nextRoll ? result.nextRoll.toDate() : null);
        if(result.promoCodeResults) { // TODO: move to a processResult hook
            for(let i = 0; i < result.promoCodeResults.length; i++) {
                let item = result.promoCodeResults[i];
                CFPromotions.updateFaucetForCode(item.promoCode, this.currentSite.id, item.promoStatus);
            }
        }
        if(result.rolledNumber) {
            CFHistory.addRoll(result.rolledNumber);
        }
    }

    getNextRun(nextRollFromCountdown) {
        let useCustom = this.useOverride('defaults.nextRun');
        let useCountdown = this.getCustomOrDefaultVal('defaults.nextRun.useCountdown', useCustom);
        let nextRunMode = this.getCustomOrDefaultVal('defaults.nextRun', useCustom);
        let min = this.getCustomOrDefaultVal('defaults.nextRun.min', useCustom);
        let max = this.getCustomOrDefaultVal('defaults.nextRun.max', useCustom);
        let nextRun;

        if (useCountdown && nextRollFromCountdown) {
            nextRun = nextRollFromCountdown;
        } else {
            let minutes = (nextRunMode == 0) ? helpers.randomInt(min, max) : nextRunMode;
            let msDelay = helpers.getRandomMs(minutes, 1);

            nextRun = helpers.addMs(msDelay).toDate();
        }
        nextRun = this.sleepCheck(nextRun)

        return nextRun;
    }

    errorTreatment() { // Move to group custom getNextRoll
        try {
            switch(this.currentSite.stats.errors.errorType) {
                case K.ErrorType.NEED_TO_LOGIN:
                    this.currentSite.enabled = false;
                    this.currentSite.nextRoll = null;
                    return true;
                case K.ErrorType.FAUCET_EMPTY: // retry in 8 hours
                    this.currentSite.enabled = true;
                    this.currentSite.nextRoll = new Date(new Date().setHours(new Date().getHours() + 8));
                    return true;
            }
        } catch {}
        return false;
    }

    sleepCheck(nextRun) {
        let useCustom = this.useOverride('defaults.sleepMode');
        let sleepMode = this.getCustomOrDefaultVal('defaults.sleepMode', useCustom);

        if (sleepMode) {
            let intNextRunTime = nextRun.getHours() * 100 + nextRun.getMinutes();
            let min = this.getCustomOrDefaultVal('defaults.sleepMode.min', useCustom).replace(':', '');
            let max = this.getCustomOrDefaultVal('defaults.sleepMode.max', useCustom).replace(':', '');

            if (+min < +max) {
                if (+min < intNextRunTime && intNextRunTime < +max) {
                    nextRun.setHours(max.slice(0, 2), max.slice(-2), 10, 10);
                    ui.log({ schedule: this.uuid, 
                        msg: `Next run adjusted by Sleep Mode: ${helpers.getPrintableDateTime(nextRun)}` });
                }
            } else if (+min > +max) {
                if (intNextRunTime > +min || intNextRunTime < +max) {
                    nextRun.setHours(max.slice(0, 2), max.slice(-2), 10, 10);
                    if (nextRun.getTime() < Date.now()) {
                        nextRun.setDate(nextRun.getDate() + 1);
                    }
                    ui.log({ schedule: this.uuid, 
                        msg: `Next run adjusted by Sleep Mode: ${helpers.getPrintableDateTime(nextRun)}` });
                }
            }
        }
        return nextRun;
    }

    moveAfterNormalRun() {
        this.currentSite.nextRoll = this.getNextRun(null);

        shared.clearFlowControl(this.uuid);
        update(true);
        this.timeWaiting = 0;
        this.status = STATUS.IDLE;
        shared.clearFlowControl(this.uuid);
        readUpdateValues(true);
    }

    moveNextAfterTimeoutOrError() {
        let useCustom = this.useOverride('defaults.postponeMinutes');

        let mode = this.getCustomOrDefaultVal('defaults.postponeMinutes', useCustom);
        let min = this.getCustomOrDefaultVal('defaults.postponeMinutes.min', useCustom);
        let max = this.getCustomOrDefaultVal('defaults.postponeMinutes.max', useCustom);

        let minutes = (mode == 0) ? helpers.randomInt(min, max) : mode;
        let msDelay = helpers.getRandomMs(minutes, 5);

        this.currentSite.nextRoll = this.sleepCheck(helpers.addMs(msDelay).toDate());
        if(this.errorTreatment()) {
        }

        shared.clearFlowControl(this.uuid);
        update(true);
        this.timeWaiting = 0;
        this.status = STATUS.IDLE;
        shared.clearFlowControl(this.uuid);
        readUpdateValues(true);
    }

}

            async function start() {
                await loader.initialize();
                ui.init(getCFlist(), Schedule.getAll());
                uiRenderer.appendEventListeners();
                shared.purgeFlowControlSchedules(Schedule.getAll().map(x => x.uuid));
                update();
                uiRenderer.wallet.legacyRenderWalletTable(userWallet);
                intervalUiUpdate = setInterval(readUpdateValues, 10000);
                Schedule.getAll().forEach(x => {
                    x.start();
                });
                if (document.querySelector('#console-log').innerText == 'Loading...') {
                    document.querySelector('#console-log').innerHTML = '<table><tr><td><b>Running...</b></td></tr></table>';
                }
                getFeedInterval = setInterval(getCodesFeed, 25000);
            };

            let loader = function() {
                async function initialize() {
                    setTimestamp();
                    await Schedule.initialize();
                    await initializeSites();
                    initializeUserWallet();
                    initializePromotions();
                    initializeHistory();
                };
                async function initializeSites() {
                    Site.createFromDataArray(sites);
                    await updateSitesWithStoredData();
                    await addSitesToSchedules();
                };
                async function updateSitesWithStoredData() {
                    let storedData = persistence.load('webList', true);
                    if (storedData) {
                        storedData.forEach( function (stored) {
                            if (stored.isExternal) {
                                stored.url = new URL(stored.url);
                                Site.add(stored);
                            }
                            let site = Site.getById(stored.id);
                            if (!site) {
                                return;
                            }
                            for (const prop in stored) {
                                site[prop] = stored[prop];
                            }
                            if (!site.enabled) {
                                site.nextRoll = null;
                            } else {
                                site.nextRoll = site.nextRoll ? new Date(site.nextRoll) : new Date();
                            }
                            if (site.aggregate || site.balance) {
                                site.firstRun = false;
                            }
                        })
                    }
                };
                async function addSitesToSchedules() {
                    Site.getAll().forEach(site => {
                        let scheduleOfSite = Schedule.getById(site.schedule);
                        if (!scheduleOfSite) {
                            console.warn(`Attention! Site ${site.name} has a reference to a schedule that does not exist: (${site.schedule})`);
                            scheduleOfSite = Schedule.getAll()[0];
                            console.warn(`Assigning it to first schedule (${scheduleOfSite.uuid}) instead.`);
                            site.schedule = scheduleOfSite.uuid; // use .changeSchedule to save the change???
                        }
                        scheduleOfSite.addSite(site);
                    });
                };
                function initializeUserWallet() {
                    addWallets();
                    addStoredWalletData();
                };
                function addWallets() {
                    wallet.forEach(x => userWallet.push(x));
                    userWallet.forEach(function (element, idx, arr) {
                        arr[idx].address = '';
                    });
                };
                function addStoredWalletData() {
                    let storedData = persistence.load('userWallet', true);
                    if(storedData) {
                        storedData.forEach( function (element) {
                            let idx = userWallet.findIndex(x => x.id == element.id);
                            if(idx != -1) {
                                userWallet[idx].address = element.address ?? userWallet[idx].address;
                            }
                        });
                    }
                };
                function initializePromotions() {
                    let storedData = persistence.load('CFPromotions', true);
                    if (storedData) {
                        let mig00200799 = false;
                        try {
                            mig00200799 = shared.getConfig().migrations.find(x => x.version == '00200799' && !x.applied);
                        } catch (err) {}
                        console.info(`Migration 00200799: ${mig00200799 ? 'APPLYING' : 'previously applied or not needed'}`);

                        let allCFs = manager.getFaucetsForPromotion().map( cf => cf.id );
                        storedData.forEach( function (element, idx, arr) {
                            arr[idx].added = new Date(element.added);
                            arr[idx].statusPerFaucet.forEach( function (el, i, a) {
                                a[i].execTimeStamp = (el.execTimeStamp != null) ? new Date(el.execTimeStamp) : null;
                                if (mig00200799 && el.status == 4) {
                                    a[i].status = 1;
                                }
                            });
                            allCFs.forEach( function (cf) {
                                if (!arr[idx].statusPerFaucet.find( x => x.id == cf )) {
                                    let newCf = { id: cf, status: 1, execTimeStamp: null };
                                    arr[idx].statusPerFaucet.push(newCf);
                                }
                            });
                        });
                        if (mig00200799) {
                            shared.migrationApplied('00200799');
                        }
                        CFPromotions.load(storedData);
                    }
                };
                function initializeHistory() {
                    CFHistory.initOrLoad();
                };
                function setTimestamp() {
                    timestamp = Date.now();
                    persistence.save('timestamp', timestamp);
                };
                return {
                    initialize: initialize
                };
            }();
            function getCodesFeed(force = false) {
                clearInterval(getFeedInterval);
                if (!force) {
                    let tryGet = shared.getConfig()['cf.tryGetCodes'] || false;
                    if (!tryGet) {
                        return;
                    }
                }

                let nextFeed = helpers.randomMs(2 * 60 * 60 * 1000, 4 * 60 * 60 * 1000);
                getFeedInterval = setInterval(getCodesFeed, nextFeed)

                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://criptologico.com/api/?key=XI2HV-1P9PQ-W637F-68B9B-A248&requests[cf_codes]",
                    timeout: 10000,
                    onload: function(response) {
                        try {
                            let txt = response.responseText;
                            let parsed = JSON.parse(txt);
                            if (parsed.success) {
                                let newCodes = [];
                                for(let i = 0; i < parsed.cf_codes.length; i++) {
                                    let item = parsed.cf_codes[i];
                                    let newCode = {};
                                    newCode.code = item.code;
                                    newCode.oneTimeOnly = item.is_one_time == '1';
                                    newCode.expirationDate = item.expiration_date.replace(' ', 'T') + 'Z';
                                    newCode.expirationDate = new Date(newCode.expirationDate);
                                    newCodes.push(newCode);
                                }
                                CFPromotions.includeNewCodes(newCodes);
                                uiRenderer.promos.legacyRenderPromotionTable(CFPromotions.getAll());
                            }
                        } catch(err) {
                            console.error('unexpected error parsing codes list');
                            console.error(err);
                        }
                    },
                    onerror: function(e) {
                        console.error('error getting codes');
                        console.error(e);
                    },
                    ontimeout: function() {
                        console.error('timeout getting codes');
                    },
                });
            }
            function readUpdateValues(forceCheck = false) {
                readPromoCodeValues();
                readModalData();

                if(true) {
                    let updateDataElement = document.getElementById('update-data');
                    let updateValues = updateDataElement.innerText.clean();

                    if (updateValues != '') {
                        updateDataElement.innerText = '';
                        let updateObj = JSON.parse(updateValues);
                        if(updateObj.editSingle.changed) {
                            updateObj.editSingle.items.forEach(function (element, idx, arr) {
                                try {
                                    let site = Site.getById(element.id);

                                    site.name = element.displayName;

                                    if (site.enabled != element.enabled) {
                                        site.enabled = element.enabled;
                                        if(site.enabled) {
                                            site.nextRoll = new Date(idx);
                                        } else {
                                            site.nextRoll = null;
                                        }
                                    }
                                    ui.log({ schedule: site.schedule,
                                        msg: `Faucet updated. New name: ${element.displayName}. Active: ${element.enabled}` });
                                } catch (err) {
                                    ui.log({ schedule: this.uuid,
                                        msg: `Error updating faucet data: ${err}` });
                                }
                            });
                        }

                        if(updateObj.wallet.changed) {
                            updateObj.wallet.items.forEach(function (element) {
                                try {
                                    let itemIndex = userWallet.findIndex(x => x.id == element.id);
                                    userWallet[itemIndex].address = element.address;

                                    ui.log({ msg: `Wallet Address updated [${userWallet[itemIndex].name}]: ${userWallet[itemIndex].address}` });
                                } catch (err) {
                                    ui.log({ msg: `Error updating wallet/address: ${err}` });
                                }
                            });

                            uiRenderer.wallet.legacyRenderWalletTable(userWallet);
                            saveUserWallet();
                        }

                        if(updateObj.config.changed) {
                            try {
                                shared.updateConfig(updateObj.config.items);
                                ui.log({ msg: `Config updated. Reloading in a few seconds...` });
                                window.location.reload();
                                return;
                            } catch (err) {
                                ui.log({ msg: `Error updating config: ${err}` });
                            }

                        }

                        if(updateObj.site.changed) {
                            updateObj.site.list.forEach( (x) => {
                                try {
                                    updateSite(x.id, x.items);
                                } catch (err) {
                                    ui.log({ msg: `Error updating site: ${err}` });
                                }
                            });
                        }

                        if(updateObj.runAsap.changed || updateObj.editSingle.changed || updateObj.site.changed) {
                            resyncAll({ withUpdate: true });
                            return;
                        }
                    }
                }
                if(forceCheck) {
                    resyncAll({ withUpdate: false });
                }
            };
            function resyncAll(options = { withUpdate: false} ) {
                if (options.withUpdate) {
                    update(true);
                }
                Schedule.getAll().forEach(x => {
                    x.checkNextRoll();
                });
            }
            function updateSite(id, items) {
                let site = Site.getById(id);
                if (site) {
                    site.params = site.params || {};
                    items.forEach( (item) => {
                        site.params[item.prop] = item.value;
                    });

                    ui.log({ schedule: site.schedule, siteName: site.name, 
                        msg: `Site ${site.name} updated` });
                }
            }
            function readModalData() { // This should be migrated and dissapear!
                if(document.getElementById('modal-spinner').isVisible()) {
                    let targetObject = JSON.parse(document.getElementById('target-spinner').innerHTML);
                    let target = targetObject.id;
                    if (target == 'modal-ereport') {
                        let temp = shared.getDevLog();
                        document.getElementById('log-textarea').value = temp.join('\n');
                    } else if (target == 'modal-config') {
                        uiRenderer.config.legacyRenderConfigData(shared.getConfig());
                    } else if (target == 'modal-site') {
                        let site = Site.getById(targetObject.siteId);
                        uiRenderer.sites.legacyRenderSiteData(site, shared.getConfig());
                    }
                    document.getElementById('modal-spinner').classList.toggle('d-none');
                    document.getElementById(target).classList.toggle('d-none');
                    document.getElementById('target-spinner').innerHTML = '';
                }
            }
            function sortSites () { // Temporary, just to decouple it...
                Site.sortAll();
                Schedule.getAll().forEach( schedule => schedule.sortSites() );
            };
            function update(sortIt = true) {
                if(sortIt) {
                    sortSites();
                    Schedule.getAll().forEach( schedule => schedule.setCurrentSite() );
                }

                Site.saveAll();
                Site.getAll().forEach(site => {
                    uiRenderer.sites.renderSiteRow(site);
                });
                uiRenderer.sites.removeDeletedSitesRows(Site.getAll().map(x => x.id));
                convertToFiat();
                uiRenderer.sites.sortSitesTable(); // y reordenar
                uiRenderer.promos.legacyRenderPromotionTable(CFPromotions.getAll());
                updateRollStatsSpan();
            };

            function saveUserWallet() {
                const data = userWallet.map(x => {
                    return {
                        id: x.id,
                        address: x.address
                    };});

                persistence.save('userWallet', data, true);
            }
            function isObsolete() {
                let savedTimestamp = persistence.load('timestamp');
                if (savedTimestamp && savedTimestamp > timestamp) {
                    ui.log({ msg: '<b>STOPING EXECUTION!<b> A new Manager UI window was opened. Process should continue there' });
                    clearInterval(intervalUiUpdate);
                    return true;
                }
                return false;
            };

            function readPromoCodeValues() {
                let promoCodeElement = document.getElementById('promo-code-new');
                let promoDataStr = promoCodeElement.innerText.clean();

                if (promoDataStr == '') {
                    return;
                }

                let promoData = JSON.parse(promoDataStr);

                if(promoData.action) {
                    switch (promoData.action) {
                        case 'FORCESTOPFAUCET':
                            Schedule.getAll().forEach(s => {
                                if (s.status != STATUS.IDLE) {
                                    s.currentSite.enabled = false
                                }
                            });
                            update(true);
                            window.location.reload();

                            promoCodeElement.innerText = '';
                            break;
                        case 'ADD':
                            CFPromotions.addNew(promoData.code, promoData.repeatDaily);
                            promoCodeElement.innerText = '';
                            document.getElementById('promo-text-input').value = '';
                            uiRenderer.toast("Code " + promoData.code + " added!");
                            ui.log({ msg: `Promo code ${promoData.code} added` });
                            uiRenderer.promos.legacyRenderPromotionTable(CFPromotions.getAll());
                            break;
                        case 'REMOVEALLPROMOS':
                            CFPromotions.removeAll();
                            promoCodeElement.innerText = '';
                            uiRenderer.toast("Promo codes removed!");
                            ui.log({ msg: `Promo codes removed` });
                            uiRenderer.promos.legacyRenderPromotionTable(CFPromotions.getAll());
                            break;
                        case 'REMOVE':
                            if(CFPromotions.remove(promoData.id, promoData.code) != -1) {
                                ui.log({ msg: `Promo code ${promoData.code} removed` });
                            } else {
                                ui.log({ msg: `Unable to remove code ${promoData.code}` });
                            }
                            promoCodeElement.innerText = '';
                            uiRenderer.promos.legacyRenderPromotionTable(CFPromotions.getAll());
                            break;
                        case 'TRYGETCODES':
                            getCodesFeed(true);
                            promoCodeElement.innerText = '';
                            uiRenderer.toast("Looking for new codes!");
                            break;
                    }
                }
            };

            function updateRollStatsSpan() {
                let rollsSpanElement = document.getElementById('rolls-span');
                rollsSpanElement.innerText = CFHistory.getRollsMeta().join(',');
            };

            function getCFlist() {
                let items;
                items = Site.getAll().filter(f => f.type === K.WebType.CRYPTOSFAUCETS);
                items = items.map(x => {
                    return {
                        id: x.id,
                        name: x.coinRef
                    };});
                items.sort((a, b) => (a.name > b.name) ? 1 : -1);

                return items;
            };

            function closeWorkingTab(schedule) {
                let sc = Schedule.getAll().find(x => x.uuid == schedule);
                if (sc) sc.closeTab()
            };
            function reloadWorkingTab(schedule) {
                let sc = Schedule.getAll().find(x => x.uuid == schedule);
                if (sc) {
                    sc.closeTab();
                    sc.reopenTab();
                }
            };
            function getAllSites() {
                return Site.getAll();
            }
            return{
                init:start,
                getFaucetsForPromotion: getCFlist,
                readPromoCodeValues: readPromoCodeValues,
                closeWorkingTab: closeWorkingTab,
                reloadWorkingTab: reloadWorkingTab,
                Schedule: Schedule,
                Site: Site,
                getAllSites: getAllSites,
                resyncAll: resyncAll
            };
        },
        createUi: function() {

            let injectables = {
                managerJs: function () {

                    window.myBarChart = null;
                    window.landing = window.location.host;

                    window.sendErrorReport = function sendErrorReport() {
                        try {
                            let header = new Headers();
                            header.append("Content-Type", "application/json");
                            let description = document.getElementById("log-message").value;
                            let log = document.getElementById("log-textarea").value.split('\n');
                            let content = {"description":description, "log":log};
                            let opt = { method: "POST", header, mode: "cors", body: JSON.stringify(content) };
                            fetch("https://1d0103ec5a621b87ea27ffed3c072796.m.pipedream.net", opt).then(response => {
                            }).catch(err => {
                                console.error("[error] " + err.message);
                            });
                        } catch { }
                    };

                    window.getUpdateObject = function getUpdateObject() {
                        let updateObject;
                        var updateData = document.getElementById("update-data");
                        if (updateData.innerHTML != "") {
                            updateObject = JSON.parse(updateData.innerHTML);
                        } else {
                            updateObject = { runAsap: { ids: [], changed: false}, editSingle: { changed: false, items: [] }, wallet: { changed: false, items: []}, config: { changed: false, items: []}, site: { changed: false, list: []} };
                        }
                        return updateObject;
                    };

                    window.removePromoCode = function removePromoCode(id, code) {
                        var promoCode = document.getElementById("promo-code-new");
                        var promoObject = { action: "REMOVE", id: id, code: code };
                        promoCode.innerHTML =JSON.stringify(promoObject);
                    };

                    window.editWallet = {
                        save: function() {
                            let updateObject = getUpdateObject();
                            document.querySelectorAll("#wallet-table-body tr").forEach( function(row) {
                                let textInput = row.querySelector(".em-input input");
                                if(textInput.dataset.original != textInput.value) {
                                    let single = { id: row.dataset.id, address: textInput.value.trim() };
                                    updateObject.wallet.items.push(single);
                                    updateObject.wallet.changed = true;
                                }
                            });
                            if(updateObject.wallet.changed) {
                                document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);
                                uiRenderer.toast("Wallet will be updated as soon as possible");
                            }
                        },
                        toggleJson: function(val) {
                            if (document.querySelector('#wallet-json').isVisible()) {
                                if(val != 'cancel') {
                                    editWallet.fromJson();
                                }
                            } else {
                                editWallet.toJson();
                            }
                            document.querySelector('.footer-json').classList.toggle('d-none');
                            document.querySelector('.footer-table').classList.toggle('d-none');
                            document.querySelector('#wallet-table').classList.toggle('d-none');
                            document.querySelector('#wallet-json').classList.toggle('d-none');
                        },
                        toJson: function() {
                            let j = [];
                            document.querySelectorAll('#wallet-table-body tr').forEach(function (row) {
                                j.push({ id: row.dataset.id, address: row.querySelector('.em-input input').value });
                            });
                            document.querySelector('#wallet-json').value = JSON.stringify(j);
                        },
                        fromJson: function() {
                            let j = JSON.parse(document.querySelector('#wallet-json').value);
                            document.querySelectorAll('#wallet-table-body tr').forEach(function (row) {
                                let element = j.find(x => x.id == row.dataset.id);
                                if (element) {
                                    row.querySelector('.em-input input').value = element.address;
                                }
                            });
                        },
                        cancel: function() {
                            document.querySelectorAll("#wallet-table-body .em-input input").forEach( function(x) {
                                x.value = x.dataset.original;
                            });
                        }
                    };

                    window.editConfig = {
                        save: function() {
                            let updateObject = getUpdateObject();
                            document.querySelectorAll("#modal-config [data-original][data-prop]").forEach(function(elm) {
                                let single = { prop: elm.dataset.prop, value: elm.dataset.value };
                                if(elm.dataset.original != elm.value && (elm.type == "select-one" || elm.type == "text" || elm.type == "password" || elm.type == "number" || elm.type == "time") ) {
                                    single.value = elm.value;
                                    updateObject.config.items.push(single);
                                    updateObject.config.changed = true;
                                } else if (elm.type == "checkbox" && ((elm.dataset.original == "0" && elm.checked) || (elm.dataset.original == "1" && !elm.checked)) ) {
                                    single.value = elm.checked;
                                    updateObject.config.items.push(single);
                                    updateObject.config.changed = true;
                                }
                            });
                            if(updateObject.config.changed) {
                                document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);
                                uiRenderer.toast("Config will be updated as soon as possible");
                            }
                        },
                        cancel: function() {
                            document.querySelectorAll("#modal-config [data-original][data-prop]").forEach(function(elm) {
                                if(elm.type == "select-one" || elm.type == "text" || elm.type == "password" || elm.type == "number" || elm.type == "time") {
                                    elm.value = elm.dataset.original;
                                } else if (elm.type == "checkbox") {
                                    elm.checked = (elm.dataset.original == "1" ? true : false)
                                }
                            });
                        }
                    };

                    window.editSite = {
                        save: function() {
                            let updateObject = getUpdateObject();
                            let faucet = { id: document.querySelector("#faucet-name").dataset.id, items: [] };
                            document.querySelectorAll("#modal-site [data-original][data-site-prop]").forEach(function(elm) {
                                let single = { prop: elm.dataset.siteProp, value: elm.dataset.original };
                                if(elm.dataset.original != elm.value && (elm.type == "select-one" || elm.type == "text" || elm.type == "password" || elm.type == "number" || elm.type == "time") ) {
                                    single.value = elm.value;
                                    faucet.items.push(single);
                                    updateObject.site.changed = true;
                                } else if (elm.type == "checkbox" && ((elm.dataset.original == "0" && elm.checked) || (elm.dataset.original == "1" && !elm.checked)) ) {
                                    single.value = elm.checked;
                                    faucet.items.push(single);
                                    updateObject.site.changed = true;
                                }
                            });
                            if(updateObject.site.changed) {
                                updateObject.site.list.push(faucet);
                                document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);
                                toastr["info"]("Site will be updated as soon as possible");
                            }

                        },
                        cancel: function() {
                            document.querySelectorAll("#modal-site [data-original][data-site-prop]").forEach(function(elm) {
                                if(elm.type == "select-one" || elm.type == "text" || elm.type == "password" || elm.type == "number" || elm.type == "time") {
                                    elm.value = elm.dataset.original;
                                } else if (elm.type == "checkbox") {
                                    elm.checked = (elm.dataset.original == "1" ? true : false)
                                }
                            });
                        }
                    };

                    window.editEreport = {
                        save: function() {
                            sendErrorReport();
                        },
                        cancel: function() {
                        }
                    };

                    window.modalSave = function modalSave(content) {
                        switch(content) {
                            case "wallet":
                                editWallet.save();
                                break;
                            case "ereport":
                                editEreport.save();
                                break;
                            case "config":
                                editConfig.save();
                                break;
                            case "site":
                                editSite.save();
                                break;
                            case "slAlert":
                                shortlinkAlert.save();
                                break;
                        }
                    };

                    window.modalCancel = function modalCancel(content) {
                        if(content == "wallet") {
                            editWallet.cancel();
                        } else if ("ereport") {
                            editEreport.cancel();
                        }
                        document.querySelectorAll("modal-content").forEach(x => x.classList.add("d-none"));
                    };

                    window.updateValues = function updateValues(type, values) {
                        let updateObject = getUpdateObject();
                        if (type == "runAsap") {
                            updateObject.runAsap.ids.push(values.id);
                            updateObject.runAsap.changed = true;
                            document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);
                            uiRenderer.toast("Faucet will be updated to run as soon as possible");
                        }
                    };

                    window.schedulesInterval = null;
                    window.startSchedulesInterval = function startSchedulesInterval(uuids) {
                        if (window.schedulesInterval) {
                            clearInterval(window.schedulesInterval);
                        }

                        let innerWaitTimes = '';
                        uuids.forEach(x => {
                            innerWaitTimes += `<span data-schedule="${x}" data-nextroll="UNDEFINED" class="mx-1"><i class="fas fa-square pr-1" style="color: #${x};"></i><span></span></span>`;
                        });

                        let container = document.querySelector('#wait-times');
                        container.innerHTML = innerWaitTimes;
                        window.schedulesInterval = setInterval(() => {
                            [...document.querySelectorAll('#wait-times > span')].forEach(sp => {
                                let nroll = sp.getAttribute('data-nextroll');
                                let spanScheduleId = sp.getAttribute('data-schedule');
                                if (nroll == 'UNDEFINED') {
                                    sp.querySelector('span').innerText = '-';
                                } else if (nroll == 'RUNNING') {
                                    sp.querySelector('span').innerText = 'Running';
                                    let inUseElm = document.querySelector(`#schedule-table tr[data-schedule="${spanScheduleId}"]`);
                                    if (inUseElm) {
                                        inUseElm.classList.add('in-use');
                                    }
                                } else {
                                    let timeVal = +nroll - Date.now();
                                    sp.querySelector('span').innerText = timeVal.msToCountdown();
                                    if (timeVal < -15000) {
                                        console.warn(`HITTING RELOAD: ${timeVal}`);
                                        console.info(sp);
                                    }
                                }
                            })
                        }, 1000);
                    }
                    window.confirmable = {
                        open: function (req, details = null, params = null) {
                            let btn = document.getElementById("confirm-req-btn");
                            btn.setAttribute('data-request', req);
                            btn.setAttribute('data-params', params ? JSON.stringify(params) : '{}');

                            if(details) {
                                document.querySelector("#confirmable-modal p").innerText = details;
                            }
                            return;
                        },
                        accept: function () {
                            let btn = document.getElementById("confirm-req-btn");
                            let req = { type: '', params: {}};
                            req.type = btn.getAttribute('data-request');
                            req.params = JSON.parse(btn.getAttribute('data-params'));
                            switch(req.type) {
                                case 'removeAllPromos':
                                    window.removeAllPromos();
                                    break;
                                case 'forceStopFaucet':
                                    window.forceStopFaucet();
                                    break;
                                default:
                                    break;
                            }
                        }
                    }

                    window.removeAllPromos = function removeAllPromos() {
                        var promoCode = document.getElementById("promo-code-new");
                        var promoObject = { action: "REMOVEALLPROMOS" };
                        promoCode.innerHTML =JSON.stringify(promoObject);
                        uiRenderer.toast("This could take around a minute", "Removing all promo codes");
                    };

                    window.forceStopFaucet = function removeAllPromos() {
                        var promoCode = document.getElementById("promo-code-new");
                        var promoObject = { action: "FORCESTOPFAUCET" };
                        promoCode.innerHTML =JSON.stringify(promoObject);
                        uiRenderer.toast("Please wait for reload...", "Trying to stop");
                    };

                    window.openStatsChart = function openStatsChart() {
                        if(myBarChart) { myBarChart.destroy(); }
                        let statsFragment = document.getElementById("stats-fragment");
                        if (statsFragment.style.display === "block") { statsFragment.style.display = "none"; document.getElementById("stats-button").innerText = "Lucky Number Stats"; } else {
                            statsFragment.style.display = "block"; document.getElementById("stats-button").innerText = "Close Stats";
                            var canvas = document.getElementById("barChart");
                            var ctx = canvas.getContext("2d");
                            var dataSpan = document.getElementById("rolls-span");
                            var data = {
                                labels: ["0000-9885", "9886-9985", "9986-9993", "9994-9997", "9998-9999", "10000"],
                                datasets: [ { fill: false, backgroundColor: [ "#990000", "#660066", "#000099", "#ff8000", "#ffff00", "#00ff00"],
                                             data: dataSpan.innerText.split(",") } ] };
                            var options = { plugins: { legend: { display: false } }, title: { display: true, text: "Rolled Numbers", position: "top" }, rotation: -0.3 * Math.PI };
                            myBarChart = new Chart(ctx, { type: "doughnut", data: data, options: options });
                        }
                    };

                    window.shortlinkAlert = {
                        load: function(id, destination) {
                            let hideShortlinkAlerts = localStorage.getItem("hideShortlinkAlerts");
                            hideShortlinkAlerts = hideShortlinkAlerts ? JSON.parse(hideShortlinkAlerts) : false;

                            if (hideShortlinkAlerts) {
                            } else {
                                document.getElementById(id).classList.remove("d-none");
                            }
                        },
                        save: function () {
                            localStorage.setItem("hideShortlinkAlerts", JSON.stringify(document.getElementById("hideShortlinkAlerts").checked));
                            window.open("https://example.com", "_blank");
                        }
                    }
                }
            };

            let logLines = [];
            function init(cfFaucets, schedules) {
                appendJavaScript();
                appendHtml(schedules);
                updateSchedulesToggler();
                appendEventListeners();
                appendWidgets();
                setupEventerListeners();

                createPromoTable(cfFaucets);
                try {
                    document.querySelector('.page-title h1').innerHTML = 'Auto Claim';
                } catch (err) {}
            };
            function setupEventerListeners() {
                eventer.on('siteUpdated', (site) => {
                    manager.Site.sortAll(); // en todos los sites...
                    let schedule = manager.Schedule.getById(site.schedule);
                    schedule.sortSites(); // solo en el schedule de este site
                    schedule.setCurrentSite(); // solo en el schedule de este site
                    manager.Site.saveAll();
                    uiRenderer.sites.renderSiteRow(site); // solo la row de este site
                    uiRenderer.sites.sortSitesTable(); // y reordenar

                    schedule.checkNextRoll(); // solo en el schedule de este site    
                    convertToFiat();
                });
            }
            function appendWidgets() {
                $('.tableSortable').sortable({
                    placeholder:'sort-highlight',
                    handle:'.row-handle',
                    cursor: 'grabbing',
                    axis: 'y',
                    stop: function(event, ui) {
                        $("tbody.ui-sortable tr").each(function(index) {
                          $(this).attr("data-order", index);
                        });
                      }
                });
                $('#promo-daily').bootstrapSwitch();
                $('#bss-log').bootstrapSwitch({
                    onSwitchChange(event, state) {
                        $('#console-log').collapse('toggle');
                    },
                    onInit: function(event, state) {
                        this.$element.closest('.bootstrap-switch-container').find('.bootstrap-switch-handle-on').first().addClass('fa fa-eye').text('');
                        this.$element.closest('.bootstrap-switch-container').find('.bootstrap-switch-handle-off').first().addClass('fa fa-eye-slash').text('');
                    }
                });
            };
            function updateSchedulesToggler() {
                let container = document.querySelector('#schedules-toggler');
                let html = '<label class="btn btn-outline-primary active" data-schedule="all"><input type="radio" name="options" autocomplete="off" checked="true"> All</label>';
                manager.Schedule.getAll().forEach(x => {
                    html += `<label class="btn btn-outline-primary" data-schedule="${x.uuid}">
                    <i class="fas fa-square pr-1" style="color: #${x.uuid};"></i>${x.name}
                    <input type="radio" name="options" autocomplete="off">
                    </label>`;
                });
                container.innerHTML = html;
                startSchedulesInterval(manager.Schedule.getAllForCrud().map(x => x.uuid));
                uiRenderer.schedules.toggleSchedule('all');
            };
            function appendEventListeners() {
                document.querySelector('.dropdown-settings-menu').addEventListener('click', function(e) {
                    let actionElement = e.target.tagName === 'I' ? e.target.parentElement : e.target;
                    if (actionElement.dataset.target) {
                        e.stopPropagation();
                        uiRenderer.openModal(actionElement.dataset.target);
                    }
                });

                const modalSchedules = document.querySelector('#modal-schedules');
                modalSchedules.addEventListener('click', function(e) {
                    let actionElement = e.target.tagName === 'I' ? e.target.parentElement : e.target;
                    if (actionElement.classList.contains('action-schedule-add')) {
                        e.stopPropagation();
                        let rows = modalSchedules.querySelectorAll('table tbody tr');
                        let rndColor = helpers.randomHexColor();
                        let rowTemplate = uiRenderer.schedules.renderRow({
                            uuid: rndColor,
                            name: rndColor,
                            order: rows.length,
                            added: true
                        });
                        $(modalSchedules.querySelector('table tbody tr:last-child')).after(rowTemplate);
                        uiRenderer.appendColorPickers('table tbody tr:last-child .color-picker');
                    } else if (actionElement.classList.contains('action-schedule-remove')) {
                        let rows = modalSchedules.querySelectorAll('table tbody tr:not(.d-none)');
                        if (rows.length <= 1) {
                            alert('You need to keep at least 1 schedule');
                        } else {
                            let current = actionElement.closest('tr');
                            if (current.dataset.added === 'true') {
                                current.remove();
                            } else {
                                current.dataset.removed = 'true';
                                current.classList.add('d-none');
                            }
                        }
                    } else if (actionElement.classList.contains('modal-save')) {
                        let data = uiRenderer.parseTable(modalSchedules.querySelector('table'));
                        let isValid = manager.Schedule.crud(data);
                        updateSchedulesToggler();
                        manager.resyncAll({withUpdate: true});
                        if (!isValid) {
                            uiRenderer.toast('Some schedules might have errors/invalid colors', 'warning');
                        }
                    }
                });
            };
            function appendJavaScript() {
                addJS_Node (null, null, injectables.managerJs);
            };
            function addCardHtml(obj) {
                return `<div class="card m-1"><div class="card-header">${obj.header}</div><div class="card-body px-4">${obj.body}</div></div>`;
            };
            function addRandomBetween(propSelect, propMin, propMax) {
                return `<table><tr><td>
                  <select class="form-control" ${propSelect.name}="${propSelect.value}">
                   <option value="0">Random between...</option><option value="15">15 minutes</option><option value="30">30 minutes</option><option value="35">35 minutes</option><option value="45">45 minutes</option><option value="65">65 minutes</option><option value="90">90 minutes</option><option value="120">120 minutes</option>
                  </select></td>
                  <td><input type="number" data-original="" ${propMin.name}="${propMin.value}" min="1" value="15" step="5" class="form-control"></td><td>and</td><td><input type="number" data-original="" ${propMax.name}="${propMax.value}" value="65" step="5" class="form-control"></td><td>minutes</td></tr></table>`;
            }
            function appendHtml(schedules) {
                let html ='';

                html += '<div class="modal fade" id="confirmable-modal" tabindex="-1" role="dialog" aria-hidden="true">';
                html += '<div class="modal-dialog modal-sm modal-dialog-centered"><div class="modal-content">';
                html += '<div class="modal-header"><h4 class="modal-title">Are you sure?</h4><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button></div>';
                html += '<div class="modal-body"><p></p></div>';
                html += '<div class="modal-footer justify-content-between"><button type="button" class="btn btn-default" data-dismiss="modal">No</button>';
                html += '<button type="button" class="btn btn-primary" data-dismiss="modal" id="confirm-req-btn" onclick="confirmable.accept()">Yes</button></div>';
                html += '</div></div>';
                html += '</div>';

                html += '<div class="modal fade" id="modal-dlg" tabindex="-1" role="dialog" data-backdrop="static" aria-hidden="true">';
                html += ' <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document">';

                html += '<div class="modal-content bg-beige" id="modal-spinner"><div class="modal-body"><div class="d-flex justify-content-center"><span id="target-spinner" class="d-none"></span><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Loading data</div></div></div>';

                html += '  <div class="modal-content bg-beige d-none" id="modal-ereport">';
                html += '   <div class="modal-header"><h5 class="modal-title"><i class="fa fa-history"></i> Submit an Error</h5></div>';
                html += '    <div class="modal-body">';
                html += '     <div class="alert alert-danger">Don\'t send private information as data might be publicly access.</div>';
                html += '      <textarea rows="4" id="log-message" class="form-control" placeholder="PLEASE do not send logs without describing here the issue you are facing..."></textarea>';
                html += '      <label for="log-textarea">Log</label>';
                html += '      <textarea rows="10" id="log-textarea" class="form-control"></textarea>';
                html += '    </div>';
                html += '    <div class="modal-footer"><a class="btn m-2 anchor btn-outline-danger align-middle" onclick="modalCancel(\'ereport\')" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '    <a class="btn m-2 anchor btn-outline-success align-middle" onclick="modalSave(\'ereport\')" data-dismiss="modal"><i class="fa fa-paper-plane"></i> Send</a></div>';
                html += '  </div>';

                html += '  <div class="modal-content bg-beige d-none" id="modal-wallet">';
                html += '   <div class="modal-header"><h5 class="modal-title"><i class="fa fa-wallet"></i> Your Addresses</h5></div>';
                html += '    <div class="modal-body">';
                html += '     <div><table class="table custom-table-striped" id="wallet-table">';
                html += '          <thead><tr><th class="">Name</th><th class="">Address</th></tr></thead>';
                html += '          <tbody class="overflow-auto" id="wallet-table-body"></tbody></table><textarea rows="14" id="wallet-json" class="d-none w-100"></textarea>';
                html += '     </div>';
                html += '    </div>';
                html += '<div class="modal-footer">';
                html += '<div class="footer-json d-none">';
                html += '<a class="btn m-2 anchor btn-outline-danger align-middle" onclick="editWallet.toggleJson(\'cancel\')"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '<a class="btn m-2 anchor btn-outline-primary align-middle" onclick="editWallet.toggleJson()"><i class="fa fa-edit"></i> Confirm</a></div>';
                html += '<div class="footer-table"><a class="btn m-2 anchor btn-outline-primary align-middle" onclick="editWallet.toggleJson()"><i class="fa fa-edit"></i> Edit as JSON</a>';
                html += '<a class="btn m-2 anchor btn-outline-danger align-middle" onclick="modalCancel(\'wallet\')" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '<a class="btn m-2 anchor btn-outline-success align-middle" onclick="modalSave(\'wallet\')" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a></div></div>';
                html += '   </div>';

                html += '  <div class="modal-content bg-beige d-none" id="modal-info">';
                html += '   <div class="modal-header"><h5 class="modal-title"><i class="fa fa-info"></i> Info</h5></div>';
                html += '    <div class="modal-body">';
                html += '<ul>';
                html += '<li>Almost all sites in the list require an external hCaptcha solver, you can find one in our <a href="https://discord.gg/23s9fDgHqe" target="_blank">discord</a>.</li>';
                html += '<li>Stormgain requires a GeeTest solver. You can use <a href="https://greasyfork.org/en/scripts/444560" target="_blank">this script</a> to solve the captchas through 2Captcha API service.</li>';
                html += '<li>You can set default configurations at Settings</li>';
                html += '<li>You can override configurations for a specific site using the edit (<i class="fa fa-edit"></i>) buttons</li>';
                html += '<li>Some sites might only work if the tab running it is on focus</li>';
                html += '<li>When enabling a new site, try it first with the tab on focus, to detect potential issues</li>';
                html += '<li>You can enable the log in Settings to detect processing problems</li>';
                html += '</ul>';
                html += '    </div>';
                html += '<div class="modal-footer">';
                html += '<a class="btn m-2 anchor btn-outline-warning align-middle" data-dismiss="modal"><i class="fa fa-edit"></i> Close</a></div>';
                html += '   </div>';

                html += '<div class="modal-content bg-beige" id="modal-schedules">';
                html += '    <div class="modal-header py-2"><h5 class="modal-title"><i class="fa fa-stopwatch"></i> Schedules</h5>';
                html += '        <div class="ml-auto"><button type="button" class="btn btn-default btn-sm action-schedule-add">';
                html += '            <i class="fa fa-plus"></i> Add Schedule';
                html += '        </button></div>';
                html += '    </div>';
                html += '    <div class="modal-body">';
                html += '<div class="callout callout-warning m-0"><p class="text-justify">Each schedule opens sites in a new/different tab.<br>Colors must be unique.</p></div>';
                html += '    <table class="table">';
                html += '        <thead>';
                html += '        <tr><th></th><th class="text-center" width="35%">Color</th><th class="text-center">Name</th><th></th></tr>';
                html += '        </thead>';
                html += '        <tbody class="tableSortable">';
                html += '        </tbody>';
                html += '    </table>';
                html += '    </div>';
                html += '    <div class="modal-footer">';
                html += '    <a class="btn m-2 anchor btn-outline-danger align-middle" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '    <a class="btn m-2 anchor btn-outline-success align-middle modal-save" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a>';
                html += '    </div>';
                html += '</div>';

                html += '<div class="modal-content bg-beige" id="modal-assign-schedule">';
                html += '    <div class="modal-header py-2"><h5 class="modal-title"><i class="fa fa-exchange-alt"></i> Move to...</h5>';
                html += '    </div>';
                html += '    <div class="modal-body">';
                html += '      <div class="form-container">';
                html += '       <input type="hidden" name="site_id" value="not_set">';
                html += '       <input type="hidden" name="original_schedule_id" value="not_set">';
                html += '       <label class="control-label">Schedule</label>';
                html += '       <select class="form-control" name="schedule">';
                html += '       </select>';
                html += '      </div>';
                html += '    </div>';
                html += '    <div class="modal-footer">';
                html += '    <a class="btn m-2 anchor btn-outline-danger align-middle" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '    <a class="btn m-2 anchor btn-outline-success align-middle modal-save" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a>';
                html += '    </div>';
                html += '</div>';

                html += '<div class="modal-content bg-beige" id="modal-add-site">';
                html += '    <div class="modal-header py-2"><h5 class="modal-title"><i class="fa fa-code"></i> Add Site...</h5>';
                html += '    </div>';
                html += '    <div class="modal-body">';
                html += '      <div class="form-container">';
                html += uiRenderer.addInputTextHtml({ required: true, name: 'site_name', value: '', text: 'Display name'});
                html += uiRenderer.addInputTextHtml({ required: true, name: 'site_url', value: '', text: 'Url to open', placeholder: 'Example: https://freebitcoin.io/free' });
                html += '       <label class="control-label">Schedule</label>';
                html += '       <select class="form-control" name="schedule">';
                html += '       </select>';
                html += '      </div>';
                html += '    </div>';
                html += '    <div class="modal-footer">';
                html += '    <a class="btn m-2 anchor btn-outline-danger align-middle" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '    <a class="btn m-2 anchor btn-outline-success align-middle modal-save" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a>';
                html += '    </div>';
                html += '</div>';

                html += '<div class="modal-content bg-beige" id="modal-site-parameters">';
                html += '    <div class="modal-header py-2"><h5 class="modal-title"><i class="fa fa-edit"></i> Edit Site Parameters...</h5>';
                html += '    </div>';
                html += '    <div class="modal-body">';
                html += '      <div class="form-container"><form action="">';
                html += '      </form></div>';
                html += '    </div>';
                html += '    <div class="modal-footer">';
                html += '    <a class="btn m-2 anchor btn-outline-danger align-middle" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '    <a class="btn m-2 anchor btn-outline-success align-middle modal-save"><i class="fa fa-check-circle"></i> Save</a>';
                html += '    </div>';
                html += '</div>';

                html += '  <div class="modal-content bg-beige d-none" id="modal-slAlert">';
                html += '   <div class="modal-header"><h5 class="modal-title">Attention</h5></div>';
                html += '    <div class="modal-body">';
                html += '     <div class="alert alert-warning">You will be redirected to a shortlink, and after completing it the new Twitter Daily Promo Code will be added to your table.<br>';
                html += 'This is an optional contribution. You can still get the code the old fashion way.</div>';
                html += uiRenderer.addLegacySliderHtml('id', 'hideShortlinkAlerts', `Stop warning me before a shortlink`);
                html += '   </div>';
                html += '<div class="modal-footer"><a class="btn m-2 anchor btn-outline-danger align-middle" onclick="modalCancel(\'slAlert\')" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '<a class="btn m-2 anchor btn-outline-success align-middle" onclick="modalSave(\'slAlert\')" data-dismiss="modal"><i class="fa fa-external-link-alt"></i> Lets Go!</a></div>';
                html += '   </div>';

                html += '  <div class="modal-content bg-beige d-none" id="modal-site">';
                html += '    <div class="modal-header"><h5 class="modal-title"><i class="fa fa-clock"></i> <span id="faucet-name" data-id=""></span> Schedule Parameters</h5></div>';
                html += '    <div class="modal-body">';
                html += '     <div class="alert alert-warning">Override Settings for the selected faucet.<br>Faucet-specific configurations will be moved here soon.</div>';
                html += '     <div class="row">';

                html += '     <div class="col-md-12 col-sm-12">';
                html += addCardHtml({
                    header: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.workInBackground.override', 'Override Work Mode'),
                    body: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.workInBackground', 'Open tab in background')
                });
                html += addCardHtml({
                    header: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.nextRun.override', 'Override Next Run'),
                    body: `<div>${uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.nextRun.useCountdown', 'Use faucet countdown when possible')}</div>` +
                    `<label class="control-label">Otherwise wait:</label>` +
                    addRandomBetween({ name: 'data-site-prop', value: 'defaults.nextRun' }, { name: 'data-site-prop', value: 'defaults.nextRun.min' }, { name: 'data-site-prop', value: 'defaults.nextRun.max' })
                });
                html += addCardHtml({
                    header: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.sleepMode.override', 'Override Sleep Mode'),
                    body: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.sleepMode', 'Sleep mode') +
                    `<table><tr><td>Don't claim between </td><td><input type="time" data-original="" data-site-prop="defaults.sleepMode.min" class="form-control"></td><td>and</td>
                  <td><input type="time" data-original="" data-site-prop="defaults.sleepMode.max" class="form-control"></td></tr></table>`
                });
                html += '         <div class="card m-1"><div class="card-header">Timeout</div>';
                html += '           <div class="card-body px-4">';
                html += addCardHtml({
                    header: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.timeout.override', 'Override Timeout'),
                    body: `<table><tr><td>After</td><td><input type="number" data-original="" data-site-prop="defaults.timeout" min="2" value="5" step="1" class="form-control"></td><td>minutes</td></tr></table>`
                });
                html += addCardHtml({
                    header: uiRenderer.addLegacySliderHtml('data-site-prop', 'defaults.postponeMinutes.override', 'Override Postpone'),
                    body: `<label class="control-label">After timeout/error, postpone for:</label>` +
                    addRandomBetween({ name: 'data-site-prop', value: 'defaults.postponeMinutes' }, { name: 'data-site-prop', value: 'defaults.postponeMinutes.min' }, { name: 'data-site-prop', value: 'defaults.postponeMinutes.max' })
                });
                html += '       </div>';
                html += '     </div>';
                html += '    </div>';
                html += '     </div>';
                html += '    </div>';
                html += '<div class="modal-footer"><a class="btn m-2 anchor btn-outline-danger align-middle" onclick="modalCancel(\'site\')" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '<a class="btn m-2 anchor btn-outline-success align-middle" onclick="modalSave(\'site\')" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a></div>';
                html += '   </div>';

                html += '<div class="modal-content bg-beige d-none" id="modal-config">';
                html += '  <div class="modal-header"><h5 class="modal-title"><i class="fa fa-cog"></i> Settings</h5></div>';
                html += '  <div class="modal-body">';
                html += '     <div class="row">';

                html += '     <div class="col-md-12 col-sm-12">';
                html += '         <div class="card card-info m-1"><div class="card-header">Defaults<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-minus"></i></button></div></div>';
                html += '           <div class="card-body px-4">';
                html += `<div>${uiRenderer.addLegacySliderHtml('data-prop', 'defaults.workInBackground', 'Open tabs in background')}</div>`;
                html += `<div>${uiRenderer.addLegacySliderHtml('data-prop', 'defaults.extraInterval', 'Use extra timer to detect ad redirects faster')}</div>`;

                html += addCardHtml({
                    header: 'Next Run',
                    body: `<div>${uiRenderer.addLegacySliderHtml('data-prop', 'defaults.nextRun.useCountdown', 'Use faucet countdown when possible')}</div>` +
                    `<label class="control-label">Otherwise wait:</label>` +
                    addRandomBetween({ name: 'data-prop', value: 'defaults.nextRun' }, { name: 'data-prop', value: 'defaults.nextRun.min' }, { name: 'data-prop', value: 'defaults.nextRun.max' })
                });
                html += addCardHtml({
                    header: 'Timeout',
                    body: `<table><tr><td>After</td><td><input type="number" data-original="" data-prop="defaults.timeout" min="2" value="5" step="1" class="form-control"></td><td>minutes</td></tr></table>` +
                    `<label class="control-label">After timeout/error, postpone for:</label>` +
                    addRandomBetween({ name: 'data-prop', value: 'defaults.postponeMinutes' }, { name: 'data-prop', value: 'defaults.postponeMinutes.min' }, { name: 'data-prop', value: 'defaults.postponeMinutes.max' })
                });
                html += addCardHtml({
                    header: 'Logging',
                    body: `<div>${uiRenderer.addLegacySliderHtml('data-prop', 'devlog.enabled', 'Store log (enables the \'Log\' button)')}</div>` +
                    `<table><tr><td>Max log size in lines:</td><td><input type="number" data-original="" data-prop="devlog.maxLines" min="100" step="100" class="form-control"></td></tr></table>`
                });
                html += addCardHtml({
                    header: uiRenderer.addLegacySliderHtml('data-prop', 'defaults.sleepMode', 'Sleep mode'),
                    body: `<table><tr><td>Don't claim between </td><td><input type="time" data-original="" data-prop="defaults.sleepMode.min" class="form-control"></td><td>and</td>
                  <td><input type="time" data-original="" data-prop="defaults.sleepMode.max" class="form-control"></td></tr></table>`
                });
                html += '       </div></div>';
                html += '     </div>';

                html += '     <div class="col-md-12 col-sm-12">';
                html += '         <div class="card card-info m-1"><div class="card-header">Site Specifics<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-minus"></i></button></div></div>';
                html += '           <div class="card-body px-4">';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">CryptosFaucets<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '          <div><label class="switch"><input type="checkbox" data-prop="cf.tryGetCodes" ><span class="slider round"></span></label> Auto update promo codes </div>';
                html += '          <div><label class="switch"><input type="checkbox" data-prop="cf.rollOnce" ><span class="slider round"></span></label> Roll once per round </div>';
                html += '          <div><label class="switch"><input type="checkbox" data-prop="cf.autologin" ><span class="slider round"></span></label> Autologin when necessary</div>';
                html += '           <select class="form-control" data-prop="cf.credentials.mode">';
                html += '            <option value="1">Use Email and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="cf.credentials.email" required="required" class="form-control" placeholder="Email address..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="cf.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '           <label class="control-label">Hours to wait If IP is banned:</label>';
                html += '           <select class="form-control" data-prop="cf.sleepHoursIfIpBan">';
                html += '            <option value="0">Disabled</option><option value="2">2</option><option value="4">4</option><option value="8">8</option><option value="16">16</option><option value="24">24</option><option value="26">26</option>';
                html += '           </select>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">FPig<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">Login Mode</label>';
                html += '           <select class="form-control" data-prop="fpb.credentials.mode">';
                html += '            <option value="1">Use Username and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="fpb.credentials.username" required="required" class="form-control" placeholder="Email address..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="fpb.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">FreeBCH<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">Login Mode</label>';
                html += '           <select class="form-control" data-prop="fbch.credentials.mode">';
                html += '            <option value="1">Use Username and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="fbch.credentials.username" required="required" class="form-control" placeholder="Email address..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="fbch.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">JTFey<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">Login Mode</label>';
                html += '           <select class="form-control" data-prop="jtfey.credentials.mode">';
                html += '            <option value="1">Use Username and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="jtfey.credentials.username" required="required" class="form-control" placeholder="Email address..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="jtfey.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">BscAds<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">Login Mode</label>';
                html += '           <select class="form-control" data-prop="bscads.credentials.mode">';
                html += '            <option value="1">Use Username and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="bscads.credentials.username" required="required" class="form-control" placeholder="Username..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="bscads.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">FaucetPay PTC<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <div><label class="switch"><input type="checkbox" data-prop="fp.randomPtcOrder" ><span class="slider round"></span></label> Random PTC order </div>';
                html += '           <label class="control-label">Max duration per run:</label>';
                html += '           <select class="form-control" data-prop="fp.maxTimeInMinutes">';
                html += '            <option value="5">5 minutes</option><option value="10">10 minutes</option><option value="15">15 minutes</option><option value="30">30 minutes</option>';
                html += '           </select>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">Dutchy<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <div><label class="switch"><input type="checkbox" data-prop="dutchy.useBoosted" ><span class="slider round"></span></label> Try boosted roll </div>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">BestChange<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">BTC Address:</label>';
                html += '           <select class="form-control" data-prop="bestchange.address">';
                html += '            <option value="101">Faucet Pay BTC</option><option value="1">BTC Alt Address</option>';
                html += '           </select>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">SatoHost<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">Login Mode</label>';
                html += '           <select class="form-control" data-prop="shost.credentials.mode">';
                html += '            <option value="1">Use Username and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="shost.credentials.username" required="required" class="form-control" placeholder="Username..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="shost.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '       </div></div>';

                html += '         <div class="card m-1 collapsed-card"><div class="card-header">Yes Coiner<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
                html += '           <div class="card-body px-4" style="display: none;">';
                html += '           <label class="control-label">Login Mode</label>';
                html += '           <select class="form-control" data-prop="ycoin.credentials.mode">';
                html += '            <option value="1">Use Username and Password</option><option value="2">Filled by 3rd party software/extension</option>';
                html += '           </select>';
                html += '           <label class="control-label">E-Mail</label>';
                html += '           <input maxlength="200" type="text" data-prop="ycoin.credentials.username" required="required" class="form-control" placeholder="Account number..."/>';
                html += '           <label class="control-label">Password</label>';
                html += '           <input maxlength="200" type="password" data-prop="ycoin.credentials.password" required="required" class="form-control" placeholder="Password..."/>';
                html += '       </div></div>';

                html += '    </div></div>';
                html += '  </div>';
                html += ' </div>';
                html += '</div>';
                html += '<div class="modal-footer"><a class="btn m-2 anchor btn-outline-danger align-middle" onclick="modalCancel(\'config\')" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '<a class="btn m-2 anchor btn-outline-success align-middle" onclick="modalSave(\'config\')" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a></div>';
                html += '   </div>';

                html += '</div>';
                html += '</div>';

                html += '<section id="table-struct" class="fragment "><div class="container-fluid "><div class="py-1 "><div class="row mx-0 justify-content-center">';
                html += '<a class="btn m-2 anchor btn-outline-danger align-middle" data-toggle="modal" data-target="#confirmable-modal" onclick="confirmable.open(\'forceStopFaucet\', \'Running faucet will be disabled and the manager will reload.\')"><i class="fa fa-stop-circle"></i>Force Stop</a>';
                html += '</div>';

                html += '<div class="card">';

                html += '<div class="card-header">';
                html += '<div class="d-flex p-0">';

                html += '<div id="schedules-toggler" class="btn-group btn-group-toggle" data-toggle="buttons">';

                html += '</div>';

                html += '<div class="card-tools ml-auto mt-2 mr-1">';
                html += '<input type="checkbox" data-toggle="switch" data-label-text="Log" title="Show/Hide Log" id="bss-log" checked>';

                html += `<button type="button" class="btn btn-flat btn-sm btn-outline-primary mx-1 dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><i class="fa fa-cog"></i> Settings</button>
                <div class="dropdown-menu text-sm dropdown-settings-menu" style="">
                <a class="dropdown-item btn-open-dialog" data-target="modal-config"><i class="fa fa-cog"></i>&nbsp;Defaults...</a>
                <div class="dropdown-divider"></div>
                <a class="dropdown-item btn-open-dialog" data-target="modal-schedules"><i class="fa fa-stopwatch"></i>&nbsp;Schedules...</a>
                <a class="dropdown-item btn-open-dialog" data-target="modal-wallet"><i class="fa fa-wallet"></i>&nbsp;Wallets...</a>
                <!-- <a class="dropdown-item btn-open-dialog" data-target="modal-sites"><i class="fa fa-window-restore"></i>&nbsp;Sites...</a> -->
                <div class="dropdown-divider"></div>
                <a class="dropdown-item btn-open-dialog" data-target="modal-ereport"><i class="fa fa-history"></i>&nbsp;Log...</a>
                <div class="dropdown-divider"></div>
                <a class="dropdown-item btn-open-dialog" data-target="modal-info"><i class="fa fa-info"></i>&nbsp;Help/Info...</a>
                </div>`;

                html += '<button type="button" class="btn btn-tool" data-card-widget="collapse"><i class="fas fa-minus"></i></button>';
                html += '<button type="button" class="btn btn-tool mx-1" data-card-widget="maximize"><i class="fas fa-expand"></i></button>';
                html += '</div></div>';
                html += '<div id="wait-times" class="row mx-0 p-0 justify-content-center"></div>';
                html += '</div>';

                html += '<div class="card-body table-responsive p-0" style="height: 400px;" id="schedule-container">';
                html += '<pre class="collapse show" id="console-log"><b>Loading...</b></pre>';
                html += '</div>';

                html += '</div>';

                html += '</div>';
                html += '<span id="update-data" style="display:none;"></span></section>';
                html += '<section id="table-struct-promo" class="fragment "><div class="container-fluid "><div class="py-1 ">';

                html += '<div class="card"><div class="card-header"><h3 class="card-title font-weight-bold">Promo Codes</h3><span id="promo-code-new" style="display:none;"></span>';
                html += '<div class="card-tools">';

                html += '<div class="input-group input-group-sm btn-tool">';
                html += '<input id="promo-text-input" type="text" name="table_search" class="form-control float-right" list="promoCode_list" placeholder="CF Promo Code..." style="width:130px;">';
                html += '<input type="checkbox" data-toggle="switch" title="Check if the code can be reused every 24hs" id="promo-daily" data-on-text="Daily" data-off-text="1 Time">';
                html += '<div class="input-group-append"><button type="submit" class="btn btn-default" id="promo-button""><i class="fas fa-plus"></i> Add</button></div>';
                html += '<div class="input-group-append"><button type="submit" class="btn btn-default btn-outline-danger mx-1" data-toggle="modal" data-target="#confirmable-modal" onclick="confirmable.open(\'removeAllPromos\', \'All promo codes will be removed.\')"><i class="fas fa-times-circle"></i> Remove All</button></div>';
                html += '<div class="input-group-append"><button type="submit" class="btn btn-default btn-outline-primary" id="button-try-get-codes"><i class="fas fa-bolt"></i> Try to Get Codes</button></div>';
                html += '<div class="input-group-append"><button type="button" class="btn btn-tool btn-sm mx-1" data-card-widget="collapse" title="Collapse"><i class="fas fa-minus"></i></button></div>';
                html += '<div class="input-group-append"><button type="button" class="btn btn-tool btn-sm mx-1" data-card-widget="maximize" title="Maximize"><i class="fas fa-expand"></i></button></div>';
                html += '</div>';
                html += '<datalist id="promoCode_list">';
                K.CF.ReusableCodeSuggestions.forEach( function(x) { html += '<option>' + x + '</option>' });
                html += '</datalist>';
                html += '</div>';

                html += '</div>';
                html += '<div class="card-body table-responsive p-0" id="promo-container">';
                html += '</div></div>';

                html +='</div></div></section>';
                html += '<section class="fragment"><div class="container-fluid ">';
                html += '<div class="row justify-content-center"><a class="btn  m-2 anchor btn-outline-primary" id="stats-button" onclick="openStatsChart()">CF Lucky Number Stats</a></div>';
                html +='<div class="py-1" id="stats-fragment" style="display:none;"><div class="row align-items-center text-center justify-content-center">';
                html += '<div class="col-md-12 col-lg-8"><canvas id="barChart"></canvas><span id="rolls-span" style="display:none;"></span></div></div></div></div></div></section>';

                let wrapper = document.createElement('div');
                wrapper.innerHTML = html.trim();

                let tgt = document.querySelector('div.row.py-3');
                if (tgt) {
                    let rowDiv = document.createElement('div');
                    rowDiv.innerHTML = '<div class="row py-3 ac-log"><div class="col-12 justify-content-center"><div class="card"><div class="card-body" id="referral-table"></div></div></div></div>';
                    tgt.after(rowDiv);
                }

                let target = document.getElementById('referral-table');
                target.parentNode.insertBefore(wrapper, target);
                document.getElementById('schedule-container').appendChild( createScheduleTable() );

                if (document.querySelector('.main-header .navbar-nav.ml-auto')) {
                    let discord = document.createElement('li');
                    discord.classList.add('nav-item');
                    discord.innerHTML = '<a class="btn btn-primary btn-sm m-1" href="https://discord.gg/23s9fDgHqe" target="_blank"><div class="">discord</div></a>';
                    document.querySelector('.main-header .navbar-nav.ml-auto').prepend(discord);
                } else {
                    let discord = document.createElement('div');
                    discord.innerHTML = '<a class="btn m-2 btn-primary" href="https://discord.gg/23s9fDgHqe" target="_blank"><div class="">discord</div></a>';
                    document.querySelector('.navbar-nav').prepend(discord);
                }
            };
            function createPromoTable(faucets) {
                let table = document.createElement('table');
                let inner = '';
                table.classList.add('table', 'custom-table-striped');
                table.setAttribute('id','promo-table');

                inner += '<caption style="text-align: -webkit-center;">⏳ Pending ✔️ Accepted 🕙 Used Before ❌ Invalid code ❗ Unknown error ⚪ No code</caption>';
                inner += '<thead><tr><th class="">Code</th><th class="">Added</th>';

                for (let i = 0, all = faucets.length; i < all; i++) {
                    inner += '<th data-faucet-id="' + faucets[i].id + '">' + faucets[i].name + '</th>';
                }

                inner += '</tr></thead><tbody id="promo-table-body"></tbody></table>';

                table.innerHTML = inner
                document.getElementById('promo-container').appendChild( table );
            };
            function createScheduleTable() {
                let table = document.createElement('table');
                let inner;
                table.classList.add('table', 'custom-table-striped', 'table-head-fixed', 'text-nowrap');
                table.setAttribute('id','schedule-table');

                inner = '<thead><tr>';
                inner += '<th scope="col" class="edit-status d-none em-only" style="">Active</th><th class="">Next Roll</th><th class=""></th><th class="">Name</th><th class="text-center">Last Claim</th>';
                inner += '<th class="text-center">Aggregate</th><th class="text-center">Balance</th><th class="text-center em-hide" id="converted-balance-col">FIAT</th>';
                inner += '<th scope="col" class="text-center em-hide">Msgs</th>';
                inner += '<th scope="col" class="" style="">';
                inner += `<div class="btn-group btn-group-sm">
                <button type="button" data-toggle="tooltip" title="Add site..." class="btn btn-default action-add-external-site em-hide">
                    <i class="fa fa-plus"></i>
                </button>
                <button type="button" title="Cancel" class="btn btn-danger action-edit-all-sites-cancel em-only d-none"><i class="fa fa-times-circle"></i> Cancel</button>
                <button type="button" title="Save" class="btn btn-success action-edit-all-sites-save em-only d-none"><i class="fa fa-check-circle"></i> Save</button>
                <button type="button" data-toggle="tooltip" title="Edit all..." class="btn btn-default action-edit-all-sites em-hide"><i class="fa fa-toggle-off"></i></button>
                </div>`;
                inner += '</th></tr></thead><tbody id="schedule-table-body"></tbody>';
                table.innerHTML = inner;

                return table;
            };
            function renderLogRow(data) {
                let tr = document.createElement('tr');
                tr.dataset.schedule = data.schedule;
                tr.dataset.ts = data.ts.getTime();
                tr.dataset.siteName = data.siteName || '';
                tr.dataset.elapsed = data.elapsed || '';
                let color = data.schedule ? `#${data.schedule}` : `transparent`;
                let showIt = !data.schedule || !uiRenderer.schedules.selectedSchedule 
                            || uiRenderer.schedules.selectedSchedule == 'all' || uiRenderer.schedules.selectedSchedule == data.schedule;
                if (!showIt) {
                    tr.classList.add('d-none');
                }

                let tds = '';
                tds += `<td>${helpers.getPrintableTime(data.ts)}</td>`;
                tds += `<td><i class="fas fa-square pr-1" style="color: ${color};"></i></td>`;
                if (data.elapsed) {
                    tds += `<td>${data.msg} [Elapsed time: ${data.elapsed} seconds]</td>`;
                } else {
                    tds += `<td>${data.msg}</td>`;
                }
                tr.innerHTML = tds;

                document.querySelector('#console-log table').appendChild(tr);
            };
            function log(data) {
                if (!data || !data.msg) {
                    console.warn(`Log attempt without data or msg!`, data);
                    return;
                }
                data.ts = new Date();
                data.schedule = data.schedule || false;
                data.siteName = data.siteName || false;
                data.elapsed = data.elapsed || false;

                if(shared.getConfig()['devlog.enabled']) {
                    if (data.schedule) {
                    } else {
                    }
                };

                if (data.elapsed) {
                    let previous = logLines.find(x => x.msg == data.msg && x.schedule == data.schedule);
                    if (previous) {
                        previous.elapsed = data.elapsed;
                        previous.ts = data.ts;
                        logLines.sort( (a, b) => b.ts.getTime() - a.ts.getTime());
                    } else {
                        logLines.unshift(data);
                    }
                } else {
                    logLines.unshift(data);
                }
                while(logLines.length > 30) {
                    logLines.pop();
                }

                document.querySelector('#console-log table').innerHTML = '';
                logLines.forEach(r => renderLogRow(r));
            };
            function legacyLog(data, elapsed = false) {
                if (!data || !data.msg) {
                    return;
                }
                elapsed = data.elapsed || false;
                let msg = data.msg;
                if (data.schedule) {
                    msg = `[${data.schedule}] ${data.msg}`;
                }

                if(shared.getConfig()['devlog.enabled']) { shared.devlog(msg, elapsed) };
                if(msg) {
                    let waitingIdx = logLines.findIndex(line => {
                        let waitingMsg= line.split('&nbsp')[1];
                        if (waitingMsg == msg) {
                            return true;
                        }
                    });

                    let previous = waitingIdx > -1 ? logLines[waitingIdx].split('&nbsp')[1] : '';
                    if (elapsed && (previous == msg)) {
                        logLines[waitingIdx] = helpers.getPrintableTime() + '&nbsp' + msg + '&nbsp[Elapsed time:&nbsp' + elapsed + '&nbspseconds]';
                    } else {
                        while(logLines.length > 20) {
                            logLines.pop();
                        }
                        logLines.unshift(helpers.getPrintableTime() + '&nbsp' + msg);
                    }

                    document.getElementById('console-log').innerHTML = logLines.map(x => {
                        const regex = /\[([0-9a-fA-F]+)\]/;
                        const match = regex.exec(x);
                        let colorNumber = null;
                        if (match !== null) {
                          colorNumber = match[1];
                        }

                        let showIt = !colorNumber || !window.selectedSchedule || window.selectedSchedule == 'all' || window.selectedSchedule == colorNumber;

                        const formattedMsg = x.replace(/\[([0-9a-fA-F]+)\]/, '<i class="fas fa-square pr-1" style="color: #$1;"></i>');

                        let line = `<span data-schedule="${colorNumber ? colorNumber : ''}" class="${showIt ? '' : 'd-none'}">${formattedMsg}</span>`;
                        return line;
                    }).join('');
                }
            };
            return {
                init: init,
                log: log
            }
        },
        createCFPromotions: function() {
            let codes = [];

            function PromotionCode(id, code, repeatDaily = false, expirationDate = null, isRemoved = false) {
                this.id = id;
                this.code = code;
                this.added = new Date();
                this.statusPerFaucet = [];
                this.repeatDaily = repeatDaily;
                this.lastExecTimeStamp = null;
                this.expirationDate = expirationDate;
                this.isRemoved = isRemoved;
            };

            function getFaucetStatusInPromo(promo, faucetId) {
                let faucet = promo.statusPerFaucet.find(x => x.id == faucetId);
                if (faucet.status && promo.repeatDaily) {
                    if((faucet.status == K.CF.PromoStatus.ACCEPTED && (Date.now() - faucet.execTimeStamp.getTime()) > K.Integers.HS_26_IN_MILLISECONDS)
                       || (faucet.status == K.CF.PromoStatus.USEDBEFORE && (Date.now() - faucet.execTimeStamp.getTime()) > K.Integers.HS_2_IN_MILLISECONDS)) {
                        faucet.status = K.CF.PromoStatus.PENDING;
                    }
                }
                return faucet.status ?? K.CF.PromoStatus.NOCODE;
            };

            function addNew(code, repeatDaily = false, expirationDate = null) {
                let found = codes.find(x => x.code == code);
                if (found) {
                    found.repeatDaily = repeatDaily;
                    found.expirationDate = expirationDate;
                    found.isRemoved = false;
                } else {
                    found = new PromotionCode(codes.length, code, repeatDaily, expirationDate);
                    codes.push(found);
                }

                found.statusPerFaucet = manager.getFaucetsForPromotion().map(x => {
                    return {
                        id: x.id,
                    };});
                found.statusPerFaucet.forEach(function (element, idx, arr) {
                    arr[idx].status = K.CF.PromoStatus.PENDING;
                    arr[idx].execTimeStamp = null;
                });

                save();
            };

            function includeNewCodes(newCodes) {
                for(let i=0; i<newCodes.length; i++) {
                    let item = newCodes[i];
                    let exists = codes.find(x => x.code.toLowerCase() == item.code.toLowerCase());
                    if (!exists) {
                        addNew(item.code, !item.oneTimeOnly, item.expirationDate);
                    } else {
                    }
                }
            };

            function getAll() {
                return codes.filter(x => !x.isRemoved);
            };

            function updateFaucetForCode(code, faucetId, newStatus) {
                let promo = codes.find(x => x.code == code);
                let faucet = promo.statusPerFaucet.find(x => x.id == faucetId);
                if(faucet) {
                    faucet.status = newStatus;
                    faucet.execTimeStamp = new Date();
                    promo.lastExecTimeStamp = faucet.execTimeStamp;
                }
                save();
            };

            function hasPromoAvailable(faucetId) {
                let resp = [];
                codes.forEach(function (promotion, idx, arr) {
                    let status = getFaucetStatusInPromo(promotion, faucetId);
                    if (status == K.CF.PromoStatus.PENDING && !promotion.isRemoved) {
                        resp.push(promotion.code);
                    }
                });
                if (resp.length > 0) {
                return resp;
                } else {
                    return false;
                }
            };

            function save() {
                persistence.save('CFPromotions', codes, true);
            };

            function load(data) {
                codes = data;
                save();
            };

            function removeAll() {
                codes.forEach(x => x.isRemoved = true);
                codes = codes.filter(x => x.expirationDate && Date.parse(x.expirationDate) > Date.now());
                save();
            };

            function remove(id, code) {
                let idx = codes.findIndex(x => x.id == id && x.code == code);
                if(idx != -1) {
                    codes[idx].isRemoved = true;
                    save();
                }

                return idx;
            };

            return {
                addNew: addNew,
                removeAll: removeAll,
                remove: remove,
                getAll: getAll,
                load: load,
                updateFaucetForCode: updateFaucetForCode,
                hasPromoAvailable: hasPromoAvailable,
                includeNewCodes: includeNewCodes
            }
        },
        createInteractions: function(){
            let randomInteractionLevel = K.RandomInteractionLevel.MEDIUM;
            let maxActions = 0;
            let performedActions = -1;
            let selectableElements;
            let actions = {
                available: [
                    function() {
                        $('html, body').animate({
                            scrollTop: helpers.randomInt(0, $('html, body').get(0).scrollHeight)
                        }, {
                            complete: setTimeout(interactions.addPerformed, helpers.randomMs(100, 3000)),
                            duration: helpers.randomMs(100, 1500)
                        });
                    },
                    function() {
                        let element = interactions.selectableElements[helpers.randomInt(0, interactions.selectableElements.length - 1)];

                        try {
                            if (document.body.createTextRange) {
                                const range = document.body.createTextRange();
                                range.moveToElementText(element);
                                range.select();
                            } else if (window.getSelection) {
                                const selection = window.getSelection();
                                const range = document.createRange();
                                range.selectNodeContents(element);
                                selection.removeAllRanges();
                                selection.addRange(range);
                            }
                        } catch (err) { }

                        interactions.addPerformed();
                    }
                ]
            };

            function start(selectableElements) {
                performedActions = 0;
                switch(randomInteractionLevel) {
                    case K.RandomInteractionLevel.NONE:
                        maxActions = 0;
                        break;
                    case K.RandomInteractionLevel.LOW:
                        maxActions = helpers.randomInt(2, 4);
                        break;
                    case K.RandomInteractionLevel.MEDIUM:
                        maxActions = helpers.randomInt(5, 8);
                        break;
                    case K.RandomInteractionLevel.HIGH:
                        maxActions = helpers.randomInt(12, 16);
                        break;
                }
                interactions.selectableElements = selectableElements;
                performActions();
            }

            function performActions() {
                if(performedActions >= maxActions) {
                    return;
                }
                let delay = 0;
                for(let i = 0; i < maxActions; i++) {
                    delay += helpers.randomMs(350, 1500);
                    setTimeout(actions.available[helpers.randomInt(0, actions.available.length - 1)], delay);
                }
            }

            function addPerformed() {
                performedActions++;
            }
            function completed() {
                return (performedActions >= maxActions);
            }

            return {
                start: start,
                completed: completed,
                addPerformed: addPerformed,
                selectableElements: selectableElements
            };
        },
        createSGProcessor: function() {
            let timerSpans;
            function run() {
                if(isLoading()) {
                    setTimeout(run, helpers.randomMs(5000, 10000));
                    return;
                } else if (hasPopup()) {
                    closePopup();
                    setTimeout(run, helpers.randomMs(5000, 10000));
                } else {
                    if(isMinerActive()) {
                        processRunDetails();
                    } else {
                        setTimeout(run, helpers.randomMs(5000, 10000));
                    }
                }
            };
            function hasPopup() {
                if (document.querySelector('div.absolute.flex.top-0.right-0.cursor-pointer.p-4.text-white.md-text-gray-1')) {
                    return true;
                }
                return false;
            };
            function closePopup() {
                try {
                    document.querySelector("div.absolute.flex.top-0.right-0.cursor-pointer.p-4.text-white.md-text-gray-1").click();
                    document.querySelector('svg.flex.w-8.h-8.fill-current').parentElement.click();
                } catch { shared.devlog(`@SG: error closing popup`); }
            };
            function isLoading() {
                return document.getElementById('loader-logo') ? true : false;
            };
            function isMinerActive() {
                timerSpans = document.querySelector('.font-bold.text-center.text-accent.w-11-12.text-18 span');

                if(timerSpans) {
                    return true;
                } else {
                    return false;
                }
                return (!!timerSpans);
            };
            function activateMiner() {
                let activateButton = document.querySelector("#region-main button.activate.block.w-full.h-full.mx-auto.p-0.rounded-full.select-none.cursor-pointer.focus-outline-none.border-0.bg-transparent");
                if (activateButton) {
                    activateButton.click();
                    setTimeout(run, helpers.randomMs(10000, 20000));
                } else {
                    processRunDetails()
                }
            };

            function processRunDetails() {
                let result = {};
                result.nextRoll = helpers.addMinutes(readCountdown().toString());
                result.balance = readBalance();
                shared.closeWindow(result);
            };

            function readCountdown() {
                let synchronizing = document.querySelector('.text-15.font-bold.text-center.text-accent'); // use
                let mins = 15;
                try {
                    let timeLeft = timerSpans.innerText.split(':');
                    if (timeLeft[0] == 'Synchronizing') {
                    }

                    if(timeLeft.length === 3) {
                        mins = parseInt(timeLeft[0]) * 60 + parseInt(timeLeft[1]);
                    }
                } catch (err) { shared.devlog(`SG Error reading countdown: ${err}`); }
                return mins;
            };
            function readBalance() {
                let balance = "";
                try {
                    balance = document.querySelector('span.text-accent').innerText + " BTC";
                } catch (err) { }
                return balance;
            };
            return {
                run: run,
                processRunDetails: processRunDetails
            };
        },
        createCFProcessor: function() {
            const NavigationProcess = {
                ROLLING: 1,
                PROCESSING_PROMOTION: 2,
                LOGIN: 3
            };
            let navigationProcess;
            let countdown;
            let rollButton;
            let promotionTag;
            let timeWaiting= 0;
            let loopingForErrors = false;
            let tempRollNumber = null;

            function init() {
                let urlType = helpers.cf.getUrlType(window.location.href);
                switch(urlType) {
                    case K.CF.UrlType.FREE:
                        if(localeConfig.setToEnglish) {
                            let refValue = document.querySelectorAll('.nav-item a')[4].innerHTML;
                            if (refValue != 'Settings') {
                                window.location.href = '/set-language/en';
                            }
                        }
                        addJS_Node (null, null, overrideSelectNativeJS_Functions);
                        interactions = objectGenerator.createInteractions();
                        run();
                        break;

                    case K.CF.UrlType.PROMOTION:
                        interactions = objectGenerator.createInteractions();
                        runPromotion();
                        break;

                    case K.CF.UrlType.HOME:
                        if (shared.getConfig()['cf.autologin']) {
                            addJS_Node (null, null, overrideSelectNativeJS_Functions);
                            doLogin();
                        } else {
                            shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, '');
                        }
                        break;

                    case K.CF.UrlType.CONTACTTWITTER:
                        shared.closeWithError(K.ErrorType.IP_BAN, '');
                        break;
                    default:
                        break;
                }
                return;
            }

            function run() {
                navigationProcess = NavigationProcess.ROLLING;
                displayStatusUi();
                setInterval(tryClosePopup, helpers.randomMs(3000, 6000));
                setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
            };

            function doLogin() {
                navigationProcess = NavigationProcess.LOGIN;
                displayStatusUi();

                setTimeout(findLoginForm, helpers.randomMs(2000, 5000));
            };

            function isFullyLoaded() { //Waits 55 seconds max
                if(document.readyState == 'complete' || timeWaiting == -1) {
                    document.getElementById('process-status').innerHTML = 'Interacting';
                    timeWaiting = 0;
                    interact();
                } else {
                    timeWaiting = -1;
                    document.getElementById('process-status').innerHTML = 'Waiting for document fully loaded';
                    setTimeout(isFullyLoaded, helpers.randomMs(15000, 25000));
                }
            };
            function runPromotion() {
                navigationProcess = NavigationProcess.PROCESSING_PROMOTION
                displayStatusUi();
                setTimeout(findPromotionTag, helpers.randomMs(1000, 3000));
            };
            function tryClosePopup() {
                let popupBtn = document.querySelector('.popup-close');
                if (popupBtn && popupBtn.isVisible()) {
                    popupBtn.click();
                }
            };
            function isRollResultVisible() {
                let rollDiv = document.querySelector('.result');
                if (rollDiv && rollDiv.isVisible() && rollDiv.innerText != '') {
                }
            };
            let waitRollNumberCount = 0;
            async function waitForRollNumber() {
                let newNumber = -1;
                try { // intento leer el rolled number
                    newNumber = [...document.querySelectorAll('.lucky-number')].map(x => x.innerText).join('');
                    newNumber = parseInt(newNumber)
                } catch(err) {
                    newNumber = null;
                }
                if (newNumber === null) { // si no logro leerlo, bajo 1 en tempRollNumber
                    if (tempRollNumber < 0) {
                        tempRollNumber -= 1;
                    } else {
                        tempRollNumber = -1;
                    }
                    if (tempRollNumber < -5) {
                        processRunDetails();
                        return;
                    } else {
                        await wait(3000);
                        return waitForRollNumber();
                    }
                }

                if (newNumber == tempRollNumber) {
                    timeWaiting = 0;
                    if (shared.getConfig()['cf.rollOnce']) {
                        processRunDetails();
                        return;
                    } else {
                        setTimeout(findCountdownOrRollButton, helpers.randomMs(1000, 2000));
                        return;
                    }
                } else {
                    waitRollNumberCount++;
                    if (waitRollNumberCount > 15) {
                        setTimeout(() => { location.reload(); }, 5000);
                        return;
                    }

                    tempRollNumber = newNumber;
                    await wait(3000);
                    return waitForRollNumber();
                }

            };
            function findCountdownOrRollButton() {
                if( isCountdownVisible() && !isRollButtonVisible() ) {
                    timeWaiting = 0;
                    processRunDetails();
                } else if ( !isCountdownVisible() && isRollButtonVisible() ) {
                    timeWaiting = 0;
                    setTimeout(isFullyLoaded, helpers.randomMs(1000, 5000));
                } else if ( isCountdownVisible() && isRollButtonVisible() ) {
                    try {
                        let minLeft = document.querySelector('.minutes .digits').innerText;
                        if (minLeft < 1) {
                            timeWaiting = 0;
                            setTimeout(isFullyLoaded, helpers.randomMs(1000, 5000));
                        }
                    } catch (err) { shared.devlog(`Error on alt logic of CF roll: ${err}`); }
                } else {
                    if (timeWaiting/1000 > shared.getConfig()['defaults.timeout'] * 60) {
                        shared.closeWithError(K.ErrorType.TIMEOUT, '');
                        return;
                    }

                    timeWaiting += 3000;
                    setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
                }
            };
            function findLoginForm() {
                if ( document.querySelector('div.login-wrapper').isVisible() ) {
                    let errElement = document.querySelector('.login-wrapper .error');
                    if( errElement && errElement.innerHTML != '') {
                        let errorMessage = errElement.innerText;
                        shared.closeWithError(K.ErrorType.LOGIN_ERROR, errorMessage);
                        return;
                    }
                    if(!loopingForErrors) {
                        if(shared.getConfig()['cf.credentials.mode'] == 1) {
                            timeWaiting = 0;
                            document.querySelector('.login-wrapper input[name="email"]').value = shared.getConfig()['cf.credentials.email'];
                            document.querySelector('.login-wrapper input[name="password"]').value = shared.getConfig()['cf.credentials.password'];
                            document.querySelector('.login-wrapper button.login').click();
                            loopingForErrors = true;
                        } else {
                            if(document.querySelector('.login-wrapper input[name="email"]').value != '' && document.querySelector('.login-wrapper input[name="password"]').value != '') {
                                document.querySelector('.login-wrapper button.login').click();
                                document.getElementById('process-status').innerHTML = 'Processing';
                                loopingForErrors = true;
                            } else {
                                document.getElementById('process-status').innerHTML = 'Waiting for credentials...';
                                if (timeWaiting/1000 > (shared.getConfig()['defaults.timeout'] / 1.5) * 60) {
                                    shared.closeWithError(K.ErrorType.LOGIN_ERROR, 'No credentials were provided');
                                    return;
                                }
                            }
                        }
                    }
                }

                if (timeWaiting/1000 > shared.getConfig()['defaults.timeout'] * 60) {
                    shared.closeWithError(K.ErrorType.TIMEOUT, '');
                    return;
                }

                timeWaiting += 3000;
                setTimeout(findLoginForm, helpers.randomMs(2000, 5000));
            };
            function interact() {
                let selectables = [].concat([...document.querySelectorAll('td')], [...document.querySelectorAll('p')], [...document.querySelectorAll('th')]);

                interactions.start(selectables);
                setTimeout(waitInteractions, helpers.randomMs(2000, 4000));
            }
            function waitInteractions() {
                if(interactions.completed()) {
                    roll();
                } else {
                    setTimeout(waitInteractions, helpers.randomMs(2000, 4000));
                }
            }
            function isCountdownVisible() {
                countdown = document.querySelectorAll('.timeout-wrapper');
                return (countdown.length > 0 && countdown[0].isVisible());
            };
            function isRollButtonVisible() {
                rollButton = document.querySelectorAll('.main-button-2.roll-button.bg-2');
                return (rollButton.length > 0 && rollButton[0].isVisible());
            };
            function roll() {
                document.getElementById('process-status').innerHTML = 'Roll triggered';
                rollButton[0].click();
                tempRollNumber = -1;
                setTimeout(waitForRollNumber, helpers.randomMs(4000, 7000));
            }
            function isPromotionTagVisible() {
                let pTag;
                try {
                    pTag = document.querySelectorAll('div.header-wrapper')[0];
                } catch(err) {
                    return false;
                }
                if (pTag) {
                    promotionTag = pTag;
                    return true;
                }
                return false;
            };
            function findPromotionTag() {
                if( isPromotionTagVisible() ) {
                    processRunDetails();
                } else {
                    setTimeout(findPromotionTag, helpers.randomMs(2000, 5000));
                }
            };
            function processRunDetails() {
                let result = {};
                if(navigationProcess == NavigationProcess.ROLLING) {
                    result.claimed = readClaimed();
                    result.balance = readBalance();
                    if(result.claimed != 0) {
                        result.rolledNumber = readRolledNumber();
                    }
                    let minOneHour = result.rolledNumber && result.rolledNumber != 0;
                    result.nextRoll = readCountdown(minOneHour);
                    result.balance = readBalance();
                } else if (navigationProcess == NavigationProcess.PROCESSING_PROMOTION) {
                    result = shared.getResult() || {};
                    if (!result.promoCodeResults) {
                        result.promoCodeResults = [];
                    }
                    let pc = {
                        promoCode: readPromoCode(),
                        promoStatus: readPromoStatus()
                    };

                    result.promoCodeResults.push(pc);
                    shared.updateWithoutClosing(result, 'WORKING');
                    setTimeout(gotoNextPromoCode, helpers.randomMs(1000, 2500));
                    return;
                }
                shared.closeWindow(result);
            };
            function gotoNextPromoCode() {
                let codes = shared.getCurrent().params.promoCodes;
                if (!codes) {
                    shared.closeWindow();
                    return;
                }
                let pc = readPromoCode();
                let pcIdx = codes.findIndex(x => x == pc);
                if (pcIdx == -1 || pcIdx == codes.length - 1) {
                    shared.closeWindow();
                    return;
                }
                window.location.href = '/promotion/' + codes[pcIdx + 1];
            };
            function readCountdown(minOneHour = false) {
                let minsElement = document.querySelector('.timeout-container .minutes .digits');
                let mins = "0";
                if (minsElement) {
                    mins = minsElement.innerHTML;
                }
                if (mins) {
                    let estimated = helpers.addMinutes(+mins + 1);
                    let oneHour = Date.now() + (60*60*1000);
                    if (minOneHour && (oneHour > estimated) ) {
                        return oneHour;
                    }
                    return estimated;
                } else {
                    return null;
                }
            };
            function readClaimed() {
                let claimed = 0;
                try {
                    claimed = document.querySelector('.result').innerHTML;
                    claimed = claimed.trim();
                    claimed = claimed.slice(claimed.lastIndexOf(" ") + 1);
                } catch(err) { }
                return claimed;
            };
            function readRolledNumber() {
                let number = 0;
                try {
                    number = [...document.querySelectorAll('.lucky-number')].map(x => x.innerText).join('');
                    number = parseInt(number);
                } catch(err) { }
                return number;
            };
            function readBalance() {
                let balance = "";
                try {
                    balance = document.querySelector('.navbar-coins.bg-1 a').innerText;
                } catch(err) { }
                return balance;
            };
            function readPromoStatus() {
                let promoStatus = K.CF.PromoStatus.UNKNOWNERROR;
                try {
                    if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeAccepted) > 0) {
                        return K.CF.PromoStatus.ACCEPTED;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeUsed) > 0) {
                        return K.CF.PromoStatus.USEDBEFORE;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeExpired) > 0) {
                        return K.CF.PromoStatus.EXPIRED;
                    } else if(localeConfig.stringSearches.promoCodeInvalid.findIndex(x => promotionTag.innerHTML.indexOf(x) > -1) == -1) {
                        return K.CF.PromoStatus.INVALID;
                    }
                } catch ( err ) { }
                return promoStatus;
            };
            function validatePromoString() {

            };
            function readPromoCode() {
                var urlSplit = window.location.href.split('/');
                return urlSplit[urlSplit.length - 1];
            };
            function displayStatusUi() {
                let wrapper = document.createElement('div');
                wrapper.innerHTML = '<div class="withdraw-button bg-2" style="top:30%; z-index:1500;" href="#">⚙️ <span id="process-status">Processing</span></div>';
                document.querySelector( 'body' ).prepend( wrapper.firstChild );
            };
            return {
                init: init
            };
        },
        createCFHistory: function() {
            let rollsMeta = [
                { id: 0, range: '0000-9885', count: 0 },
                { id: 1, range: '9886-9985', count: 0 },
                { id: 2, range: '9986-9993', count: 0 },
                { id: 3, range: '9994-9997', count: 0 },
                { id: 4, range: '9998-9999', count: 0 },
                { id: 5, range: '10000', count: 0 }
            ];

            function initOrLoad() {
                let storedData = persistence.load('CFHistory', true);
                if(storedData) {
                    rollsMeta = storedData;
                }
            };

            function addRoll(number) {
                switch(true) {
                    case (number <= 9885):
                        rollsMeta[0].count++;
                        break;
                    case (number <= 9985):
                        rollsMeta[1].count++;
                        break;
                    case (number <= 9993):
                        rollsMeta[2].count++;
                        break;
                    case (number <= 9997):
                        rollsMeta[3].count++;
                        break;
                    case (number <= 9999):
                        rollsMeta[4].count++;
                        break;
                    case (number == 10000):
                        rollsMeta[5].count++;
                        break;
                    default:
                        break;
                }
                save();
            };

            function getRollsMeta() {
                return rollsMeta.map(x => x.count);
            };

            function save() {
                persistence.save('CFHistory', rollsMeta, true);
            };

            return {
                initOrLoad: initOrLoad,
                addRoll: addRoll,
                getRollsMeta: getRollsMeta
            }
        },
        createFBProcessor: function() {
            let countdownMinutes;
            let timeout = new Timeout(this.maxSeconds);
            let captcha = new HCaptchaWidget();

            function run() {
                setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
            };
            function findCountdownOrRollButton() {
                if ( isCountdownVisible() ) {
                    timeout.restart();
                    countdownMinutes = +document.querySelectorAll('.free_play_time_remaining.hasCountdown .countdown_amount')[0].innerHTML + 1;
                    let result = {};
                    result.balance = readBalance();
                    result.nextRoll = helpers.addMinutes(countdownMinutes.toString());

                    shared.closeWindow(result);
                    return;
                }

                if ( isRollButtonVisible() ) {

                    try {
                        let doBonus = false; // true;
                        if (doBonus) {
                            if (!document.getElementById('bonus_span_free_wof')) {
                                RedeemRPProduct('free_wof_5');
                                setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
                                return;
                            }
                        }
                    } catch { }

                    /* For 'Play without captcha accounts' */
                    if (!captcha.isUserFriendly) {
                        clickRoll()
                    } else {
                        captcha.isSolved().then(() => { clickRoll(); });
                    }
                } else {
                    setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
                }
            };
            function isCountdownVisible() {
                return document.querySelectorAll('.free_play_time_remaining.hasCountdown .countdown_amount').length > 0;
            };
            function isHCaptchaVisible() {
                let hCaptchaFrame = document.querySelector('.h-captcha > iframe');
                if (hCaptchaFrame && hCaptchaFrame.isVisible()) {
                    return true;
                }
                return false;
            };
            function isRollButtonVisible() {
                return document.getElementById('free_play_form_button').isVisible();
            };
            function clickRoll() {
                try {
                    document.getElementById('free_play_form_button').click();
                    setTimeout(processRunDetails, helpers.randomMs(3000, 10000));
                } catch (err) {
                    shared.closeWithError(K.ErrorType.CLICK_ROLL_ERROR, err);
                }
            };
            function processRunDetails() {
                if (document.getElementById('winnings').isVisible()) {
                    closePopup();

                    let result = {};
                    result.claimed = readClaimed();
                    result.balance = readBalance();
                    if(result.claimed != 0) {
                        result.rolledNumber = readRolledNumber();
                    }
                    shared.closeWindow(result);
                    return;
                }

                if (document.querySelector('.free_play_result_error').isVisible()) {
                    shared.closeWithError(K.ErrorType.ROLL_ERROR, document.querySelector('.free_play_result_error').innerHTML);
                    return;
                }

                if(document.getElementById('free_play_error').isVisible()) {
                    shared.closeWithError(K.ErrorType.ROLL_ERROR, document.querySelector('.free_play_error').innerHTML);
                    return;
                }

                if (document.getElementById('same_ip_error').isVisible()) {
                    shared.closeWithError(K.ErrorType.ROLL_ERROR, document.getElementById('same_ip_error').innerHTML);
                    return;
                }

                setTimeout(processRunDetails, helpers.randomMs(5000, 6000));
            };
            function closePopup() {
                let closePopupBtn = document.querySelector('.reveal-modal.open .close-reveal-modal');
                if (closePopupBtn) {
                    closePopupBtn.click();
                }
            };
            function readRolledNumber() {
                let rolled = 0;
                try {
                    rolled = parseInt([... document.querySelectorAll('#free_play_digits span')].map( x => x.innerHTML).join(''));
                } catch { }
                return rolled;
            };
            function readBalance() {
                let balance = 0;
                try {
                    balance = document.getElementById('balance').innerHTML;
                } catch { }
                return balance;
            };
            function readClaimed() {
                let claimed = 0;
                try {
                    claimed = document.getElementById('winnings').innerHTML;
                } catch { }
                return claimed;
            };

            return {
                run: run
            };
        },
        createFPProcessor: function() {
            let timeout = new Timeout(this.maxSeconds);
            let captcha = new HCaptchaWidget();

            function init() {
                if(window.location.href.includes('ptc/view')) {
                    addDuration();
                    ptcSingle();
                } else if (window.location.href.includes('ptc')) {
                    ptcList();
                } else if (window.location.href.includes('account/login')) {
                    tryLogin();
                } else if (window.location.href.includes('page/user-admin')) {
                    window.location.href = 'https://faucetpay.io/ptc';
                }
                return;
            }

            function tryLogin() {
                let username = document.querySelector('input[name="user_name"');
                let password = document.querySelector('input[name="password"');
                let captcha = document.querySelector('.h-captcha > iframe');
                let btn = document.querySelector('button[type="submit"');
                if (username && password && btn && username.value != '' && password.value != '') {
                    if ( captcha && captcha.getAttribute('data-hcaptcha-response').length > 0 ) {
                        btn.click();
                    } else {
                        setTimeout(tryLogin, helpers.randomMs(9000, 11000));
                    }
                } else {
                    shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, '');
                }
            }

            function addDuration() {
                let duration = document.querySelector('#duration');
                if(duration && !isNaN(duration.innerText)) {
                    timeout.restart(parseInt(duration.innerText));
                } else {
                    setTimeout(addDuration, 10000);
                }
            }

            function ptcList() {
                let result;
                let runMsgDiv = document.querySelector('.alert.alert-info');
                if (runMsgDiv) {
                    let runMsg = runMsgDiv.innerHTML;
                    if (runMsg.includes('invalid captcha')) {
                    } else if (runMsg.includes('Good job')) {
                        try {
                            let idx = runMsg.search(/\d/);
                            let claimed = parseFloat(runMsg.slice(idx, idx + 10));
                            result = shared.getResult();
                            result.claimed = (result.claimed ?? 0) + claimed;
                            shared.updateWithoutClosing(result, 'WORKING');
                        } catch { }
                    }
                }

                if ([...document.querySelectorAll('b')].filter(x => x.innerText.includes('Whoops!')).length > 0) {
                    result = shared.getResult();
                    shared.closeWindow(result);
                    return;
                }

                let adButtons = [...document.querySelectorAll('button')].filter(x => x .innerHTML.includes('VISIT AD FOR'));

                if (adButtons.length > 0) {
                    if (shared.getConfig()['fp.randomPtcOrder']) {
                    adButtons[helpers.randomInt(0, adButtons.length-1)].click();
                    } else {
                        adButtons[0].click();
                    }
                    return;
                }

                setTimeout(ptcList, helpers.randomMs(10000, 12000));
            }

            function ptcSingle() {
                if(document.querySelector('input[name="complete"]').isVisible()) {
                    captcha.isSolved().then(() => { clickClaim(); });
                } else if (document.querySelector('body').innerText.toLowerCase().includes('ad does not exist')) {
                    window.location.href = 'https://faucetpay.io/ptc';
                } else {
                    setTimeout(ptcSingle, helpers.randomMs(5000, 6000));
                }
            }

            function clickClaim() {
                let input = document.querySelector('input[name="complete"]');
                input.focus();
                input.onclick = '';
                input.click();
                setTimeout(shared.closeWithError.bind(null, 'TIMEOUT', 'Timed out after clicking a CLAIM button.'), helpers.minToMs(shared.getConfig()['defaults.timeout']));
            }

            return {
                init: init
            };
        },
        createBigBtcProcessor: function() {
            let timeout = new Timeout(this.maxSeconds);
            let countdownMinutes;
            let captcha = new HCaptchaWidget();
            let selectElement = {
                loadingDiv: function() {
                    let loading = document.querySelector('#loading');
                    if (loading && loading.isVisible()) {
                        return true;
                    } else {
                        return false;
                    }
                },
                addressInput: function() {
                    return document.querySelector('#login input[name="address"]');
                },
                loginButton: function() {
                    return document.querySelector('#login input[type="submit"]');
                },
                claimButton: function() {
                    return document.getElementById('claimbutn');
                },
                countdown: function() { // "You have to wait\n60 minutes"
                    let cd = document.getElementById('countdown');
                    if(cd && cd.isVisible()) {
                        return parseInt(cd.innerText);
                    }
                    return null;
                },
                claimedAmount: function() {
                    let elm = document.querySelector('.alert.alert-success.pulse'); //"Yuppie! You won 2 satoshi!"
                    if(elm && elm.isVisible()) {
                        let val = parseInt(elm.innerText.replace(/\D/g, ''));
                        if (Number.isInteger(val)) {
                            val = val / 100000000;
                        }

                        return val;
                    } else {
                        return null;
                    }
                },
                balance: function() {
                    let elm = document.querySelector('a b');
                    if (elm && elm.isVisible()) {
                        let val = parseInt(elm.innerText.replace(',', ''));
                        if (Number.isInteger(val)) {
                            val = val / 100000000;
                        }

                        return val;
                    } else {
                        return null;
                    }
                },
                error: function () {
                    return null;
                }
            };

            function init() {
                window.scrollTo(0, document.body.scrollHeight);
                let m = document.getElementById('main'); if (m) { m.style.display='block'; }
                m = document.getElementById('block-adb-enabled'); if (m) { m.style.display='none'; }
                m = document.getElementById('ielement'); if (m) { m.style.display='block'; }
                setInterval(() => {
                    let frames = [...document.querySelectorAll('iframe')];
                    frames.forEach(x => {
                        if (!x.src.includes('hcaptcha')) {
                            x.remove()
                        }
                    });
                }, 5000);

                if (window.location.href.includes('/faucet')) {
                    setTimeout(runFaucet, helpers.randomMs(12000, 14000));
                    return;
                } else {
                    setTimeout(run, helpers.randomMs(3000, 5000));
                    return;
                }
            }

            function run() {
                try {
                    setTimeout(waitIfLoading, helpers.randomMs(12000, 15000));
                } catch (err) {
                    shared.closeWithErrors(K.ErrorType.ERROR, err);
                }
            };
            function doLogin() {
                let address = selectElement.addressInput();
                if(address && address.value != shared.getCurrent().params.address) {
                    address.value = shared.getCurrent().params.address;
                } else {
                    selectElement.loginButton().click();
                    return;
                }
                setTimeout( doLogin , helpers.randomMs(1000, 2000));
            };
            function waitIfLoading() {
                if ( !selectElement.loadingDiv() ) {
                    doLogin();
                    return;
                } else {
                }

                setTimeout(waitIfLoading, helpers.randomMs(5000, 7000));
            };
            function runFaucet() {
                let claimedAmount = selectElement.claimedAmount();
                if(claimedAmount) {
                    processRunDetails();
                    return;
                } else if (selectElement.countdown()) {
                    let result = {};

                    shared.closeWindow(result);
                } else {
                    captcha.isSolved().then(() => { clickClaim(); });
                }
            }
            function clickClaim() {
                try {
                    selectElement.claimButton().click();
                    return;
                } catch (err) {
                    shared.closeWithError(K.ErrorType.CLICK_ROLL_ERROR, err);
                }
            };
            function processRunDetails() {
                let claimedAmount = selectElement.claimedAmount();
                let balance = selectElement.balance();
                let countdown = selectElement.countdown();

                if (claimedAmount && balance) {
                    let result = {};
                    result.claimed = claimedAmount;
                    result.balance = balance;

                    shared.closeWindow(result);
                    return;
                }

                setTimeout(processRunDetails, helpers.randomMs(5000, 6000));
            };

            return {
                init: init
            };
        },
        createBestChangeProcessor: function() {
            let timeout = new Timeout(this.maxSeconds);
            let countdownMinutes;
            let captcha = new HCaptchaWidget({selector: '.hcaptcha > iframe'});
            let elements = {
                captcha: function() {
                    return document.querySelector('.hcaptcha > iframe');
                },
                container: function() {
                    return document.querySelector('#info_bonus');
                },
                containerOpener: function() {
                    return document.querySelector('#tab_bonus a');
                },
                addressInput: function() {
                    return document.querySelector('#bonus_purse');
                },
                claimButton: function() {
                    return document.querySelector('#bonus_button');
                },
                countdown: function() { // Time left: mm:ss
                    let elm = document.querySelector('#bonus_button');
                    try {
                        if (elm.value) {
                            let timeLeft = elm.value.split(':');
                            if (timeLeft.length > 1) {
                                return parseInt(timeLeft[1]);
                            }
                        }
                    } catch (err) {
                        return null;
                    }
                },
                claimedAmount: function() {
                    let elm = document.querySelector("#bonus_status b");
                    try {
                        let sats = elm.innerText.replace(/\D/g, '');
                        return sats / 100000000;
                    } catch (err) {
                        return null;
                    }
                },
                balance: function() {
                    let elm = document.querySelector("#faucet_unpaid_balance b");
                    try {
                        let sats = elm.innerText.replace(/\D/g, '');
                        return sats / 100000000;
                    } catch (err) {
                        return null;
                    }
                }
            };

            function init() {
                run();
            }

            function run() {
                try {
                    if (!elements.container().isUserFriendly()) {
                        let co = elements.containerOpener();
                        if(co.isUserFriendly()) {
                            co.onclick = co.onmousedown;
                            co.click();
                        }
                    }
                    setTimeout(findCountdownOrRoll, helpers.randomMs(4000, 5000));
                } catch (err) {
                    shared.closeWithErrors(K.ErrorType.ERROR, err);
                }
            };
            function findCountdownOrRoll() {
                let countdown = elements.countdown();
                if(countdown) {
                    let result = { };
                    result.nextRoll = helpers.addMinutes(countdown.toString());

                    shared.closeWindow(result);
                    return;
                }

                let ai = elements.addressInput();

                if (ai.isUserFriendly()) {
                    if (ai.value != shared.getCurrent().params.address) {
                        ai.value = shared.getCurrent().params.address;
                    }
                    captcha.isSolved().then(() => { clickClaim(); });
                    return;
                }

                setTimeout(findCountdownOrRoll, helpers.randomMs(10000, 12000));
            };

            function clickClaim() {
                try {
                    let btn = elements.claimButton();
                    if(btn.isUserFriendly()) {
                        btn.click();
                        setTimeout(processRunDetails, helpers.randomMs(4000, 8000));
                    } else {
                        setTimeout(clickClaim, helpers.randomMs(4000, 8000));
                    }
                    return;
                } catch (err) {
                    shared.closeWithError(K.ErrorType.CLICK_ROLL_ERROR, err);
                }
            };

            function processRunDetails() {
                let claimedAmount = elements.claimedAmount();
                let balance = elements.balance();

                if (claimedAmount && balance) {
                    let result = {};
                    result.claimed = claimedAmount;
                    result.balance = balance;

                    shared.closeWindow(result);
                    return;
                }

                setTimeout(processRunDetails, helpers.randomMs(5000, 6000));
            };

            return {
                init: init
            };
        },
    };

    function overrideSelectNativeJS_Functions () {
        window.alert = function alert (message) {
        }
    }
    function addJS_Node (text, s_URL, funcToRun) {
        var scriptNode= document.createElement ('script');
        scriptNode.type= "text/javascript";
        if (text)scriptNode.textContent= text;
        if (s_URL)scriptNode.src= s_URL;
        if (funcToRun)scriptNode.textContent = '(' + funcToRun.toString() + ')()';
        var element = document.getElementsByTagName ('head')[0] || document.body || document.documentElement;
        element.appendChild (scriptNode);
    }

    function detectWeb() {
        if(!shared.isOpenedByManager()) {
            return;
        }
        instance = K.LOCATION.SITE;

        let typeFromManager = shared.getCurrent().type;

        siteTimer = new Timer({ isManager: false, delaySeconds: 20, uuid: shared.getProp('schedule'), webType: typeFromManager });
        switch( typeFromManager ) {
            case K.WebType.STORMGAIN:
                SiteProcessor = objectGenerator.createSGProcessor();
                setTimeout(SiteProcessor.run, helpers.randomMs(10000, 20000));
                break;
            case K.WebType.CRYPTOSFAUCETS:
                SiteProcessor = objectGenerator.createCFProcessor();
                setTimeout(SiteProcessor.init, helpers.randomMs(1000, 3000));
                break;
            case K.WebType.FREEBITCOIN:
                SiteProcessor = objectGenerator.createFBProcessor();
                setTimeout(SiteProcessor.run, helpers.randomMs(2000, 5000));
                break;
            case K.WebType.FAUCETPAY:
                SiteProcessor = objectGenerator.createFPProcessor();
                setTimeout(SiteProcessor.init, helpers.randomMs(2000, 5000));
                break;
            case K.WebType.BIGBTC:
                SiteProcessor = objectGenerator.createBigBtcProcessor();
                setTimeout(SiteProcessor.init, helpers.randomMs(2000, 4000));
                break;
            case K.WebType.BESTCHANGE:
                SiteProcessor = objectGenerator.createBestChangeProcessor();
                setTimeout(SiteProcessor.init, helpers.randomMs(4000, 6000));
                break;
            case K.WebType.BFBOX:
                SiteProcessor = new BFRoll(helpers.getEnumText(K.CMC, shared.getCurrent().params.cmc).toLowerCase());
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(2000, 5000));
                break;
            case K.WebType.DUTCHYROLL:
                SiteProcessor = new DutchyRoll();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(2000, 5000));
                break;
            case K.WebType.FCRYPTO:
                SiteProcessor = new FCryptoRoll();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(2000, 5000));
                break;
            case K.WebType.FPB:
                SiteProcessor = new FPB(shared.getCurrent().params.sitePrefix);
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.FREEGRC:
                SiteProcessor = new GRCRoll();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.VIE:
                SiteProcessor = new VieRoll();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.O24:
                SiteProcessor = new O24Roll();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.YCOIN:
                SiteProcessor = new YCoin();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.CDIVERSITY:
                SiteProcessor = new CDiversity();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.BSCADS:
                SiteProcessor = new BscAds();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            case K.WebType.CTOP:
                SiteProcessor = new CTop();
                setTimeout(() => { SiteProcessor.init() }, helpers.randomMs(3000, 5000));
                break;
            default:
                break;
        }
    }

    class UiBaseRenderer {
    constructor(uiRenderer) { this.uiRenderer = uiRenderer; }
}
class UiSitesRenderer extends UiBaseRenderer {
    appendEventListeners() {

        document.querySelector('#modal-assign-schedule').addEventListener('click', this.onClickOnModalAssignSchedule.bind(this));
        eventer.on('siteChangedSchedule', (e) => {
            this.uiRenderer.toast(`Site moved to schedule ${e.scheduleId}`);
            manager.resyncAll({withUpdate: true}); // should act based on data only
        });

        document.querySelector('#schedule-table-body').addEventListener('click', this.onClickOnSitesTableBody.bind(this));

        document.querySelector('.action-edit-all-sites').addEventListener('click', this.onClickOnEditAllSites.bind(this));
        document.querySelector('.action-edit-all-sites-cancel').addEventListener('click', this.onClickOnCancelEditAllSites.bind(this));
        document.querySelector('.action-edit-all-sites-save').addEventListener('click', this.onClickOnSaveEditAllSites.bind(this));

        document.querySelector('.action-add-external-site').addEventListener('click', this.onClickOnAddSiteButton.bind(this));
        document.querySelector('#modal-add-site').addEventListener('click', this.onClickOnModalAddSite.bind(this));
        eventer.on('siteAdded', (e) => {
            this.uiRenderer.toast(`Site ${e.siteName} added`);
            manager.resyncAll({withUpdate: true}); // should act based on data only
        });
        eventer.on('siteRemoved', (e) => {
            this.uiRenderer.toast(`Site ${e.siteName} removed`);
            manager.resyncAll({withUpdate: true}); // should act based on data only
        });
    }

    _legacyAddBadges(stats) {
        let consecutiveTimeout = stats.countTimeouts;
        let otherErrors = stats.errors;
        let html = ' ';

        if (consecutiveTimeout) {
            html += `<span class="badge badge-pill badge-warning" title="${consecutiveTimeout} consecutive timeouts">${consecutiveTimeout}</span>`;
        }

        if (otherErrors) {
            html += `<span class="badge badge-pill badge-warning" title="${otherErrors.errorMessage}">${helpers.getEnumText(K.ErrorType, otherErrors.errorType)}</span>`;
        }
        return html;
    }

    removeDeletedSitesRows(validSiteIds) {
        let removableRows = [...document.querySelectorAll('#schedule-table-body tr')].filter(r => !validSiteIds.includes(r.dataset.id));
        removableRows.forEach(r => {
            r.remove();
        });
    }

    renderSiteRow(site) {

        let row = [...document.querySelectorAll('#schedule-table-body tr')]
                    .filter(r => r.dataset.id == site.id);

        if (row.length == 0) {
            row = document.createElement('tr');
            document.querySelector('#schedule-table-body').appendChild(row);
            row.setAttribute('aria-expanded', false);
            row.classList.add('align-middle');
            row.dataset.id = site.id;
            row.dataset.cmc = site.cmc;
        } else {
            row = row[0];
        }

        row.dataset.json = `${JSON.stringify(site)}`;
        row.dataset.schedule = site.schedule;
        row.dataset.nextRollTimestamp = site.nextRoll ? site.nextRoll.getTime() : 'null';
        row.dataset.enabled = site.enabled ? '1' : '0';
        if (site.balance) {
            if (typeof site.balance == 'string') {
                row.dataset.balance = site.balance.split(' ')[0];
            } else {
                row.dataset.balance = site.balance.toFixed(8);
            }
        } else {
            row.dataset.balance = '';
        }

        let tds = '';

        tds += '<td class="align-middle edit-status d-none em-only"><label class="switch"><input type="checkbox" data-original="' + (site.enabled ? '1' : '0') + '" ' + (site.enabled ? 'checked' : ' ') + '><span class="slider round"></span></label></td>';
        tds += '<td class="align-middle" title="' + helpers.getPrintableDateTime(site.nextRoll) + '"><span><i class="fas fa-square pr-1" style="color: #' + site.schedule + ';"></i></span>' + helpers.getTdPrintableTime(site.nextRoll) + '</td>';
        if (site.isExternal && site.clId == -1) {
            tds += '<td class="align-middle text-left"><a class="" title="Visit site" target="_blank" rel="noreferrer" href="' + site.url + '"><i class="fa fa-external-link-alt"></i></a></td>';
        } else {
            tds += '<td class="align-middle text-left"><a class="" title="Visit site" target="_blank" rel="noreferrer" href="' + (new URL(site.clId, 'https://criptologico.com/goto/')).href + '"><i class="fa fa-external-link-alt"></i></a></td>';
        }

        tds += '<td class="align-middle em-input text-left" data-field="displayName">';
        if (site.cmc) {
            tds +='<div class="input-group input-group-sm">';
            tds += '<div class="input-group-prepend"><span class="input-group-text">';
            if (site.cmc > 0) {
                let cmcLower = helpers.getEnumText(K.CMC, site.cmc).toLowerCase();
                tds += '<img loading="lazy" src="/static/c-icons/' + cmcLower + '.svg" height="20" alt="' + cmcLower + '">';
            } else {
                tds += '<i class="fa fa-question-circle"></i>';
            }
            tds += '</span></div>';
        }
        tds += ' <span class="site-name-container px-1">' + site.name + '</span></div></td>';

        tds +='<td class="align-middle text-right">' + site.lastClaim.toFixed(Number.isInteger(site.lastClaim) ? 0 : 8) + '</td>';
        tds +='<td class="align-middle text-right">' + site.aggregate.toFixed(Number.isInteger(site.aggregate) ? 0 : 8) + '</td>';

        tds += '<td class="align-middle text-right">' + (+row.dataset.balance > 100 ? (+row.dataset.balance).toFixed(2) : row.dataset.balance)  + '</td>';
        tds +='<td class="align-middle text-right fiat-conversion em-hide"></td>';
        tds +='<td class="align-middle">' + this._legacyAddBadges(site.stats) + '</td>';
        tds +='<td class="align-middle justify-content-center em-hide">';

        tds += 
        `<div class="btn-group btn-group-sm">
            <button type="button" title="Run ASAP" class="btn btn-default action-run-asap">
                <i class="fa fa-bolt"></i>
            </button>
            <button type="button" title="Schedule parameters..." 
                class="btn btn-default action-edit-site ${Object.keys(site.params).some( k => k.endsWith('.override') && site.params[k] == true ) ? 'text-warning' : ''}">
                <i class="fa fa-clock"></i>
            </button>
            <div class="btn-group btn-group-sm">
                <button type="button" class="btn btn-default dropdown-toggle dropdown-icon" data-toggle="dropdown" aria-expanded="false">
                </button>
                <div class="dropdown-menu dropdown-menu-right text-sm" style="">
                    <!-- <a class="dropdown-item action-site-edit-parameters"><i class="fa fa-edit"></i> Site parameters...</a> -->
                    <a class="dropdown-item action-site-assign-schedule"><i class="fa fa-exchange-alt"></i> Move to...</a>`;
        if (site.isExternal) {
            tds += `<a class="dropdown-item action-site-remove-external"><i class="fa fa-trash"></i> Remove site</a>`;
        }
        tds += `</div></div></div>`;

        tds +='</td></tr>';

        row.innerHTML = tds;
    }

    legacyRenderSiteData(site, config) {
        document.querySelector('#faucet-name').innerHTML = site.name;
        document.querySelector('#faucet-name').dataset.id = site.id;
        let data = site.params || {};

        for (const prop in config) {
            let overrideElement = document.querySelector('[data-site-prop="' + prop + '.override"]');
            if (overrideElement) {
                overrideElement.dataset.original = (data[prop + '.override'] ? "1" : "0");
                overrideElement.checked = data[prop + '.override'];
            }

            let element = document.querySelector('[data-site-prop="' + prop + '"]');
            if(element) {
                if(element.type == 'select-one' || element.type == 'text' || element.type == 'password' || element.type == 'number' || element.type == 'time') {
                    element.dataset.original = data[prop] ?? config[prop];
                    element.value = data[prop] ?? config[prop];
                } else if (element.type == 'checkbox') {
                    element.dataset.original = ((data[prop] ?? config[prop]) ? "1" : "0");
                    element.checked = data[prop] ?? config[prop];
                }
                element.disabled = true;
            }
        }

        let elWorkInBackgroundOverride = document.querySelector('[data-site-prop="defaults.workInBackground.override"]');
        let elWorkInBackground = document.querySelector('[data-site-prop="defaults.workInBackground"]');
        elWorkInBackground.disabled = !elWorkInBackgroundOverride.checked;
        elWorkInBackgroundOverride.onchange = function (e) {
            document.querySelector('[data-site-prop="defaults.workInBackground"]').disabled = !e.target.checked;
        }

        let elTimeoutOverride = document.querySelector('[data-site-prop="defaults.timeout.override"]');
        let elTimeout = document.querySelector('[data-site-prop="defaults.timeout"]');
        elTimeout.disabled = !elTimeoutOverride.checked;
        elTimeoutOverride.onchange = function (e) {
            document.querySelector('[data-site-prop="defaults.timeout"]').disabled = !e.target.checked;
        }

        let elPostponeOverride = document.querySelector('[data-site-prop="defaults.postponeMinutes.override"]');
        let elPostpone = document.querySelector('[data-site-prop="defaults.postponeMinutes"]');
        let elPostponeMin = document.querySelector('[data-site-prop="defaults.postponeMinutes.min"]');
        let elPostponeMax = document.querySelector('[data-site-prop="defaults.postponeMinutes.max"]');
        elPostpone.disabled = !elPostponeOverride.checked;
        elPostponeMin.disabled = !elPostponeOverride.checked || (elPostpone.value > "0");
        elPostponeMax.disabled = !elPostponeOverride.checked || (elPostpone.value > "0");
        elPostponeOverride.onchange = function (e) {
            let mode = document.querySelector('[data-site-prop="defaults.postponeMinutes"]');
            mode.disabled = !e.target.checked;
            document.querySelector('[data-site-prop="defaults.postponeMinutes.min"]').disabled = !e.target.checked || mode.value > 0;
            document.querySelector('[data-site-prop="defaults.postponeMinutes.max"]').disabled = !e.target.checked || mode.value > 0;
        }
        elPostpone.onchange = function (e) {
            document.querySelector('[data-site-prop="defaults.postponeMinutes.min"]').disabled = e.target.value > 0;
            document.querySelector('[data-site-prop="defaults.postponeMinutes.max"]').disabled = e.target.value > 0;
            if (e.target.value > 0) {
                document.querySelector('[data-site-prop="defaults.postponeMinutes.min"]').value = e.target.value;
                document.querySelector('[data-site-prop="defaults.postponeMinutes.max"]').value = e.target.value;
            }
        }

        let elNextRunOverride = document.querySelector('[data-site-prop="defaults.nextRun.override"]');
        let elNextRun = document.querySelector('[data-site-prop="defaults.nextRun"]');
        let elNextRunMin = document.querySelector('[data-site-prop="defaults.nextRun.min"]');
        let elNextRunMax = document.querySelector('[data-site-prop="defaults.nextRun.max"]');
        let elNextRunUseCountdown = document.querySelector('[data-site-prop="defaults.nextRun.useCountdown"]');
        elNextRun.disabled = !elNextRunOverride.checked;
        elNextRunMin.disabled = !elNextRunOverride.checked || (elNextRun.value > "0");
        elNextRunMax.disabled = !elNextRunOverride.checked || (elNextRun.value > "0");
        elNextRunUseCountdown.disabled = !elNextRunOverride.checked;
        elNextRunOverride.onchange = function (e) {
            let mode = document.querySelector('[data-site-prop="defaults.nextRun"]');
            mode.disabled = !e.target.checked;
            document.querySelector('[data-site-prop="defaults.nextRun.min"]').disabled = !e.target.checked || mode.value > 0;
            document.querySelector('[data-site-prop="defaults.nextRun.max"]').disabled = !e.target.checked || mode.value > 0;
            document.querySelector('[data-site-prop="defaults.nextRun.useCountdown"]').disabled = !e.target.checked;
        }
        elNextRun.onchange = function (e) {
            document.querySelector('[data-site-prop="defaults.nextRun.min"]').disabled = e.target.value > 0;
            document.querySelector('[data-site-prop="defaults.nextRun.max"]').disabled = e.target.value > 0;
            if (e.target.value > 0) {
                document.querySelector('[data-site-prop="defaults.nextRun.min"]').value = e.target.value;
                document.querySelector('[data-site-prop="defaults.nextRun.max"]').value = e.target.value;
            }
        }

        let elSleepOverride = document.querySelector('[data-site-prop="defaults.sleepMode.override"]');
        let elSleep = document.querySelector('[data-site-prop="defaults.sleepMode"]');
        let elSleepMin = document.querySelector('[data-site-prop="defaults.sleepMode.min"]');
        let elSleepMax = document.querySelector('[data-site-prop="defaults.sleepMode.max"]');
        elSleep.disabled = !elSleepOverride.checked;
        elSleepMin.disabled = !elSleepOverride.checked || !elSleep.checked;
        elSleepMax.disabled = !elSleepOverride.checked || !elSleep.checked;
        elSleepOverride.onchange = function (e) {
            let mode = document.querySelector('[data-site-prop="defaults.sleepMode"]');
            mode.disabled = !e.target.checked;
            document.querySelector('[data-site-prop="defaults.sleepMode.min"]').disabled = !e.target.checked || !mode.checked;
            document.querySelector('[data-site-prop="defaults.sleepMode.max"]').disabled = !e.target.checked || !mode.checked;
        }
        elSleep.onchange = function (e) {
            document.querySelector('[data-site-prop="defaults.sleepMode.min"]').disabled = !e.target.checked;
            document.querySelector('[data-site-prop="defaults.sleepMode.max"]').disabled = !e.target.checked;
        }

        return;
    }

    sortSitesTable() {
        const tbody = document.querySelector('#schedule-table-body');

        let rows, switching, i, shouldSwitch;
        switching = true;
        while (switching) {
            switching = false;
            rows = tbody.rows;

            for (i = 0; i < (rows.length - 1); i++) {
                shouldSwitch = false;

                let aNextRoll, bNextRoll, aHasLoginError, bHasLoginError, aName, bName;
                aNextRoll = rows[i].dataset.nextRollTimestamp;
                bNextRoll = rows[i + 1].dataset.nextRollTimestamp;
                if (aNextRoll == 'null' && bNextRoll == 'null') {
                    aName = rows[i].querySelector('.site-name-container').innerText;
                    bName = rows[i + 1].querySelector('.site-name-container').innerText;
                    if (aName.toLowerCase() > bName.toLowerCase()) {
                        shouldSwitch = true;
                        break;
                    }
                } else if (aNextRoll == 'null' || (aNextRoll > bNextRoll)) {
                    shouldSwitch = true;
                    break;
                }
            }
            if (shouldSwitch) {
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                switching = true;
            }
        }
    }

    onClickOnSitesTableBody(e) {
        let actionElement = e.target;
        if (actionElement.tagName === 'I') {
            actionElement = actionElement.parentElement;
        }
        const row = actionElement.closest('tr');
        if (actionElement.classList.contains('action-edit-site')) {
            e.stopPropagation();
            this.uiRenderer.openModal('modal-site', row.dataset.id);
        } else if (actionElement.classList.contains('action-run-asap')) {
            e.stopPropagation();
            manager.Site.setAsRunAsap(row.dataset.id);
        } else if (actionElement.classList.contains('action-site-assign-schedule')) {
            this.uiRenderer.openModal('modal-assign-schedule', { site_id: row.dataset.id, schedule_id: row.dataset.schedule });
        } else if (actionElement.classList.contains('action-site-edit-parameters')) {
            this.uiRenderer.openModal('modal-site-parameters', { site_id: row.dataset.id });
        } else if (actionElement.classList.contains('action-site-remove-external')) {
            manager.Site.remove(row.dataset.id);
            console.info('TODO: remove site and all the related configuration', row.dataset.id);
        }
    }

    onClickOnModalAssignSchedule(e) {
        const modalAssignScheduleToSite = document.querySelector('#modal-assign-schedule');
        let actionElement = e.target.tagName === 'I' ? e.target.parentElement : e.target;
        if (actionElement.classList.contains('modal-save')) {
            let data = this.uiRenderer.parseContainer(modalAssignScheduleToSite.querySelector('.form-container'));
            if (data.original_schedule_id == data.schedule) {
            } else {
                manager.Site.getById(data.site_id).changeSchedule(data.schedule);
            }
        }
    }

    onClickOnModalAddSite(e) {
        const modal = document.querySelector('#modal-add-site');
        let actionElement = e.target.tagName === 'I' ? e.target.parentElement : e.target;
        if (actionElement.classList.contains('modal-save')) {
            let formData = this.uiRenderer.parseContainer(modal.querySelector('.form-container'));
            let data = {};
            data.name = formData.site_name;
            data.url = new URL(formData.site_url);
            data.schedule = formData.schedule;
            data.clId = -1;
            data.id = 'ext_rnd_id_' + helpers.randomString(8);
            data.type = K.WebType.UNDEFINED;
            data.cmc = -1;
            data.rf = '';
            data.isExternal = true;

            console.warn('Savable new site');
            manager.Site.add(data);
            return;
            if (data.original_schedule_id == data.schedule) {
            } else {
                manager.Site.getById(data.site_id).changeSchedule(data.schedule);
            }
        }
    }

    onClickOnEditAllSites(e) {
        document.querySelectorAll("#schedule-table-body td.em-input .site-name-container").forEach(function (x) {
            let val = x.innerHTML;
            x.innerHTML = "<input type=\'text\' class=\'form-control form-control-sm\' data-original=\'" + val.trim() + "\' value=\'" + val.trim() + "\' />";
        });
        document.querySelectorAll("#schedule-table-body td.edit-status").forEach(function (x) {
            x.classList.remove("d-none");
        });
        document.querySelectorAll(".em-only").forEach(x => x.classList.remove("d-none"));
        document.querySelectorAll(".em-hide").forEach(x => x.classList.add("d-none"));
    }

    onClickOnCancelEditAllSites(e) {
        document.querySelectorAll("#schedule-table-body td.em-input .site-name-container input").forEach(function(x) {
            x.parentNode.innerHTML = x.dataset.original;
        });
        document.querySelectorAll(".em-only").forEach(x => x.classList.add("d-none"));
        document.querySelectorAll(".em-hide").forEach(x => x.classList.remove("d-none"));
    }

    onClickOnSaveEditAllSites(e) {
        let updateObject;
        var updateData = document.getElementById("update-data");
        if (updateData.innerHTML != "") {
            updateObject = JSON.parse(updateData.innerHTML);
        } else {
            updateObject = {
                runAsap: { ids: [], changed: false }, 
                editSingle: { changed: false, items: [] }, 
                wallet: { changed: false, items: [] }, 
                config: { changed: false, items: [] }, 
                site: { changed: false, list: [] }
            };
        }

        document.querySelectorAll("#schedule-table-body tr").forEach(function (row) {
            let textInputCell = row.querySelector(".em-input .site-name-container");
            let textInput = textInputCell.querySelector("input");
            let activeSwitch = row.querySelector("td.edit-status input");
            let single = { id: row.dataset.id, displayName: textInput.dataset.original, enabled: activeSwitch.dataset.original };
            textInputCell.innerHTML = textInput.value;
            if(textInput.dataset.original != textInput.value) {
                single.displayName = textInput.value;
            }
            if(activeSwitch.dataset.original != Boolean(activeSwitch.checked)) {
                single.enabled = Boolean(activeSwitch.checked);
            }
            if(textInput.dataset.original != textInput.value || activeSwitch.dataset.original != Boolean(activeSwitch.checked)) {
                updateObject.editSingle.items.push(single);
                updateObject.editSingle.changed = true;
            }
        });
        if(updateObject.editSingle.changed) {
            document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);
            this.uiRenderer.toast("Data will be updated as soon as possible");
        }

        document.querySelectorAll(".em-only").forEach(x => x.classList.add("d-none"));
        document.querySelectorAll(".em-hide").forEach(x => x.classList.remove("d-none"));
    }

    onClickOnAddSiteButton(e) {
        e.stopPropagation();
        this.uiRenderer.openModal('modal-add-site');
    }

    renderAddExternalSite() {
        const modalAssignSchedule = document.getElementById('modal-add-site');
        let selectElm = modalAssignSchedule.querySelector('select');
        let options = [];
        let firstSchedule = '';
        manager.Schedule.getAllForCrud().forEach(sch => {
            if (firstSchedule == '') {
                firstSchedule = sch.uuid;
            }
            options.push(`<option value="${sch.uuid}"><i class="fas fa-square" style="color: #${sch.uuid}"></i>${sch.name}</option>`)
        });
        selectElm.innerHTML = options.join('');
        selectElm.value = firstSchedule;
        return;
    }

    renderAssignScheduleToSite(values) {
        const modalAssignSchedule = document.getElementById('modal-assign-schedule');
        modalAssignSchedule.querySelector('input[name="site_id"]').value = values.site_id;
        modalAssignSchedule.querySelector('input[name="original_schedule_id"]').value = values.schedule_id;
        let selectElm = modalAssignSchedule.querySelector('select');
        let options = [];
        manager.Schedule.getAllForCrud().forEach(sch => {
            options.push(`<option value="${sch.uuid}"><i class="fas fa-square" style="color: #${sch.uuid}"></i>${sch.name}</option>`)
        });
        selectElm.innerHTML = options.join('');
        selectElm.value = values.schedule_id || "";
        return;
    }
}
class UiPromosRenderer extends UiBaseRenderer {
    appendEventListeners() {
        document.querySelector('#promo-button').addEventListener('click', this.onClickSavePromoCode.bind(this));
        document.querySelector('#button-try-get-codes').addEventListener('click', this.onClickTryGetCodes.bind(this));
        document.querySelector('#promo-table-body').addEventListener('click', this.onClickOnPromoTableBody.bind(this));
    }

    onClickSavePromoCode(e) {
        var promoText = document.getElementById("promo-text-input");
        var promoCode = document.getElementById("promo-code-new");
        var promoDaily = document.getElementById("promo-daily");
        var promoObject = { action: "ADD", code: promoText.value.trim(), repeatDaily: promoDaily.checked };
        promoCode.innerHTML =JSON.stringify(promoObject);
        this.uiRenderer.toast("Adding promo code: " + promoObject.code + "...");
        promoText.value = '';
    }

    onClickTryGetCodes(e) {
        var promoCode = document.getElementById("promo-code-new");
        var promoObject = { action: "TRYGETCODES" };
        promoCode.innerHTML =JSON.stringify(promoObject);
        this.uiRenderer.toast("Fetching codes...");
    }

    _legacyRemoveUsedDailyCodes(codes) {
        if(codes && codes.length) {
            codes.forEach(code => {
                if(!code.repeatDaily) {
                    let counter = 0;
                    for(let i = 0; i < code.statusPerFaucet.length; i++) {
                        if(code.statusPerFaucet[i].execTimeStamp) {
                            counter++;
                        }
                    }
                    if(counter == code.statusPerFaucet.length) {
                        setTimeout(() => removePromoCode(code.id, code.code), 20000);
                    }
                }
            });
        }
    }

    legacyRenderPromotionTable(codes) {
        let tableBody = '';
        this._legacyRemoveUsedDailyCodes(codes);

        for(let c=0; c < codes.length; c++) {
            let data = codes[c];
            tableBody += '<tr data-promotion-code="' + data.code + '" data-promotion-id="' + data.id + '">';
            tableBody += '<td class="align-middle text-left ' + (data.repeatDaily ? 'text-warning' : '') + '">';
            tableBody += `<a class="action-remove-promo-code" data-toggle="tooltip" data-placement="left" title="Remove" onclick=""><i class="fa fa-times-circle"></i></a>`;
            tableBody += '<span  title="' + (data.repeatDaily ? 'Reusable Code' : 'One-time-only Code') + '">' + data.code + '</span></td>';
            tableBody +='<td class="align-middle" title="' + (data.repeatDaily ? 'Reusable Code' : 'One-time-only Code') + '">' + helpers.getPrintableDateTime(data.added) + '</td>';

            for(let i=0, all = data.statusPerFaucet.length; i < all; i++) {
                tableBody +='<td class="align-middle" title="Runned @' + helpers.getPrintableDateTime(data.statusPerFaucet[i].execTimeStamp) + '">' + helpers.getEmojiForPromoStatus(data.statusPerFaucet[i].status ?? 0) + '</td>';
            }
            tableBody +='</tr>';
        }

        document.getElementById('promo-table-body').innerHTML = tableBody;
    }

    onClickOnPromoTableBody(e) {
        let actionElement = e.target;
        if (actionElement.tagName === 'I') {
            actionElement = actionElement.parentElement;
        }
        const row = actionElement.closest('tr');
        if (actionElement.classList.contains('action-remove-promo-code')) {
            e.stopPropagation();
            var promoCode = document.getElementById("promo-code-new");
            var promoObject = { action: "REMOVE", id: row.dataset.promotionId, code: row.dataset.promotionCode };
            promoCode.innerHTML =JSON.stringify(promoObject);
        }
    }
}
class UiConfigRenderer extends UiBaseRenderer {
    legacyRenderConfigData(data) {
        for (const prop in data) {
            let element = document.querySelector('[data-prop="' + prop + '"]');
            if(element) {
                if(element.type == 'select-one' || element.type == 'text' || element.type == 'password' || element.type == 'number' || element.type == 'time') {
                    element.dataset.original = data[prop];
                    element.value = data[prop];
                } else if (element.type == 'checkbox') {
                    element.dataset.original = (data[prop] ? "1" : "0");
                    element.checked = data[prop];
                }
            }
        }

        let elCfTryGetCodes = document.querySelector('[data-prop="cf.tryGetCodes"]')
        let elCredentialsAutologin = document.querySelector('[data-prop="cf.autologin"]');
        let elCredentialsMode = document.querySelector('[data-prop="cf.credentials.mode"]');
        let elCredentialsEmail = document.querySelector('[data-prop="cf.credentials.email"]');
        let elCredentialsPassword = document.querySelector('[data-prop="cf.credentials.password"]');
        let elDevlogEnabled = document.querySelector('[data-prop="devlog.enabled"]');
        let elDevlogMaxLines = document.querySelector('[data-prop="devlog.maxLines"]');
        let elFpigCredentialsMode = document.querySelector('[data-prop="fpb.credentials.mode"]');
        let elFpigCredentialsUsername = document.querySelector('[data-prop="fpb.credentials.username"]');
        let elFpigCredentialsPassword = document.querySelector('[data-prop="fpb.credentials.password"]');
        let elFBchCredentialsMode = document.querySelector('[data-prop="fbch.credentials.mode"]');
        let elFBchCredentialsUsername = document.querySelector('[data-prop="fbch.credentials.username"]');
        let elFBchCredentialsPassword = document.querySelector('[data-prop="fbch.credentials.password"]');
        let elSHostCredentialsMode = document.querySelector('[data-prop="shost.credentials.mode"]');
        let elSHostCredentialsUsername = document.querySelector('[data-prop="shost.credentials.username"]');
        let elSHostCredentialsPassword = document.querySelector('[data-prop="shost.credentials.password"]');
        let elJtfeyCredentialsMode = document.querySelector('[data-prop="jtfey.credentials.mode"]');
        let elJtfeyCredentialsUsername = document.querySelector('[data-prop="jtfey.credentials.username"]');
        let elJtfeyCredentialsPassword = document.querySelector('[data-prop="jtfey.credentials.password"]');
        let elYCoinCredentialsMode = document.querySelector('[data-prop="ycoin.credentials.mode"]');
        let elYCoinCredentialsUsername = document.querySelector('[data-prop="ycoin.credentials.username"]');
        let elYCoinCredentialsPassword = document.querySelector('[data-prop="ycoin.credentials.password"]');
        let elBscadsCredentialsMode = document.querySelector('[data-prop="bscads.credentials.mode"]');
        let elBscadsCredentialsUsername = document.querySelector('[data-prop="bscads.credentials.username"]');
        let elBscadsCredentialsPassword = document.querySelector('[data-prop="bscads.credentials.password"]');

        let elPostpone = document.querySelector('[data-prop="defaults.postponeMinutes"]');
        let elPostponeMin = document.querySelector('[data-prop="defaults.postponeMinutes.min"]');
        let elPostponeMax = document.querySelector('[data-prop="defaults.postponeMinutes.max"]');
        elPostponeMin.disabled = (elPostpone.value > "0");
        elPostponeMax.disabled = (elPostpone.value > "0");
        if (elPostponeMin.disabled && elPostponeMax.disabled) {
            elPostponeMin.value = elPostpone.value;
            elPostponeMax.value = elPostpone.value;
        }
        elPostpone.onchange = function (e) {
            document.querySelector('[data-prop="defaults.postponeMinutes.min"]').disabled = e.target.value > 0;
            document.querySelector('[data-prop="defaults.postponeMinutes.max"]').disabled = e.target.value > 0;
            if (e.target.value > 0) {
                document.querySelector('[data-prop="defaults.postponeMinutes.min"]').value = e.target.value;
                document.querySelector('[data-prop="defaults.postponeMinutes.max"]').value = e.target.value;
            }
        }

        let elNextRun = document.querySelector('[data-prop="defaults.nextRun"]');
        let elNextRunMin = document.querySelector('[data-prop="defaults.nextRun.min"]');
        let elNextRunMax = document.querySelector('[data-prop="defaults.nextRun.max"]');
        let elNextRunUseCountdown = document.querySelector('[data-prop="defaults.nextRun.useCountdown"]');
        elNextRunMin.disabled = (elNextRun.value > "0");
        elNextRunMax.disabled = (elNextRun.value > "0");
        if (elNextRunMin.disabled && elNextRunMax.disabled) {
            elNextRunMin.value = elNextRun.value;
            elNextRunMax.value = elNextRun.value;
        }
        elNextRun.onchange = function (e) {
            document.querySelector('[data-prop="defaults.nextRun.min"]').disabled = e.target.value > 0;
            document.querySelector('[data-prop="defaults.nextRun.max"]').disabled = e.target.value > 0;
            if (e.target.value > 0) {
                document.querySelector('[data-prop="defaults.nextRun.min"]').value = e.target.value;
                document.querySelector('[data-prop="defaults.nextRun.max"]').value = e.target.value;
            }
        }

        let elSleepMode = document.querySelector('[data-prop="defaults.sleepMode"]');
        let elSleepModeMin = document.querySelector('[data-prop="defaults.sleepMode.min"]');
        let elSleepModeMax = document.querySelector('[data-prop="defaults.sleepMode.max"]');
        elSleepModeMin.disabled = !elSleepMode.checked;
        elSleepModeMax.disabled = !elSleepMode.checked;
        elSleepMode.onchange = function (e) {
            document.querySelector('[data-prop="defaults.sleepMode.min"]').disabled = !e.target.checked;
            document.querySelector('[data-prop="defaults.sleepMode.max"]').disabled = !e.target.checked;
        }

        elCredentialsMode.disabled = !elCredentialsAutologin.checked;

        elCredentialsEmail.disabled = ( (!elCredentialsAutologin.checked || elCredentialsMode.value == "2") ? true : false);
        elCredentialsPassword.disabled = ( (!elCredentialsAutologin.checked || elCredentialsMode.value == "2") ? true : false);

        elCredentialsAutologin.onchange = function (e) {
            document.querySelector('[data-prop="cf.credentials.mode"]').disabled = !e.target.checked;
            if (elCredentialsMode.value == "2") {
                document.querySelector('[data-prop="cf.credentials.email"]').disabled = true;
                document.querySelector('[data-prop="cf.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="cf.credentials.email"]').disabled = false;
                document.querySelector('[data-prop="cf.credentials.password"]').disabled = false;
            }
        }

        elCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="cf.credentials.email"]').disabled = true;
                document.querySelector('[data-prop="cf.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="cf.credentials.email"]').disabled = false;
                document.querySelector('[data-prop="cf.credentials.password"]').disabled = false;
            }
        }

        elFpigCredentialsUsername.disabled = ( (elFpigCredentialsMode.value == "2") ? true : false);
        elFpigCredentialsPassword.disabled = ( (elFpigCredentialsMode.value == "2") ? true : false);
        elFpigCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="fpb.credentials.username"]').disabled = true;
                document.querySelector('[data-prop="fpb.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="fpb.credentials.username"]').disabled = false;
                document.querySelector('[data-prop="fpb.credentials.password"]').disabled = false;
            }
        }

        elSHostCredentialsUsername.disabled = ( (elSHostCredentialsMode.value == "2") ? true : false);
        elSHostCredentialsPassword.disabled = ( (elSHostCredentialsMode.value == "2") ? true : false);
        elSHostCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="shost.credentials.username"]').disabled = true;
                document.querySelector('[data-prop="shost.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="shost.credentials.username"]').disabled = false;
                document.querySelector('[data-prop="shost.credentials.password"]').disabled = false;
            }
        }

        elYCoinCredentialsUsername.disabled = ( (elYCoinCredentialsMode.value == "2") ? true : false);
        elYCoinCredentialsPassword.disabled = ( (elYCoinCredentialsMode.value == "2") ? true : false);
        elYCoinCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="ycoin.credentials.username"]').disabled = true;
                document.querySelector('[data-prop="ycoin.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="ycoin.credentials.username"]').disabled = false;
                document.querySelector('[data-prop="ycoin.credentials.password"]').disabled = false;
            }
        }

        elFBchCredentialsUsername.disabled = ( (elFBchCredentialsMode.value == "2") ? true : false);
        elFBchCredentialsPassword.disabled = ( (elFBchCredentialsMode.value == "2") ? true : false);
        elFBchCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="fbch.credentials.username"]').disabled = true;
                document.querySelector('[data-prop="fbch.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="fbch.credentials.username"]').disabled = false;
                document.querySelector('[data-prop="fbch.credentials.password"]').disabled = false;
            }
        }

        elJtfeyCredentialsUsername.disabled = ( (elJtfeyCredentialsMode.value == "2") ? true : false);
        elJtfeyCredentialsPassword.disabled = ( (elJtfeyCredentialsMode.value == "2") ? true : false);
        elJtfeyCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="jtfey.credentials.username"]').disabled = true;
                document.querySelector('[data-prop="jtfey.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="jtfey.credentials.username"]').disabled = false;
                document.querySelector('[data-prop="jtfey.credentials.password"]').disabled = false;
            }
        }

        elBscadsCredentialsUsername.disabled = ( (elBscadsCredentialsMode.value == "2") ? true : false);
        elBscadsCredentialsPassword.disabled = ( (elBscadsCredentialsMode.value == "2") ? true : false);
        elBscadsCredentialsMode.onchange = function (e) {
            if (e.target.value == "2") {
                document.querySelector('[data-prop="bscads.credentials.username"]').disabled = true;
                document.querySelector('[data-prop="bsdads.credentials.password"]').disabled = true;
            } else {
                document.querySelector('[data-prop="bscads.credentials.username"]').disabled = false;
                document.querySelector('[data-prop="bscads.credentials.password"]').disabled = false;
            }
        }

        elDevlogMaxLines.disabled = !elDevlogEnabled.checked;
        elDevlogEnabled.onchange = function (e) {
            document.querySelector('[data-prop="devlog.maxLines"]').disabled = !e.target.checked;
        }
    }
}
class UiWalletRenderer extends UiBaseRenderer {
    legacyRenderWalletTable(data) {
        let tableBody = '';

        for(let i=0, all = data.length; i < all; i++) {
            tableBody += '<tr class="align-middle" data-id="'+ data[i].id + '">';
            tableBody += '<td class="align-middle">' + data[i].name + '</td>';
            tableBody += '<td class="align-middle em-input"><input type="text" class="w-100" onfocus="this.select();" data-field="address" data-original="' + data[i].address + '" value="' + data[i].address + '"></td>';
            tableBody += '</tr>';
        }

        document.getElementById('wallet-table-body').innerHTML = tableBody;
    }
}
class UiSchedulesRenderer extends UiBaseRenderer {
    appendEventListeners() {
        document.querySelector('#schedules-toggler').addEventListener('change', this.onScheduleToggled.bind(this));
    }

    onScheduleToggled(e) {
        e.stopPropagation();
        let actionElement = e.target.tagName !== 'LABEL' ? e.target.closest('label') : e.target;
        let otherActiveLabels = [...actionElement.parentElement.querySelectorAll('label.active')].filter(l => l.dataset.schedule != actionElement.dataset.schedule);
        if (otherActiveLabels.length > 0) {
            otherActiveLabels.forEach(l => l.classList.remove('active'));
        }
        this.toggleSchedule(actionElement.dataset.schedule);
    }

    toggleSchedule(uuid) {

        if (uuid) {
            this.selectedSchedule = uuid;
        } else {
            if (!this.selectedSchedule) {
                this.selectedSchedule = 'all';
            }
        }

        [...document.querySelectorAll('#schedule-table-body tr')].forEach((row) => {
            if (this.selectedSchedule == 'all') {
                row.classList.remove('d-none');
            } else if (row.getAttribute('data-schedule') == this.selectedSchedule) {
                row.classList.remove('d-none');
            } else {
                row.classList.add('d-none');
            }
        });

        if (this.selectedSchedule == 'all') {
            [...document.querySelectorAll('#console-log tr')].forEach(x => {
                x.classList.remove('d-none');
            })
        } else {
            [...document.querySelectorAll('#console-log tr')].forEach(x => {
                if (x.getAttribute('data-schedule') == 'false' || x.getAttribute('data-schedule') == this.selectedSchedule) {
                    x.classList.remove('d-none');
                } else {
                    x.classList.add('d-none');
                }
            })
        }
    };

    renderTBody() {
        let rows = [];
        manager.Schedule.getAllForCrud().forEach(sch => {
            rows.push(this.renderRow(sch));
        });
        return rows.join('');
    }

    renderRow(sch) {
        let row = 
        `<tr data-uuid="${sch.uuid}" 
                data-order="${sch.order}" 
                data-added="${sch.added ? 'true' : 'false'}" 
                data-removed="false" 
                data-updated="false" 
                data-originals='${!sch.added ? JSON.stringify(sch) : ""}'>
            <td class="row-handle"><i class="fas fa-grip-vertical"></i></td>
            <td><div class="input-group input-group-sm color-picker colorpicker-element" style="max-width: 125px;">
                <div class="input-group-prepend"><span class="input-group-text"><i class="fas fa-square" style="color: #${sch.uuid}"></i></span></div>
                <input type="text" name="uuid" class="form-control" data-original-title="" value="${sch.uuid}">
            </div></td>
            <td><input type="text" name="name" class="form-control form-control-sm" value="${sch.name}"></td>
            <td>
                <button type="button" title="Remove" class="btn btn-default btn-sm action-schedule-remove"><i class="fa fa-trash"></i></button>
            </td>
        </tr>`;
        return row;
    }
}
class UiSiteParameterRenderer extends UiBaseRenderer {
    static handlers = new Map();

    static registerHandler(name, handler) {
        UiSiteParameterRenderer.handlers.set(name, handler);
    }

    static getHandler(name) {
        const handlerClass = UiSiteParameterRenderer.handlers.get(name);
        return handlerClass || false;
    }

    appendEventListeners() {
        document.querySelector('#modal-site-parameters').addEventListener('click', this.onClickOnModalSiteParameter.bind(this));
    }

    onClickOnModalSiteParameter(e) {
        const modal = document.querySelector('#modal-site-parameters');
        let actionElement = e.target.tagName === 'I' ? e.target.parentElement : e.target;
        if (actionElement.classList.contains('modal-save')) {
            e.preventDefault();
            let form = modal.querySelector('.form-container form');
            if (!form.checkValidity()) {
                form.reportValidity();
                return;
            }
            let data = this.uiRenderer.parseContainer(form);
            $(modal).modal('hide');
        }
    }

    renderFields(values) {
        let fieldsHtml = '';
        values.forEach( field => {
            field.text = field.text || field.name.charAt(0).toUpperCase() + field.name.slice(1).toLowerCase().replaceAll('_', ' ');

            switch (field.type) {
                case 'credentials_or_autofilled': // TODO: will need to condition username/email/password!
                    break;
                case 'email':
                    fieldsHtml += uiRenderer.addInputEmailHtml(field);
                    break;
                case 'password':
                    fieldsHtml += uiRenderer.addInputPasswordHtml(field);
                    break;
                case 'checkbox':
                    field.value = (field.value === true || field.value === 'true' || field.value === 1 || field.value === "1") ? true : false;
                    fieldsHtml += uiRenderer.addSliderHtml(field);
                    break;
                case 'numberInput':
                    fieldsHtml += uiRenderer.addInputNumberHtml(field);
                    break;
                case 'textInput':
                case 'username':
                    field.required = 'true';
                default:
                    fieldsHtml += uiRenderer.addInputTextHtml(field);
                    break;
            }
        });
        const modalSiteParameters = document.getElementById('modal-site-parameters');
        modalSiteParameters.querySelector('.form-container form').innerHTML = fieldsHtml;
    }

    renderEditSiteParameters(args) { // { site_id: 'x' }
        const site = manager.Site.getById(args.site_id);

        const siteParameters = site.getSiteParameters(); // async? for external site parameters that need to be loaded from other stg...

        if (!siteParameters) {
            console.warn(`Site ${site.id} ${site.name} does not require parameters setup.`);
            return;
        }

        if (!siteParameters.handler) {
            console.warn(`Handler name is missing`);
            return;
        }

        const handlerClass = UiSiteParameterRenderer.getHandler(siteParameters.handler);
        if (!handlerClass) {
            console.warn(`Invalid handler class name: ${siteParameters.handler}`);
            return;
        }

        const handler = new handlerClass(siteParameters.values);
        handler.preRender();

        let fields = [
            { name: 'AUTO_UPDATE_PROMO_CODES', type: 'checkbox', value: 'false' },
            { name: 'MAX_ROLLS_PER_VISIT', type: 'numberInput', value: 1, min: 0 },
            { name: 'AUTO_LOGIN', type: 'checkbox', value: 'true' },
            { name: 'EMAIL', type: 'email', value: '' },
            { name: 'PASSWORD', type: 'password', value: '' }
        ];

        fields.forEach( (f, idx) => {
            if (f.order == null || f.order == undefined) {
                f.order = idx;
            }
        });

        let values = persistence.load(`site_parameters_${args.site_id}`, true) || [];
        for(const field of fields) {
            let vIdx = values.findIndex(v => v.name == field.name);
            if (vIdx > -1) {
                field.value = values[vIdx].value;
            }
        }
        values = fields;

        this.renderFields(values);

        handler.postRender();
        return;
    }

}
class UiCfSiteParametersHandler extends UiSiteParameterRenderer {
    constructor(values) {
        this.values = {
            rollsPerVisit: 1,
            tryGetCodes: true,
            autologin: false,
            credentialsMode: '2',
            email: null,
            password: null,
        }
        Object.assign(this.values, values);
    }

    render() {
        fieldValues = this.values;
        let disableModeSelect = (fieldValues['autologin'].value !== true);
        let disableEmailAndPassword = (fieldValues['autologin'].value !== true || fieldValues['credentialsMode'].value == '2');
        let html = '';
        html += '         <div class="card m-1 collapsed-card"><div class="card-header">CryptosFaucets<div class="card-tools"><button type="button" class="btn btn-white btn-sm" data-card-widget="collapse" title="Collapse"><i class="fas fa-plus"></i></button></div></div>';
        html += '           <div class="card-body px-4" style="display: none;">';
        html += '          <div><label class="switch"><input type="checkbox" data-prop="cf.tryGetCodes" ><span class="slider round"></span></label> Auto update promo codes </div>';
        html += '          <div><label class="switch"><input type="checkbox" data-prop="cf.rollOnce" ><span class="slider round"></span></label> Roll once per round </div>';
        html += `          <div><label class="switch"><input type="checkbox" data-prop="cf.autologin"><span class="slider round"></span></label> Autologin when necessary</div>`;
        html += `           <select class="form-control" data-prop="cf.credentials.mode" ${disableModeSelect ? 'disabled' : ''}>`;
        html += '            <option value="1">Use Email and Password</option><option value="2">Filled by 3rd party software/extension</option>';
        html += '           </select>';
        html += '           <label class="control-label">E-Mail</label>';
        html += `           <input maxlength="200" type="text" data-prop="cf.credentials.email" required="required" ${disableEmailAndPassword ? 'disabled=' : ''} class="form-control" placeholder="Email address..."/>`;
        html += '           <label class="control-label">Password</label>';
        html += `           <input maxlength="200" type="password" data-prop="cf.credentials.password" required="required" ${disableEmailAndPassword ? 'disabled=' : ''} class="form-control" placeholder="Password..."/>`;
        html += '           <label class="control-label">Hours to wait If IP is banned:</label>';
        html += '           <select class="form-control" data-prop="cf.sleepHoursIfIpBan">';
        html += '            <option value="0">Disabled</option><option value="2">2</option><option value="4">4</option><option value="8">8</option><option value="16">16</option><option value="24">24</option><option value="26">26</option>';
        html += '           </select>';
        html += '       </div></div>';
    }

    preRender() {

    }

    postRender() {
        let elCredentialsAutologin = document.querySelector('[name="autologin"]');
        let elCredentialsMode = document.querySelector('[name="credentialsMode"]');

        elCredentialsAutologin.addEventListener('change', function (e) {
            let form = e.target.closest('form');
            form.querySelector('[name="credentialsMode"]').disabled = !e.target.checked;
            form.querySelector('[name="email"]').disabled = !e.target.checked;
            form.querySelector('[name="password"]').disabled = !e.target.checked;
        });

        elCredentialsMode.addEventListener('change', function (e) {
            let form = e.target.closest('form');
            form.querySelector('[name="email"]').disabled = (e.target.value == '2');
            form.querySelector('[name="password"]').disabled = (e.target.value == '2');
        });
    }
}
class UiRenderer {
    constructor () {
        this.sites = new UiSitesRenderer(this);
        this.siteParameters = new UiSiteParameterRenderer(this);
        this.promos = new UiPromosRenderer(this);
        this.config = new UiConfigRenderer(this);
        this.wallet = new UiWalletRenderer(this);
        this.schedules = new UiSchedulesRenderer(this);
        this.selectedSchedule = null;
    }

    initialize() {
        this.appendCSS();
    }

    toast(msg, msgType = "info") {
        toastr[msgType](msg);
    }

    openModal(id, values = null) {
        const dlg = document.querySelector('#modal-dlg');
        dlg.querySelectorAll(".modal-content").forEach(x => x.classList.add('d-none'));
        switch (id) {
            case 'modal-backup':
                console.info('TODO: GM_listValues => loop keys => save json as blob');
                break;

            case 'modal-ereport':
            case 'modal-config':
            case 'modal-site':
                document.getElementById("target-spinner").innerHTML = JSON.stringify({id: id, siteId: values});
                document.getElementById("modal-spinner").classList.remove("d-none");
                dlg.querySelector('div').classList.add('modal-lg');
                break;
            case 'modal-add-site':
                this.sites.renderAddExternalSite();
                document.getElementById(id).classList.remove("d-none");
                dlg.querySelector('div').classList.remove('modal-lg');
                break;
            case 'modal-slAlert':
                shortlinkAlert.load(id);
                dlg.querySelector('div').classList.add('modal-lg');
                break;
            case 'modal-schedules':
                document.getElementById(id).querySelector('table tbody').innerHTML = this.schedules.renderTBody();
                this.appendColorPickers('.color-picker');
                dlg.querySelector('div').classList.remove('modal-lg');
                document.getElementById(id).classList.remove("d-none");
                break;
            case 'modal-assign-schedule':
                this.sites.renderAssignScheduleToSite(values);
                document.getElementById(id).classList.remove("d-none");
                dlg.querySelector('div').classList.remove('modal-lg');
                break;
            case 'modal-site-parameters':
                this.siteParameters.renderEditSiteParameters(values);
                document.getElementById(id).classList.remove("d-none");
                dlg.querySelector('div').classList.remove('modal-lg');
                break;
            default:
                dlg.querySelector('div').classList.add('modal-lg');
                document.getElementById(id).classList.remove("d-none");
                break;
        }

        $(dlg).modal('show');
    }

    appendEventListeners() {
        for (const renderer in this) {
            if (this[renderer] && typeof this[renderer].appendEventListeners === 'function') {
                this[renderer].appendEventListeners();
            }
        }
        $('[data-toggle="tooltip"]').tooltip({
            trigger: 'hover'
        });
    }

    appendCSS() {
        let css = document.createElement('style');
        css.innerHTML = `
            td.em-input {
                padding-top: 0;
                padding-bottom: 0;
            }
            pre {
                height: 145px;
                width:100%;
                white-space: pre-wrap;
                padding-left: 1em;
            }
            pre span {
                display: block;
            }
            .row-schedule-handle {

            }
            .grabbable:not(.d-none):not(.in-use) {
                cursor: move;
                cursor: grab;
                cursor: -moz-grab;
                cursor: -webkit-grab;
            }

            .grabbable:not(.d-none):not(.in-use):active {
                cursor: grabbing;
                cursor: -moz-grabbing;
                cursor: -webkit-grabbing;
            }

            .row-handle {
                cursor: grab;
            }

            .dropdown-item {
                cursor: pointer;
            }

            #schedule-table th,td {
                vertical-align: middle;
                padding-top: .25rem!important;
                padding-bottom: .25rem!important;
            }

            #schedule-table-body td.em-input input[readonly] {
                background-color:transparent;
                border: 0;
                font-size: 1em;
            }

            td[data-field="displayName"] .input-group-prepend .input-group-text {
                background-color: transparent;
                border-color: transparent;
                font-size: 1em;
            }

            .custom-switch input,label {
                cursor: pointer;
            }
            `;
        document.head.appendChild(css);
    }

    addLegacySliderHtml(propName, propValue, text) {
        return `<label class="switch"><input type="checkbox" ${propName}="${propValue}" data-original="1"><span class="slider round"></span></label> ${text}`;
    }

    addSliderHtml(field) {
        const rndStr = helpers.randomString(8);
        return `<div class="form-group"><div class="custom-control custom-switch custom-switch-off-danger custom-switch-on-success">
                <input type="checkbox" class="custom-control-input" name="${field.name}" id="${rndStr}" ${field.value ? 'checked' : ''}>
                <label class="custom-control-label" for="${rndStr}">${field.text}</label>
                </div></div>`;
    }

    addInputEmailHtml(field) {
        const rndStr = helpers.randomString(8);
        const pattern = "[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$";
        return `<div class="form-group row"><label for="${rndStr}" class="col-sm-4 col-form-label">${field.text}</label>
        <div class="col-sm-8">
        <input type="email" class="form-control" required id="${rndStr}" placeholder="${field.placeholder || ' '}" name="${field.name}" pattern="${pattern}" value="${field.value || ''}">
        </div></div>`;

    }

    addInputPasswordHtml(field) {
        const rndStr = helpers.randomString(8);
        return `<div class="form-group row"><label for="${rndStr}" class="col-sm-4 col-form-label">${field.text}</label>
        <div class="col-sm-8">
        <input type="password" min="${field.min !== undefined ? field.min : ''}" min="${field.max !== undefined ? field.max : ''}" class="form-control" required id="${rndStr}" placeholder="${field.placeholder || ' '}" name="${field.name}" value="${field.value || ''}">
        </div></div>`;
    }

    addInputTextHtml(field) {
        const rndStr = helpers.randomString(8);
        return `<div class="form-group row"><label for="${rndStr}" class="col-sm-4 col-form-label">${field.text}</label>
        <div class="col-sm-8">
        <input type="text" class="form-control" required="${field.required || 'false'}" id="${rndStr}" placeholder="${field.placeholder || ' '}" name="${field.name}" value="${field.value || ''}">
        </div></div>`;
    }

    addInputNumberHtml(field) {
        const rndStr = helpers.randomString(8);
        return `<div class="form-group row"><label for="${rndStr}" class="col-sm-4 col-form-label">${field.text}</label>
        <div class="col-sm-8">
        <input type="number" step="${field.step || 1}" class="form-control" required id="${rndStr}" placeholder="${field.placeholder || ' '}" name="${field.name}" value="${field.value || ''}">
        </div></div>`;
    }

    parseContainer(container) {
        let obj = {};
        Object.assign(obj, container.dataset);
        let inputs = container.querySelectorAll('input, select');
        inputs.forEach(function (input) {
            if (input.type == 'checkbox') {
                obj[input.name] = input.checked ? 'true' : 'false';
            } else if (input.type == 'number') {
                obj[input.name] = +input.value;
            } else {
                obj[input.name] = input.value;
            }
        });

        for (const p in obj) { // type converter. TODO: add int, float, etc.
            if (obj[p] === 'true') { obj[p] = true }
            if (obj[p] === 'false') { obj[p] = false }

            if (p == 'uuid') {
                obj[p] = obj[p].toLowerCase().replace('#', '');
            }
            if (p == 'originals') {
                try {
                    obj[p] = JSON.parse(obj[p]);
                } catch (err) { delete obj[p]; }
            }
        }
        return obj;
    }

    parseTable(table) {
        let rows = table.querySelectorAll('tbody tr');
        let data = [];
        rows.forEach( (r, idx) => {
            let obj = this.parseContainer(r);
            obj.order = '' + idx; // fix order
            if (!(obj.added && obj.removed)) { // skip if it was just added and removed
                data.push(obj);
            }
        });

        return data;
    }

    appendColorPickers(selector) {
        $(selector).each(function () {
            $(this).colorpicker();
            $(this).on('colorpickerChange', function(event) {
                $(event.target.querySelector('.fa-square')).css('color', event.color.toString());
            });
        });
    }
}

    class EventEmitter {
        constructor() {
            this.events = {};
        }

        on(eventName, callback) {
            if (!this.events[eventName]) {
                this.events[eventName] = [];
            }
            this.events[eventName].push(callback);
        }

        emit(eventName, data) {
            const eventCallbacks = this.events[eventName];
            if (eventCallbacks) {
                eventCallbacks.forEach(callback => {
                    callback(data);
                });
            }
        }
    }

    class Timeout {
        constructor(seconds) {
            this.startedAt;
            this.interval;
            this.cb = (() => { shared.closeWithError(K.ErrorType.TIMEOUT, '') });
            if (seconds) {
                this.wait = seconds;
            } else {
                let paramTimeout =  shared.getParam('timeout');
                if (paramTimeout) {
                    this.wait = paramTimeout * 60;
                } else {
                    this.wait = shared.getConfig()['defaults.timeout'] * 60
                }
            }
            this.restart();
        }

        get elapsed() {
            return Date.now() - this.startedAt;
        }

        restart(addSeconds = false) {
            if(this.interval) {
                clearTimeout(this.interval);
            }
            this.startedAt = Date.now();
            if(addSeconds) {
                this.wait = this.wait + addSeconds;
            }
            this.interval = setTimeout( () => { this.cb() }, this.wait * 1000);
        }
    }

    class Timer {
        constructor(params) {
            Object.assign(this, params);
            if(!useTimer || (this.webType && !Timer.webTypes().includes(this.webType))) {
                return;
            }
            this.delay = this.delaySeconds * 1000;

            if(!this.isManager) {
                this.tick();
                this.interval = setInterval(
                    () => { this.tick() }, this.delay);
            }
        }

        static webTypes() {
            return [K.WebType.FREELITECOIN, K.WebType.FREEETHEREUMIO, K.WebType.BIGBTC, K.WebType.FCRYPTO, K.WebType.FPB, K.WebType.BSCADS]
        };

        startCheck(webType) {
            this.webType = webType;
            if(!useTimer || (webType && !Timer.webTypes().includes(webType))) {
                return;
            }
            persistence.save(this.uuid + '_lastAccess', Date.now());
            this.interval = setInterval(() => {
                this.isAlive();
            }, this.delay);
        }

        stopCheck() {
            if(!useTimer) {
                return;
            }
            clearInterval(this.interval);
        }

        tick() {
            if(!useTimer) {
                return;
            }
            persistence.save(this.uuid + '_lastAccess', Date.now());
        }

        isAlive() {
            if(!useTimer) {
                return;
            }
            let now = Date.now();
            let newAccess = persistence.load(this.uuid + '_lastAccess');
            if(newAccess && (now - newAccess > this.delay)) {
                manager.reloadWorkingTab(schedule);
            }
        }
    }

    const wait = ms => new Promise(resolve => setTimeout(resolve, ms || 3000));

    class CrawlerWidget {
    constructor(params) {
        if (!params || !params.selector) {
            throw new Error('CrawlerWidget requires a selector parameter');
        }
        this.context = this.context || document;
        Object.assign(this, params);
    }

    get isUserFriendly() {
        this.element = this.context.isUserFriendly(this.selector);
        return this.element;
    }
}

class ReadableWidget extends CrawlerWidget {
    constructor(params) {
        if (params && !params.parser) {
            params.parser = Parsers.innerText; //default parser
        }
        super(params);
    }

    get value() {
        if (this.isUserFriendly) {
            return this.parser(this.element, this.options);
        } else {
            return '';
        }
    }
}

class TextboxWidget extends CrawlerWidget {
    get value() {
        if (!this.isUserFriendly) {
            return '';
        }
        return this.element.value;
    }

    set value(newValue) {
        if (!this.isUserFriendly) {
            return '';
        }
        this.element.value = newValue;
        return '';
    }
}

class ButtonWidget extends CrawlerWidget {

    click() {
        if (this.isUserFriendly) {
            this.element.click();
            return Promise.resolve(true);
        } else {
        }
    }
}

class SubmitWidget extends CrawlerWidget {
    click() {
        if (this.isUserFriendly) {
            let frm = this.element;
            while(frm.nodeName != 'FORM' && frm.nodeName != null) {
                frm = frm.parentElement;
            }
            if (frm.nodeName == 'FORM') {
                frm.submit();
            } else {
                return;
            }
            return Promise.resolve(true);
        } else {
        }
    }
}

class CountdownWidget extends CrawlerWidget {
    constructor(params) {
        if (params && !params.parser) {
            params.parser = Parsers.innerText; //default parser
        }
        super(params);
    }

    get timeLeft() {
        if (this.isUserFriendly) {
            return this.parser(this.element, this.options);
        } else {
            throw new Error(`CountdownWidget (selector: '${this.selector}') cannot be read`);
        }
    }
}

class Parsers {
    static innerText(elm) { // '0.12341234' => '0.12341234'
        try {
            return elm.innerText;
        } catch (err) { }
    }
    static trimNaNs(elm) { // 'You won 0.12341234 TRX' => '0.12341234'
        try {
            return elm.innerText.replace(/[^\d.-]/g, '');
        } catch (err) { }
    }
    static splitAndIdxTrimNaNs(elm, options) { // '17.96 Coins (17.50 + 0.46)' => 17.96
        try {
            return elm.innerText.split(options.splitter)[options.idx].replace(/[^\d.-]/g, '');
        } catch (err) { }
    }
    static innerTextIntToFloat(elm) { // 'You won 1234 satoshis' => 0.00001234
        try {
            let sats = elm.innerText.replace(/\D/g, '');
            return sats / 100000000;
        } catch (err) { }
    }
    static innerTextJoinedToInt(elm) { // '7|2|9|6' => 7296
        try {
            return parseInt([... elm].map( x => x.innerText).join(''));
        } catch (err) { }
    }
    static stormGainCountdown(elm) { // '3:01:01' => 120000
        try {
            let timeLeft = elm.innerText.split(':');
            if (timeLeft[0] == 'Synchronizing') {
            }

            if(timeLeft.length === 3) {
                return parseInt(timeLeft[0]) * 60 + parseInt(timeLeft[1]);
            }
        } catch (err) {
            return null;
        }
    }
    static kingBizCountdown(elm) { // '4|2' => 42
        try {
            let itms = elm.querySelectorAll('.flip-clock-active .up');
            if (itms.length > 1 && itms[0].isVisible() && itms[1].isVisible()) {
                return parseInt([itms[0].innerText, itms[1].innerText].join(''));
            }
        } catch (err) {
            return null;
        }
    }
    static freeGrcCountdown(elm) { // 'Wait for 53:31 before next roll' => 53
        try {
            let val = elm.innerText.split(':')[0];
            val = val.replace(/[^\d.-]/g, '');
            return parseInt(val);
        } catch (err) {
            return null;
        }
    }
    static bestChangeCountdown(elm) { // '00:58:35' => 58
        try {
            if (elm.value) {
                let timeLeft = elm.value.split(':');
                if (timeLeft.length > 1) {
                    return parseInt(timeLeft[1]);
                }
            }
        } catch (err) {
            return null;
        }
    }
    static freeEthereumIoClaimed(elm) { // 'You won 0.12341234 TRX and rolled number 7623' => 0.12341234
        try {
            let line = elm.innerHTML;
            let idx = line.search(/0\./);
            return parseFloat(line.slice(idx, idx + 10));
        } catch (err) { }
    }
    static bfBoxClaimed(elm) {
        try {
            let currency = elm.querySelector('.free-box__withdraw-currency').innerText;
            let val = elm.querySelector('.free-box__need-sum').innerText.replace(/ /g,'').split('/')[1];

            if (currency == 'Satoshi') {
                val = val/100000000;
            }
            return val;
        } catch (err) {
            return null;
        }
    }
    static g8ClaimsLeft(elm) {
        try {
            if (elm.innerText.includes('\nYou have ')) { // 'Claim 183848 satoshi (0.00012 USD) every 20 Seconds\nYou have 70 claims left today.'
                let val = elm.innerText.split('\nYou have ')[1].split(' ')[0];
                return val;
            } else {
                return null;
            }
        } catch (err) {
            return null;
        }
    }
    static cbgClaimed(elm) {
        try {
            if (elm.innerText.includes('was sent to')) { //?? was sent to you on...
                let val = elm.innerText.trim().split(' ')[0];
                if (elm.innerText.includes('oshi') || elm.innerText.includes('gwei')) {
                    val = val/100000000;
                }
                return val;
            } else {
                return null;
            }
        } catch (err) {
            return null;
        }
    }
    static dutchysClaimed(elm) { // 'You Won :101  DUTCHY + 20 XP' => 101
        try {
            let splitted = elm.innerText.split('DUTCHY');
            return splitted[0].replace(/[^\d.-]/g, '');
        } catch (err) { shared.devlog(`@Parsers.dutchysClaimed, with element [${elm}] Error: ${err}`); }
    }
    static dutchysClaimedToFloat(elm) { // 'You Won :22437 ADA + 100 XP' => 0.00022437
        try {
            let sats = elm.innerText.split('+');
            sats = sats[0].replace(/\D/g, '');
            return sats / 100000000;
        } catch (err) { shared.devlog(`@Parsers.dutchysClaimedToFloat, with element [${elm}] Error: ${err}`); }
    }
    static splitAndIdxToInt(elm, options) { // '26 Minutes 23' w/spliiter='Minutes' => 26
        try {
            return parseInt(elm.innerText.split(options.splitter)[options.idx].trim());
        } catch (err) { shared.devlog(`Error @Parsers.splitAndIdxToInt: ${err}`); }
    }
    static fromTextTimer(elm) { // '0 hours 11 minutes 1 seconds' => 12 minutes
        try {
            let hours, minutes;
            hours = +elm.innerText.split(' hours')[0].trim();
            minutes = +elm.innerText.split('hours ')[1].split('minutes')[0].trim();
            return hours * 60 + minutes + 1;
        } catch (err) { shared.devlog(`Error @Parsers.splitAndIdxToInt: ${err}`); }
    }
}
class ImageProcessor {
    constructor(img) {
        this._img = img;
    }

    isImageComplete() {
        return this._img && this._img.complete;
    }

    createDrawer(width, height) {
        let canvas = document.createElement('canvas');
        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        let ctx = canvas.getContext('2d');
        return {
            canvas: canvas,
            ctx: ctx
        };
    }

    getDrawer() {
        return this._drawer;
    }

    toCanvas() {
        this._drawer = this.createDrawer(this._img.width, this._img.height);
        this._drawer.ctx.drawImage(this._img, 0, 0);
    }

    foreach(filter) {
        let imgData = this._drawer.ctx.getImageData(0, 0, this._drawer.canvas.width, this._drawer.canvas.height);
        for (var x = 0; x < imgData.width; x++) {
            for (var y = 0; y < imgData.height; y++) {
                var i = x * 4 + y * 4 * imgData.width;
                var pixel = { r: imgData.data[i + 0], g: imgData.data[i + 1], b: imgData.data[i + 2] };

                pixel = filter(pixel);

                imgData.data[i + 0] = pixel.r;
                imgData.data[i + 1] = pixel.g;
                imgData.data[i + 2] = pixel.b;
                imgData.data[i + 3] = 255;
            }
        }
        this._drawer.ctx.putImageData(imgData, 0, 0);
    }

    binarize (threshold) {
        var image = this._drawer.canvas.getContext('2d').getImageData(0, 0, this._drawer.canvas.width, this._drawer.canvas.height);
        for (var x = 0; x < image.width; x++) {
            for (var y = 0; y < image.height; y++) {
                var i = x * 4 + y * 4 * image.width;
                var brightness = 0.34 * image.data[i] + 0.5 * image.data[i + 1] + 0.16 * image.data[i + 2];
                image.data[i] = brightness >= threshold ? 255 : 0;
                image.data[i + 1] = brightness >= threshold ? 255 : 0;
                image.data[i + 2] = brightness >= threshold ? 255 : 0;
                image.data[i + 3] = 255;
            }
        }
        this._drawer.canvas.getContext('2d').putImageData(image, 0, 0);
    }

    invert(filter) {
        this.foreach(function (p) {
            p.r = 255 - p.r;
            p.g = 255 - p.g;
            p.b = 255 - p.b;
            return p;
        });
    }

    imgDataToBool(imgData) {
        let character = [];
        const data = imgData.data;
        for (let i = 0; i < imgData.data.length; i += 4) {
            let val = data[i] + data[i+1] + data[i+2];
            character.push(val == 0 ? true : false);
        }
        return character;
    }
}

class CaptchaWidget extends CrawlerWidget {
    constructor(params) {
        super(params);
    }

    solve() { return true; }

    async isSolved() { return false; }
}

class RecaptchaWidget extends CaptchaWidget {
    constructor(params) {
        this.context = this.context || document;
        let defaultParams = {
            waitMs: [1000, 5000],
            timeoutMs: 4 * 60 * 1000
        };
        for (let p in params) {
            defaultParams[p] = params[p];
        }
        Object.assign(this, defaultParams);
    }

    get isUserFriendly() {
        this.element = grecaptcha;
        return this.element;
    }

    async isSolved() {
        return wait().then( () => {
            if (this.isUserFriendly && this.element.hasOwnProperty('getResponse') && (typeof(this.element.getResponse) == 'function')
                && this.element.getResponse().length > 0) {
                return Promise.resolve(true);
            }
            return this.isSolved();
        });
    }
}

class HCaptchaWidget extends CaptchaWidget {
    constructor(params) {
        let defaultParams = {
            selector: '.h-captcha > iframe',
            waitMs: [1000, 5000],
            timeoutMs: 4 * 60 * 1000
        };
        for (let p in params) {
            defaultParams[p] = params[p];
        }
        super(defaultParams);
    }

    async isSolved() {
        return wait().then( () => {
            if (this.isUserFriendly && this.element.hasAttribute('data-hcaptcha-response') && this.element.getAttribute('data-hcaptcha-response').length > 0) {
                return Promise.resolve(true);
            }
            return this.isSolved();
        });
    }
}

class BKCaptchaWidget extends CaptchaWidget {
    constructor() {
        let defaultParams = {
            selector: 'img[src="antibot.php"]',
            waitMs: [1000, 5000],
            timeoutMs: 4 * 60 * 1000
        };
        super(defaultParams);
        this._imgProcessor;
        this._characters = [];
    }

    charList() {
        return [{"answer":"g","width":8,"height":9,"bools":[false,true,true,true,true,true,false,true,true,true,false,false,false,true,true,true,true,true,false,false,false,true,true,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false,false,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,false,true,true,true,true,true,true,false]},
                {"answer":"5","width":8,"height":10,"bools":[true,true,true,true,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"W","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,true,true,true,true,true,true,true,true,true,false,false,true,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"O","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"N","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,true,false,false,false,true,true,true,true,true,true,false,false,true,true,true,true,true,true,false,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,false,true,true,true,true,true,true,false,false,false,true,true,true,true,true,false,false,false,true,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"T","width":8,"height":10,"bools":[true,true,true,true,true,true,true,true,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false]},
                {"answer":"q","width":8,"height":9,"bools":[false,false,true,true,true,false,true,true,false,true,true,false,false,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true]},
                {"answer":"l","width":4,"height":10,"bools":[true,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,true,true,true,true]},
                {"answer":"B","width":8,"height":10,"bools":[true,true,true,true,true,true,false,false,true,true,false,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,true,true,true,true,false,false,true,true,false,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,true,true,true,true,false,false]},
                {"answer":"3","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,true,true,false,false,false,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,true,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false,false]},
                {"answer":"s","width":8,"height":7,"bools":[false,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,true,true,true,true,false]},
                {"answer":"p","width":8,"height":9,"bools":[true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,false,false,true,true,false,true,true,false,true,true,true,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false]},
                {"answer":"L","width":7,"height":10,"bools":[true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,true]},
                {"answer":"Z","width":7,"height":10,"bools":[false,true,true,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,true]},
                {"answer":"F","width":8,"height":10,"bools":[true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,true,true,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false]},
                {"answer":"p","width":8,"height":9,"bools":[true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,false,false,true,true,false,true,true,false,true,true,true,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false]},
                {"answer":"T","width":8,"height":10,"bools":[true,true,true,true,true,true,true,true,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false]},
                {"answer":"8","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"P","width":8,"height":10,"bools":[true,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false]},
                {"answer":"J","width":6,"height":10,"bools":[false,false,true,true,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,true,false,false,false,true,true,true,true,false,true,true,false,false,true,true,true,false,false]},
                {"answer":"y","width":8,"height":9,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true,true,false,false,false,false,false,true,true,false,true,true,true,true,true,true,false]},
                {"answer":"r","width":8,"height":7,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"R","width":8,"height":10,"bools":[true,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,false,true,true,true,true,true,false,false,false,true,true,false,false,true,true,false,false,true,true,false,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"M","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,true,false,false,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"d","width":8,"height":10,"bools":[false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,true,true,true,false,true,true,false,true,true,false,false,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true]},
                {"answer":"E","width":7,"height":10,"bools":[true,true,true,true,true,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,true]},
                {"answer":"7","width":8,"height":10,"bools":[true,true,true,true,true,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false]},
                {"answer":"Z","width":7,"height":10,"bools":[true,true,true,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,true]},
                {"answer":"l","width":4,"height":10,"bools":[true,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,true,true,true,true]},
                {"answer":"K","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,false,false,true,true,false,false,true,true,false,true,true,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,false,true,true,false,false,true,true,false,false,true,true,false,false,false,true,true,false,true,true,false,false,false,false,true,true]},
                {"answer":"6","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,false,false,false,false,true,true,true,true,false,false]},
                {"answer":"H","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"5","width":8,"height":10,"bools":[true,true,true,true,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"Y","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false]},
                {"answer":"d","width":8,"height":10,"bools":[false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,true,true,true,false,true,true,false,true,true,false,false,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true]},
                {"answer":"p","width":8,"height":9,"bools":[true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,false,false,true,true,false,true,true,false,true,true,true,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false]},
                {"answer":"z","width":6,"height":7,"bools":[true,true,true,true,true,true,false,false,false,false,true,true,false,false,false,true,true,false,false,false,true,true,false,false,false,true,true,false,false,false,true,true,false,false,false,false,true,true,true,true,true,true]},
                {"answer":"n","width":8,"height":7,"bools":[true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"a","width":8,"height":7,"bools":[false,false,true,true,true,true,true,false,false,true,true,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,true,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,true,false,true,true,true,true,false,true,true]},
                {"answer":"8","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"t","width":8,"height":9,"bools":[false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,true,true,true,true,true,true,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false]},
                {"answer":"q","width":8,"height":9,"bools":[false,false,true,true,true,false,true,true,false,true,true,false,false,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true]},
                {"answer":"a","width":8,"height":7,"bools":[false,false,true,true,true,true,true,false,false,true,true,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,true,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,true,false,true,true,true,true,false,true,true]},
                {"answer":"Z","width":7,"height":10,"bools":[true,true,true,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,true,true]},
                {"answer":"1","width":6,"height":10,"bools":[false,false,true,true,false,false,false,true,true,true,false,false,true,true,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,true,true,true,true,true,true]},
                {"answer":"m","width":8,"height":7,"bools":[true,false,true,true,false,true,true,false,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true]},
                {"answer":"l","width":4,"height":10,"bools":[true,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,true,true,true,true]},
                {"answer":"q","width":8,"height":9,"bools":[false,false,true,true,true,false,true,true,false,true,true,false,false,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true]},
                {"answer":"C","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,true,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"a","width":8,"height":7,"bools":[false,false,true,true,true,true,true,false,false,true,true,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,true,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,true,false,true,true,true,true,false,true,true]},
                {"answer":"2","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,true,true]},
                {"answer":"h","width":8,"height":10,"bools":[true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"F","width":7,"height":10,"bools":[true,true,true,true,true,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,true,true,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false]},
                {"answer":"c","width":8,"height":7,"bools":[false,false,true,true,true,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"P","width":8,"height":10,"bools":[true,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,false,false,false,false,false,false,false]},
                {"answer":"r","width":8,"height":7,"bools":[true,true,false,true,true,true,false,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false]},
                {"answer":"Y","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false]},
                {"answer":"S","width":8,"height":10,"bools":[false,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,true,true,true,true,false]},
                {"answer":"u","width":8,"height":7,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,true,false,false,true,true,true,false,true,true]},
                {"answer":"M","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,true,false,false,true,true,true,true,true,true,true,true,true,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"S","width":8,"height":10,"bools":[false,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,true,true,true,true,false]},
                {"answer":"g","width":8,"height":9,"bools":[false,true,true,true,true,true,false,true,true,true,false,false,false,true,true,true,true,true,false,false,false,true,true,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false,false,true,true,false,false,false,false,false,false,false,true,true,true,true,true,true,false,true,true,false,false,false,false,true,true,false,true,true,true,true,true,true,false]},
                {"answer":"U","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,false,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"k","width":7,"height":10,"bools":[true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,true,true,false,true,true,false,true,true,false,false,true,true,true,true,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,false,false,true,true,false,false,true,true,false,true,true,false,false,false,true,true]},
                {"answer":"4","width":8,"height":10,"bools":[false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,false,false,true,true,false,false,true,true,false,true,true,false,false,false,true,true,false,true,true,true,true,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false]},
                {"answer":"A","width":8,"height":10,"bools":[false,false,false,true,true,false,false,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"b","width":8,"height":10,"bools":[true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,true,true,true,false,false,true,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,true,false,false,true,true,false,true,true,false,true,true,true,false,false]},
                {"answer":"I","width":6,"height":10,"bools":[true,true,true,true,true,true,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,true,true,true,true,true,true]},
                {"answer":"o","width":8,"height":7,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false]},
                {"answer":"i","width":6,"height":10,"bools":[false,false,true,true,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,true,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,true,true,true,true,true,true]},
                {"answer":"C","width":8,"height":10,"bools":[false,false,true,true,true,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,true,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"e","width":8,"height":7,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"w","width":8,"height":7,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,false,true,true,false,true,true,true,true,true,true,true,true,true,true,false,true,true,false,false,true,true,false]},
                {"answer":"f","width":8,"height":10,"bools":[false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,true,true,true,true,true,true,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false]},
                {"answer":"j","width":7,"height":12,"bools":[false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,false,false,false,false,false,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,true,true,true,false]},
                {"answer":"F","width":6,"height":10,"bools":[true,true,true,true,true,true,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,true,true,true,true,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false,true,true,false,false,false,false]},
                {"answer":"x","width":8,"height":7,"bools":[true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true]},
                {"answer":"e","width":8,"height":7,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,true,true,true,false,false,false,false,false,false,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"G","width":8,"height":10,"bools":[false,false,true,true,true,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,true,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"0","width":8,"height":10,"bools":[false,false,false,true,true,false,false,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false]},
                {"answer":"0","width":8,"height":10,"bools":[false,false,false,true,true,false,false,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false]},
                {"answer":"D","width":8,"height":10,"bools":[true,true,true,true,true,true,false,false,true,true,false,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,true,true,false,true,true,true,true,true,true,false,false]},
                {"answer":"e","width":8,"height":7,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,true,true,true,true,true,true,false,true,false,false,false,false,false,false,false,true,true,false,false,false,true,true,false,false,true,true,true,true,true,false]},
                {"answer":"X","width":8,"height":10,"bools":[true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,false,false,false,false,true,true,false,false,false,false,false,false,true,true,false,false,false,false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true]},
                {"answer":"Q","width":8,"height":10,"bools":[false,false,true,true,true,true,false,false,false,true,true,false,false,true,true,false,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,false,false,false,true,true,true,true,false,true,true,false,true,true,true,true,false,false,true,true,true,true,false,true,true,false,false,true,true,false,false,false,true,true,true,true,false,true]}];
    }

    async isReady() {
        return wait().then( () => {
            let img = document.querySelector(this.selector);
            if(img && img.complete) {
                this._imgProcessor = new ImageProcessor(img);
                return Promise.resolve(true);
            }
            return this.isReady();
        });
    }

    async isSolved() {
        return this.isReady()
            .then( () => this.solve())
            .then( (solution) => {
            document.querySelector('input[name="kodecaptcha"]').value = solution;
            return Promise.resolve(true);
        })
            .catch(err => {
            return Promise.reject(`Error ${err}`);
        });
    }

    preProcessImage() {
        this._imgProcessor.toCanvas();
        this._imgProcessor.binarize(200);
        this._imgProcessor.invert();
    }

    cropCharacter(startFrom = 0) {
        let imgData = this._imgProcessor.getDrawer().ctx.getImageData(startFrom, 0, this._imgProcessor.getDrawer().canvas.width - startFrom, this._imgProcessor.getDrawer().canvas.height);
        let newBounds = { left: null, right:null, top: null, bottom: null };
        let readingCharacter = false;
        let endOfCharacter = null;

        for (var x = 0; x < imgData.width; x++) {
            if (endOfCharacter) {
                newBounds.right = endOfCharacter;
                break;
            }

            let isColumnEmpty = true;
            for (var y = 0; y < imgData.height; y++) {
                var i = x * 4 + y * 4 * imgData.width;
                var pixel = { r: imgData.data[i + 0], g: imgData.data[i + 1], b: imgData.data[i + 2] };

                if (pixel.r + pixel.g + pixel.b == 0) {
                    if (newBounds.left == null || newBounds.left > x) {
                        newBounds.left = x;
                    }
                    if (newBounds.right == null || newBounds.right < x) {
                        newBounds.right = x;
                    }

                    if (newBounds.top == null || newBounds.top > y) {
                        newBounds.top = y;
                    }

                    if (newBounds.bottom == null || newBounds.bottom < y) {
                        newBounds.bottom = y;
                    }
                    readingCharacter = true;
                    isColumnEmpty = false;
                }
            }

            if (isColumnEmpty && readingCharacter) {
                endOfCharacter = x - 1;
                break;
            }
        }

        return {
            x: startFrom + newBounds.left,
            y: newBounds.top,
            width: newBounds.right - newBounds.left + 1,
            height: newBounds.bottom - newBounds.top + 1,
            nextBegins: startFrom + newBounds.right + 1
        };
    }

    splitInCharacters() {
        let chars = [];
        let i =0;
        do {
            chars.push(this.cropCharacter( i== 0 ? 0 : chars[i-1].nextBegins ) );
            let copy = document.createElement('canvas').getContext('2d');
            copy.canvas.width = chars[i].width;
            copy.canvas.height = chars[i].height;

            let trimmedData = this._imgProcessor.getDrawer().ctx.getImageData(chars[i].x, chars[i].y, chars[i].width, chars[i].height);
            copy.putImageData(trimmedData, 0, 0);

            chars[i].bools = this._imgProcessor.imgDataToBool(trimmedData);
            chars[i].dataUrl = copy.canvas.toDataURL("image/png");

            i++;
        } while(i < 5);

        this._characters = chars;
    }

    guess(charElm) {
        let bestGuess = {
            answer: '',
            blacksMatched: 0,
            blacksMissed: 0,
            percentageBlacks: 0,
            exactMatch: false
        };

        let totalPixels = charElm.width * charElm.height;
        let totalBlacks = charElm.bools.filter(x => x === true).length;
        this.charList().filter(x => x.answer != '').forEach( function (elm) {
            if (bestGuess.exactMatch) {
                return;
            }
            if (charElm.width == elm.width && charElm.height == elm.height) {
                if (charElm.bools.join(',') == elm.bools.join(',')) {
                    bestGuess = {
                        answer: elm.answer,
                        percentageBlacks: 100,
                        exactMatch: true
                    };
                    return;
                }

                let blacksMatched = 0;
                let blacksMissed = 0;
                let percentageBlacks = 0;
                for (let p = 0; p < totalPixels; p++) {
                    if (charElm.bools[p] === true || elm.bools[p] === true) {
                        if (elm.bools[p] == charElm.bools[p]) {
                            blacksMatched++;
                        } else {
                            blacksMissed++;
                        }
                    }
                }

                if (blacksMatched != 0 || blacksMissed != 0) {
                    percentageBlacks = blacksMatched/(blacksMatched + blacksMissed);
                }

                if (percentageBlacks > bestGuess.percentageBlacks) {
                    bestGuess = {
                        answer: elm.answer,
                        blacksMatched: blacksMatched,
                        blacksMissed: blacksMissed,
                        percentageBlacks: percentageBlacks
                    };
                }
            }
        });
        return bestGuess;
    }

    async solve() {
        let solution = '';
        if(this._imgProcessor.isImageComplete()) {
            this.preProcessImage();
            this.splitInCharacters();

            this._characters.forEach( ch => {
                let bestGuess = this.guess(ch);
                solution += bestGuess.answer;
            });
        }
        return Promise.resolve(solution);
    }
}

class NoCaptchaWidget extends CaptchaWidget {
    constructor(params) {
        let defaultParams = {
            selector: 'svg.feather-check-circle',
            waitMs: 10000
        };
        for (let p in params) {
            defaultParams[p] = params[p];
        }
        super(defaultParams);
    }

    async isSolved() {
        return wait().then( () => {
            if (this.isUserFriendly) {
                return Promise.resolve(true);
            }
            return this.isSolved();
        });
    }
}

class CBL01CaptchaWidget extends CaptchaWidget {
    constructor(params) {
        let defaultParams = {
            selector: '',
            waitMs: 2000
        };
        for (let p in params) {
            defaultParams[p] = params[p];
        }
        super(defaultParams);
    }

    async isReady() {
        return wait(1).then( () => {
            if(this.isUserFriendly) {
                return Promise.resolve(true);
            }
            return wait().then( () => { this.isReady(); });
        });
    }

    async solve() {
        let answer = document.getElementById('captchainput').value;
        if (answer != '') {
            if (answer.startsWith('JJJ')) {
                answer = answer.slice(3);
                document.getElementById('captchainput').value = answer;
            }

            if (answer.length != 6) {
                document.getElementById('captchainput').value ='';
                window.location.reload();
                return wait(10000).then( () => { this.solve(); });
            } else {
                return wait().then( () => { return true; } );
            }
        } else {
            return wait().then( () => { this.solve(); });
        }
    }

    async isSolved() {
        return this.isReady()
            .then( () => this.solve())
            .then( (solution) => {
            return Promise.resolve(true);
        })
            .catch(err => { shared.devlog(err); })
    }
}

class D1CaptchaWidget extends CaptchaWidget {
    constructor() {
        let defaultParams = {
            selector: '#submit_captcha span',
            waitMs: [1000, 5000],
            timeoutMs: 4 * 60 * 1000
        };
        super(defaultParams);
        this.selectors = {
            submitButton: '#submit',
            answerSpan: '#submit_captcha span'
        }
        this._elements = {
            submitButton: new ButtonWidget({selector: '#submit'}),
            answerSpan: new ReadableWidget({selector: '#submit_captcha span'})
        };
    }

    async isReady() {
        return wait().then( () => {
            if(this._elements.submitButton.isUserFriendly) {
                return Promise.resolve(true);
            }
            return this.isReady();
        });
    }

    async solve() {
        if (this._elements.answerSpan.isUserFriendly) {
            let answer = this._elements.answerSpan.value;
            answer = answer ? answer.trim() : answer;
            let input = document.querySelector(`input[value="${answer}"`);
            if (input) {
                helpers.alternativeClick(input.parentElement.querySelector('i'));
                return wait().then( () => { return true; } );
            } else {
                return Promise.reject(`@D1Captcha input NOT FOUND for answer: ${answer}`);
            }
        } else {
            return Promise.reject('Answer span not found!!!');
        }
    }

    async isSolved() {
        return this.isReady()
            .then( () => this.solve())
            .then( (solution) => {
            return Promise.resolve(true);
        })
            .catch(err => { shared.devlog(err); })
    }
}

class Faucet {
    constructor(elements, actions = {}) {
        this._url = window.location.href;
        this._timeout = new Timeout(this.maxSeconds);
        this._elements = elements;
        this._actions = {
            preRun: false,
            preRoll: false,
            altValidation: false,
            readClaimed: true,
            readBalance: true,
            readTimeLeft: true,
            readRolledNumber: false,
            isMultiClaim: false,
            checkIfOutOfFunds: false,
            preSaveResult: false
        }
        this._actions = { ...this._actions, ...actions };
        this._params = shared.getCurrent().params || {};
        this._result = this._actions.isMultiClaim ? (shared.getProp('tempResults') || {}) : (shared.getResult() || {});
    }

    checkCloudflareError() {
    }

    useUrlListener() {
        if (window.onurlchange === null) {
            window.addEventListener('urlchange', (data) => {
                if (this._url != window.location.href) {
                    this._url = window.location.href;
                    this.resetRun();
                }
            });
        }
    }

    resetRun() {
        wait().then( () => { this.init(); });
    }

    init() {
        throw new Error('Init not implemented!');
    }

    login() {
        throw new Error('Login not implemented!'); //return NEED_TO_LOGIN
    }

    async run(action = false) {
        if (this._actions.checkIfOutOfFunds) {
            this.checkIfOutOfFunds();
        }

        if (this._actions.preRun) {
            await wait().then( () => { this.preRun() } );;
        }

        if (!action) {
            this.detectAction().then( (resolve) => {
                this.perform(resolve.action);
            });
        } else {
            this.perform(action);
        }
    }

    perform(action) {
        switch(action) {
            case 'doRoll':
                if(this._actions.preRoll) {
                    this.preRoll();
                }
                this._elements.captcha.isSolved().then(() => { this.clickRoll() });
                break;
            case 'needToWait':
                this.updateResult();
                break;
            default:
                break;
        }
    }

    async detectAction() {
        return wait().then( () => {
            if ( this.isCountdownVisible() ) {
                return Promise.resolve({action: 'needToWait'});
            } else if ( this.isRollButtonVisible() ) {
                return Promise.resolve({action: 'doRoll'});
            } else {
                return this.detectAction();
            }
        });
    }

    preRoll() {
        throw new Error('PreRoll not implemented!');
    }

    preRun() {
        throw new Error('PreRun not implemented!');
    }

    altValidation() {
        throw new Error('AltValidation not implemented!');
    }

    isCountdownVisible() {
        return this._elements.countdownMinutes && this._elements.countdownMinutes.isUserFriendly;
    }

    isRollButtonVisible() {
        return this._elements.rollButton && this._elements.rollButton.isUserFriendly;
    }

    clickRoll() {
        try {
            this._elements.rollButton.click();
            this.validateRun();
        } catch (err) {
            shared.closeWithError(K.ErrorType.CLICK_ROLL_ERROR, err);
        }
    }

    failureValidation() {
        throw new Error('FailureValidation not implemented!');
    }

    async validateRun() {
        return wait(this._actions.useFailureValidation ? 6000 : null).then( () => {
            if (this._actions.useFailureValidation) {
                if (this.failureValidation()) {
                    return;
                }
            }
            if (this._elements.success.isUserFriendly) {
                return this.updateResult();
            } else if(this._actions.altValidation) {
                if(this.altValidation()) {
                    return this.updateResult();
                }
            }
            return wait(2000).then( () => { this.validateRun() });
        });
    }

    async updateResult() {
        if(this._actions.readClaimed) {
            this._result.claimed = this.readClaimed();
        }
        if(this._actions.readBalance) {
            this._result.balance = this.readBalance();
        }
        if(this._actions.readTimeLeft) {
            this._result.nextRoll = this.readNextRoll();
        }
        if(this._actions.readRolledNumber) {
            this._result.rolledNumber = this.readRolledNumber();
        }
        if (this._actions.isMultiClaim) {
            shared.setProp('tempResults', this._result);
            return this._actions.postRun ? this.postRun() : true;
        }
        if (this._actions.preSaveResult) {
            this.preSaveResult();
        }
        if (this._actions.updateWithoutClosing) {
            shared.updateWithoutClosing(this._result);
            return this._actions.postRun ? this.postRun() : true;
        } else {
            shared.closeWindow(this._result);
        }
    }

    readNextRoll() {
        try {
            if (this._elements.countdownMinutes && this._elements.countdownMinutes.isUserFriendly) {
                return helpers.addMinutes(this._elements.countdownMinutes.timeLeft);
            }
        } catch (err) { shared.devlog(`@readNextRoll: ${err}`); }
        return null;
    }

    readRolledNumber() {
        let rolled = 0;
        try {
            if(this._elements.rolledNumber.isUserFriendly) {
                rolled = this._elements.rolledNumber.value;
            }
        } catch (err) { shared.devlog(`@readRolledNumber: ${err}`); }
        return rolled;
    }

    readBalance() {
        let balance = 0;
        try {
            if(this._elements.balance.isUserFriendly) {
                balance = this._elements.balance.value;
            }
        } catch (err) { shared.devlog(`@readBalance: ${err}`); }
        return balance;
    }

    readClaimed() { //TODO: review if previous claimed should be received as arg
        let claimed = this._result.claimed ?? 0;
        if (this._actions.isMultiClaim) {
            this._oldClaimed = claimed;
        } else {
        }

        try {
            if(this._elements.claimed.isUserFriendly) {
                claimed = +claimed + +this._elements.claimed.value;
            } else {
            }
        } catch (err) { shared.devlog(`@readClaimed: ${err}`); }
        return claimed;
    }

    checkIfOutOfFunds() {
        let divAlerts = [...document.querySelectorAll(this._elements.outOfFundsDivSelector)];
        divAlerts.forEach( function (d) {
            if (d.innerText.toLowerCase().includes('not have sufficient funds')) {
                shared.closeWithError(K.ErrorType.FAUCET_EMPTY, d.innerText);
                return;
            }
        });
    }
}

class BFRoll extends Faucet {
    constructor(coinPrefix, trySpin = false) {
        let elements = {
            preRunButton: new ButtonWidget({selector: '.free-box.free-box__' + coinPrefix + ' button'}), //'#' + coinPrefix + '_free_box_withdraw_page'}),
            captcha: new NoCaptchaWidget({ selector: '.free-box-withdraw__footer .button_red.button_center.button_fullwidth' }),
            rollButton: new ButtonWidget({selector: '.free-box-withdraw__footer .button_red.button_center.button_fullwidth'}),
            success: new ReadableWidget({selector: '.modal:not(.free-box-withdraw,fury-wheel-modal), .vue-notification-template.my-notify.success'}),
            claimed: new ReadableWidget({selector: '.free-box.free-box__' + coinPrefix, parser: Parsers.bfBoxClaimed}),
            progressBar: new ReadableWidget({selector: '.free-box.free-box__' + coinPrefix + ' .free-box__progress-bar progress'}),
        };

        let actions = {
            preRun: true,
            readClaimed: true,
            readBalance: false,
            readRolledNumber: false
        };
        super(elements, actions);
        this.coinPrefix = coinPrefix;
        this.trySpin = trySpin;
    }

    init() {
        if (this._url.includes('https://betfury.io/boxes/all')) {
            this.run();
            return;
        } else {
            return;
        }
    }

    async spin() {
        let clickables = document.querySelectorAll('.fury-wheel__wheel-btn, .fury-wheel__btn-wrap, .fury-wheel__btn-content, .fury-wheel__btn-img');
        if (clickables.length > 0) {
            clickables[Math.floor(Math.random()*clickables.length)].click();
            wait(15000).then ( () => { shared.closeWindow(this._result); } );
        }
        return;
    }

    async preRun() {
        return wait().then( () => {
            try {
                let popup = document.querySelector('.modal-wrapper .modal:not(.free-box-withdraw,fury-wheel-modal) .modal__btn-close');
                if (popup) {
                    popup.click();
                    popup.click(); // twice
                }
            } catch (err) {}

            if (this.trySpin) {
                let spinUnavailable = document.querySelector('.bonus.bonus_furywheel.wait');
                if (spinUnavailable) {
                } else {
                    let spinBtn = document.querySelector('.wheel-amin'); //bonus bonus_furywheel wait
                    if (spinBtn) {
                        spinBtn.click();
                        wait(10000).then ( () => { this.spin() } );
                        return wait(60000).then ( () => { this.preRun(); } );
                    }
                }
            }

            if (!this._elements.progressBar || !this._elements.progressBar.isUserFriendly) {
                return this.preRun();
            }

            if (this._elements.preRunButton.isUserFriendly) {
                if (!this._elements.preRunButton.isUserFriendly.disabled) {
                    return this._elements.preRunButton.click();
                } else {
                    this._timeout.restart();
                    shared.closeWindow(this._result);
                    return;
                }
            } else if (document.querySelectorAll('.free-box').length > 1) {
                shared.closeWithError(K.ErrorType.ERROR, 'Box might not exist for your account.');
                return;
            }
            return this.preRun();
        });
    }

    async validateRun() {
        return wait(7000).then( () => {
            let gtHook = document.querySelector('div.geetest_slice_bg');
            if (gtHook) {
                if (gtHook.isUserFriendly()) {
                    return this.validateRun();
                }
            }
            let popup = document.querySelector('.modal-wrapper .modal:not(.free-box-withdraw,fury-wheel-modal) .modal__btn-close');
            if (!popup) {
                if (this._elements.preRunButton.isUserFriendly && !this._elements.preRunButton.isUserFriendly.disabled) {
                    this._elements.preRunButton.click();
                    return this.validateRun();
                }
            } else {
                try {
                    if (popup) {
                        popup.click();
                        popup.click();
                    }
                } catch (err) {}
            }

            if (this._elements.success.isUserFriendly) {
                return this.updateResult();
            } else if(this._actions.altValidation) {
                if(this.altValidation()) {
                    return this.updateResult();
                }
            }
            return this.validateRun();
        });
    }
}

class DutchyRoll extends Faucet {
    constructor() {
        let elements = {
            countdownMinutes: new CountdownWidget({selector: '#timer', parser: Parsers.splitAndIdxToInt, options: { splitter: 'Minutes', idx: 0} }), // "26 Minutes 23"
            captcha: new HCaptchaWidget(),
            rollButton: new ButtonWidget({selector: '#claim'}), //w/booster video: '#unlockbutton' & then #claim_boosted
            success: new ReadableWidget({selector: '.card.green.pulse p,.card.blue.pulse,.card.green.animated,.card.green.pulse'}),
            claimed: new ReadableWidget({selector: '.card.green.pulse p,.card.blue.pulse,.card.green.animated,.card.green.pulse', parser: Parsers.freeEthereumIoClaimed}) //"You Won 0.00409070 TRX + 20 XP"
        };
        let actions = {
            preRun: true,
            readClaimed: true,
            readBalance: false,
            readRolledNumber: false
        };
        super(elements, actions);
    }

    init() {
        switch(window.location.host) {
            case 'autofaucet.dutchycorp.space':
                if (this._url.includes('/roll.php')) {
                    this._elements.claimed = new ReadableWidget({selector: '.card.green.pulse p,.card.blue.pulse,.card.green.animated,.card.green.pulse', parser: Parsers.dutchysClaimed})
                } else if (this._url.includes('/login.php')) {
                    shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, '');
                    return;
                }
                break;
            case 'express.dutchycorp.space':
                if (this._url.includes('/roll.php')) {
                    this._elements.claimed = new ReadableWidget({selector: '.card.green.pulse p,.card.blue.pulse,.card.green.animated,.card.green.pulse', parser: Parsers.dutchysClaimed})
                } else if (this._url.includes('/coin_roll.php')) {
                    this._elements.claimed = new ReadableWidget({selector: '.card.green.pulse p,.card.blue.pulse,.card.green.animated,.card.green.pulse', parser: Parsers.dutchysClaimedToFloat})
                } else if (this._url.includes('/index.php')) {
                    shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, 'You need to login using ExpressCrypto (EC-UserId-XXXXXX).');
                    return;
                }
                break;
        }
        this.run();
        return;
    }

    async preRun() {
        if (this._elements.captcha.isUserFriendly) {
            if (shared.getConfig()['dutchy.useBoosted']) {
                this._elements.rollButton = new ButtonWidget({selector: '#unlockbutton'});
                this._elements.confirmBoost = new ButtonWidget({selector: '#claim_boosted'});
                setInterval(() => {
                    try {
                        if (this._elements.confirmBoost.isUserFriendly) {
                            this._elements.confirmBoost.click();
                        }
                    } catch (err) {}
                }, 8000);
            }
            return true;
        } else {
            return wait().preRun();
        }
    }
}

class YCoin extends Faucet {
    constructor() {
        let elements = {
            rollButton: new ButtonWidget({selector: 'input[type="submit"][value="Get Free Crypto!"]'}),
            claimed: new ReadableWidget({selector: 'div.alert.alert-info', parser: Parsers.freeEthereumIoClaimed}),
            captcha: new HCaptchaWidget(),
            balance: new ReadableWidget({selector: 'a.wha[href="/account?page=history"]', parser: Parsers.trimNaNs}),
            success: new ReadableWidget({selector: 'div.alert.alert-info'}),
            login: {
                inputUser: new TextboxWidget({ selector: 'input[name="number"]' }),
                inputPass: new TextboxWidget({ selector: 'input[name="pass"]' }),
                inputSubmit: new SubmitWidget({ selector: 'input[type="submit"][value="Login!"]' }),
                setCredentials: false
            },
        };

        if(shared.getConfig()['ycoin.credentials.mode'] == 1) {
            elements.login.setCredentials = {
                username: shared.getConfig()['ycoin.credentials.username'],
                password: shared.getConfig()['ycoin.credentials.password']
            };
        }

        let actions = {
            preRun: true,
            readClaimed: true,
            readBalance: true,
            readRolledNumber: false,
            checkIfOutOfFunds: false
        };
        super(elements, actions);
    }

    async preRun() {
        let msgDiv;
        msgDiv = document.querySelector('p.info.success');
        if (msgDiv && msgDiv.innerText.includes('has been transferred')) {
            let result = {};
            if (msgDiv.innerText.includes('0 claims')) {
                result.nextRoll = helpers.addMinutes(60 * 24 + helpers.randomInt(10, 50));
            } else {
                result.nextRoll = helpers.addMinutes('60');
            }
            result.claimed = +msgDiv.innerText.split(' ')[0];
            result.balance = this.readBalance();
            shared.closeWindow(result);
            return;
        }

        msgDiv = document.querySelector('p.info.warn');
        if (msgDiv) {
            if (msgDiv.innerText.includes('can claim only')) {
                let result = {};
                result.nextRoll = helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
                shared.closeWindow(result);
                return;
            } else if (msgDiv.innerText.includes('Please wait')) {
                let result = {};
                try {
                    let unit = msgDiv.innerText.includes(' seconds') ? ' seconds' : ' minutes';
                    let val = msgDiv.innerText.split('Please wait ')[1].replace(/\D/g, '');
                    if (unit == ' seconds') {
                        result.nextRoll = helpers.addSeconds(val);
                    } else {
                        result.nextRoll = helpers.addMinutes(val);
                    }
                } catch {
                    result.nextRoll = helpers.addMinutes(60);
                }
                shared.closeWindow(result);
                return;
            }
        }

        if (this._elements.captcha.isUserFriendly) {
        } else {
            if (this._elements.rollButton) {
                this._elements.rollButton.click();
                return;
            }
        }
    }

    async init() {
        if (this._url.includes('/faucet')) {
            let needToLoginButton = document.querySelector('input[type="submit"][value="Login / Signup"]');
            if (needToLoginButton) {
                needToLoginButton.click();
                return;
            }

            this.run();
            return;
        } else if (this._url.includes('/account')) {
            this.doLogin();
            return;
        }
    }

    async doLogin() {
        return wait().then( () => {
            let container = document.querySelector('#cc');
            if (container.innerText.includes('You are now logged in as account')) {
                let toFaucetButton = document.querySelector('#mmenu a[href="/faucet"]');
                if (toFaucetButton) {
                    toFaucetButton.click();
                    return;
                }
                return this.doLogin();
            }
            if (!this._elements.login.inputUser.isUserFriendly || !this._elements.login.inputPass.isUserFriendly || !this._elements.login.inputSubmit.isUserFriendly) {
                return this.doLogin();
            }

            let loginErrorDiv = document.querySelector('#cc .info.fail');
            if (loginErrorDiv && loginErrorDiv.innerText.includes('Invalid')) {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, loginErrorDiv.innerText);
                return;
            }

            if (this._elements.login.setCredentials != false) {
                this._elements.login.inputUser.value = this._elements.login.setCredentials.username;
                this._elements.login.inputPass.value = this._elements.login.setCredentials.password;
            }

            try {
                this._elements.login.rememberMe.isUserFriendly.checked = true;
            } catch (err) {}

            if (this._elements.login.inputUser.value != '' && this._elements.login.inputPass.value != '' ) {
                this._elements.login.inputSubmit.click();
            } else {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, 'No credentials were provided');
                return;
            }
        });
    }

}

class CDiversity extends Faucet {
    constructor() {
        let elements = {
            claimed: new ReadableWidget({selector: 'p.success', parser: Parsers.trimNaNs}),
            captcha: new HCaptchaWidget(),
            rollButton: new ButtonWidget({selector: 'input[type="submit"][value="Get Free Crypto!"]'}),
        };
        let actions = {
            readTimeLeft: true,
            readRolledNumber: false,
            readBalance: false
        };
        super(elements, actions);
    }

    init() {
        if(this.hasErrorMessage()) {
            shared.closeWithError(K.ErrorType.ERROR, 'Suspicious Activity Message Displayed');
            return;
        }

        let claimed = this.readClaimed();
        if (claimed != 0) {
            let result = {
                claimed: claimed,
                nextRoll: this.readNextRoll()
            };
            shared.closeWindow(result);
            return;
        }

        let nextRoll = this.readNextRoll();
        if(nextRoll) {
            let result = {
                nextRoll: nextRoll
            };
            shared.closeWindow(result);
            return;
        }

        this.solve();
    }

    hasErrorMessage() {
        return document.body.innerText.toLowerCase().includes('suspicious activity');
    }

    isFirstStep() {
        return document.querySelector('form select[name="coin"]') ? true : false;
    }

    async doFirstStep() {
        let form = document.querySelector('form');
        if (!form) {
            this.updateResult();
            return;
        }
        let coinSelect = form.querySelector('select[name="coin"]');
        if (!coinSelect) {
            this.updateResult();
            return;
        }
        let userInput = form.querySelector('input[name="ado"]');
        if (!userInput) {
            this.updateResult();
            return;
        }
        let submitButton = form.querySelector('input[type="submit"]');
        if (!submitButton) {
            this.updateResult();
            return;
        }
        coinSelect.value = this.getCoin();
        userInput.value = this._params.address;

        submitButton.parentElement.submit();
        return;
    }

    getCoin() {
        try {
            let tds = document.querySelectorAll('table tr td:nth-child(2)');
            return tds[helpers.randomInt(0, 5)].innerText.split(' ')[1]
        } catch (err) {
            return 'BTC';
        }
    }

    isSecondStep() {
        let ps = [...document.querySelectorAll('p')];
        return ps.findIndex(x => x.innerText.toLowerCase().includes('one more step...')) >= 0;
    }

    async solve() {
        if (this.isSecondStep()) {
            return this.run();
        }
        if (this.isFirstStep()) {
            return this.doFirstStep();
        }
    }

    isCountdownVisible() {
        let successDiv = document.querySelector('p.success');
        if (!successDiv) {
            return false;
        }
        if (successDiv.innerText.includes('0 claims')) {
            return true;
        }

        return false;
    }

    readClaimed() {
        let successDiv = document.querySelector('p.success');
        if (successDiv) {
            return successDiv.innerText.split(' ')[0];
        } else {
            return 0;
        }
    }

    readNextRoll() {
        try {
            let successDiv = document.querySelector('p.success');
            if (successDiv && successDiv.innerText.includes('You have')) {
                let claimsLeft;
                try {
                    claimsLeft = successDiv.innerText.split(' claims')[0].split('have ')[1];
                } catch (err) {}
                if (claimsLeft) {
                    return helpers.addMinutes(helpers.randomInt(6, 22));
                } else if (claimsLeft === '0') {
                    return helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
                }
            }
        } catch (err) { }

        try {
            let warnDiv = document.querySelector('p.warn');
            if (warnDiv) {
                if (warnDiv.innerText.includes('You can claim only')) {
                    return helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
                }

                if (warnDiv.innerText.includes('Please wait ')) {
                    try {
                        let unit = warnDiv.innerText.includes(' seconds') ? ' seconds' : ' minutes';
                        let val = warnDiv.innerText.split('Please wait ')[1].split(unit)[0].replace(/\D/g, '');
                        if (unit == ' seconds') {
                            return helpers.addSeconds(val);
                        } else {
                            return helpers.addMinutes(val);
                        }
                    } catch { }
                    let claimsLeft;
                    try {
                        claimsLeft = warnDiv.innerText.split(' seconds')[0].split('wait ')[1];
                    } catch (err) {}
                    if (claimsLeft) {
                        return helpers.addMinutes(helpers.randomInt(6, 22));
                    }
                }
            }

        } catch (err) { }
        return null;
    }
}

class CTop extends Faucet {
    constructor() {
        let elements = {
            claimed: new ReadableWidget({selector: 'p.success', parser: Parsers.trimNaNs}),
            captcha: new HCaptchaWidget(),
            rollButton: new ButtonWidget({selector: 'input[type="submit"]'}),
        };
        let actions = {
            readTimeLeft: true,
            readRolledNumber: false,
            readBalance: false
        };
        super(elements, actions);
    }

    init() {
        if(this.hasErrorMessage()) {
            shared.closeWithError(K.ErrorType.ERROR, 'Suspicious Activity Message Displayed');
            return;
        }

        let claimed = this.readClaimed();
        if (claimed != 0) {
            let result = {
                claimed: claimed,
                nextRoll: this.readNextRoll()
            };
            shared.closeWindow(result);
            return;
        }

        let nextRoll = this.readNextRoll();
        if(nextRoll) {
            let result = {
                nextRoll: nextRoll
            };
            shared.closeWindow(result);
            return;
        }

        this.solve();
    }

    hasErrorMessage() {
        return document.body.innerText.toLowerCase().includes('suspicious activity');
    }

    isFirstStep() {
        return document.querySelector('form input[name="adr"]') ? true : false;
    }

    async doFirstStep() {
        let form = document.querySelector('form');
        if (!form) {
            this.updateResult();
            return;
        }
        let userInput = form.querySelector('input[name="adr"]');
        if (!userInput) {
            this.updateResult();
            return;
        }
        let submitButton = form.querySelector('input[type="submit"]');
        if (!submitButton) {
            this.updateResult();
            return;
        }
        userInput.value = this._params.address;

        submitButton.closest('form').submit();
        return;
    }

    isSecondStep() {
        let ps = [...document.querySelectorAll('p')];
        return ps.findIndex(x => x.innerText.toLowerCase().includes('one more step...')) >= 0;
    }

    async solve() {
        if (this.isSecondStep()) {
            return this.run();
        }
        if (this.isFirstStep()) {
            return this.doFirstStep();
        }
    }

    isCountdownVisible() {
        let successDiv = document.querySelector('p.success');
        if (!successDiv) {
            return false;
        }
        if (successDiv.innerText.includes('0 claims')) {
            return true;
        }

        return false;
    }

    readClaimed() {
        let successDiv = document.querySelector('p.success');
        if (successDiv) {
            return successDiv.innerText.split(' ')[0];
        } else {
            return 0;
        }
    }

    readNextRoll() {
        try {
            let successDiv = document.querySelector('p.success');
            if (successDiv && successDiv.innerText.includes('You have')) {
                let claimsLeft;
                try {
                    claimsLeft = successDiv.innerText.split(' claims')[0].split('have ')[1];
                } catch (err) {}
                if (claimsLeft) {
                    return helpers.addMinutes(helpers.randomInt(6, 22));
                } else if (claimsLeft === '0') {
                    return helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
                }
            }
        } catch (err) { }

        try {
            let warnDiv = document.querySelector('p.warn');
            if (warnDiv) {
                if (warnDiv.innerText.includes('You can claim only')) {
                    return helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
                }

                if (warnDiv.innerText.includes('Please wait ')) {
                    try {
                        let unit = warnDiv.innerText.includes(' seconds') ? ' seconds' : ' minutes';
                        let val = warnDiv.innerText.split('Please wait ')[1].split(unit)[0].replace(/\D/g, '');
                        if (unit == ' seconds') {
                            return helpers.addSeconds(val);
                        } else {
                            return helpers.addMinutes(val);
                        }
                    } catch { }
                    let claimsLeft;
                    try {
                        claimsLeft = warnDiv.innerText.split(' seconds')[0].split('wait ')[1];
                    } catch (err) {}
                    if (claimsLeft) {
                        return helpers.addMinutes(helpers.randomInt(6, 22));
                    }
                }
            }

        } catch (err) { }
        return null;
    }
}

class BscAds extends Faucet {
    constructor() {
        let elements = {
            rollButton: new ButtonWidget({selector: 'button.btn.btn-primary.btn-lg'}),
            claimed: new ReadableWidget({selector: 'div.alert.alert-success', parser: Parsers.trimNaNs}),
            captcha: new HCaptchaWidget(),
            countdownMinutes: new CountdownWidget({selector: '#faucet_timer', parser: Parsers.fromTextTimer }), // 0 hours 15 minutes 36 seconds
            success: new ReadableWidget({selector: 'div.alert.alert-success'}),
            login: {
                inputUser: new TextboxWidget({ selector: 'input[name="username"]' }),
                inputPass: new TextboxWidget({ selector: 'input[name="password"]' }),
                inputSubmit: new ButtonWidget({ selector: 'button.btn' }),
                setCredantials: false
            }
        }

        if(shared.getConfig()['bscads.credentials.mode'] == 1) {
            elements.login.setCredentials = {
                username: shared.getConfig()['bscads.credentials.username'],
                password: shared.getConfig()['bscads.credentials.password']
            };
        }

        let actions = {
            readClaimed: true,
            readBalance: false,
            readRolledNumber: false
        };
        super(elements, actions);
    }

    init() {
        if (this._url.includes('/faucet/access')) {
            this.run();
            return;
        } else if (this._url.includes('/faucet')) {
            this.doPrePostFaucet();
            return;
        } else if (this._url.includes('/login')) {
            this.doLogin();
            return;
        } else {
            location.replace('faucet');
            return;
        }
    }

    async doPrePostFaucet() {
        return wait(10000).then( () => {
            let button = document.querySelector('button.btn.btn-primary.btn-lg');
            if (button) {
                button.click();
                return;
            }
            if (!button) {
                return this.run();

            }
        });
    }

    async doLogin() {
        if (document.body.innerText.toLowerCase().includes('please wait during')) {
            return wait(8000).then( () => {
                location.replace('faucet');
            });
        }
        return wait().then( () => {
            if (!this._elements.login.inputUser.isUserFriendly || !this._elements.login.inputPass.isUserFriendly || !this._elements.login.inputSubmit.isUserFriendly) {
                return this.doLogin();
            }

            let loginErrorDiv = document.querySelector('div.alert.alert-danger');
            if (loginErrorDiv && loginErrorDiv.innerText.toLowerCase().includes('invalid')) {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, loginErrorDiv.innerText);
                return;
            }

            if (this._elements.login.setCredentials != false) {
                this._elements.login.inputUser.value = this._elements.login.setCredentials.username;
                this._elements.login.inputPass.value = this._elements.login.setCredentials.password;
            }

            try {
                this._elements.login.rememberMe.isUserFriendly.checked = true;
            } catch (err) {}

            if (this._elements.login.inputUser.value != '' && this._elements.login.inputPass.value != '' ) {
                this._elements.captcha.isSolved().then(() => {
                    this._elements.login.inputSubmit.click();
                    return;
                });
            } else {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, 'No credentials were provided');
                return;
            }
        });
    }

    async preRun() {
    }
}

class FPB extends Faucet {
    constructor(sitePrefix = null) {
        let elements = {
            rollButton: new ButtonWidget({selector: 'input[type="submit"][value="Claim From Faucet"],input[type="submit"][name="claim"]'}),
            claimed: new ReadableWidget({selector: 'div.alert.alert-info', parser: Parsers.freeEthereumIoClaimed}),
            captcha: new HCaptchaWidget(),
            success: new ReadableWidget({selector: 'div.alert.alert-info'}),
            login: {
                inputUser: new TextboxWidget({ selector: 'input[name="user_name"]' }),
                inputPass: new TextboxWidget({ selector: 'input[name="password"]' }),
                rememberMe: new TextboxWidget({ selector: 'input[name="remember_me"]' }),
                inputSubmit: new ButtonWidget({ selector: 'input[type="submit"][name="login"]' }),
                setCredentials: false
            },
            outOfFundsDivSelector: '.alert.alert-info'
        };

        if(shared.getConfig()[sitePrefix + '.credentials.mode'] == 1) {
            elements.login.setCredentials = {
                username: shared.getConfig()[sitePrefix + '.credentials.username'],
                password: shared.getConfig()[sitePrefix + '.credentials.password']
            };
        }

        let actions = {
            readClaimed: true,
            readBalance: false,
            readRolledNumber: false,
            checkIfOutOfFunds: true
        };
        super(elements, actions);
    }

    init() {
        if (this._url.includes('/dashboard')) {
            this.run();
            return;
        } else if (this._url.includes('/login')) {
            this.doLogin();
            return;
        }
    }

    async doLogin() {
        return wait().then( () => {
            if (!this._elements.login.inputUser.isUserFriendly || !this._elements.login.inputPass.isUserFriendly || !this._elements.login.inputSubmit.isUserFriendly) {
                return this.doLogin();
            }

            let loginErrorDiv = document.querySelector('div.alert.alert-info');
            if (loginErrorDiv && loginErrorDiv.innerText.includes('not valid')) {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, loginErrorDiv.innerText);
                return;
            }

            if (this._elements.login.setCredentials != false) {
                this._elements.login.inputUser.value = this._elements.login.setCredentials.username;
                this._elements.login.inputPass.value = this._elements.login.setCredentials.password;
            }

            try {
                this._elements.login.rememberMe.isUserFriendly.checked = true;
            } catch (err) {}

            if (this._elements.login.inputUser.value != '' && this._elements.login.inputPass.value != '' ) {
                this._elements.captcha.isSolved().then(() => {
                    this._elements.login.inputSubmit.click();
                    return;
                });
            } else {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, 'No credentials were provided');
                return;
            }
        });
    }

    async detectAction() {
        return wait().then( () => {
            if ( this.isCountdownVisible() ) {
                return Promise.resolve({action: 'needToWait'});
            } else if ( this._elements.success.isUserFriendly ) {
                return this.updateResult();
            } else if ( this.isRollButtonVisible() ) {
                return Promise.resolve({action: 'doRoll'});
            } else {
                return this.detectAction();
            }
        });
    }

    clickRoll() {
        try {
            try {
                window.scrollTo(0, document.body.scrollHeight);
                this._elements.rollButton.scrollIntoView(false);
            } catch (err) { }
            this._elements.rollButton.click();
            setTimeout( () => { this._elements.rollButton.click(); }, 5000);
        } catch (err) {
            shared.closeWithError(K.ErrorType.CLICK_ROLL_ERROR, err);
        }
    }
}

class VieRoll extends Faucet {
    constructor() {
        let elements = {
            rollButton: new SubmitWidget({selector: '.main-content button[type="submit"]'}),
            claimed: new ReadableWidget({selector: '.swal2-html-container', parser: Parsers.trimNaNs}),
            captcha: new HCaptchaWidget(),
            success: new ReadableWidget({selector: '.swal2-success-ring'}),
            login: {
                inputUser: new TextboxWidget({ selector: '#email' }),
                inputPass: new TextboxWidget({ selector: '#password' }),
                inputSubmit: new SubmitWidget({ selector: 'button[type="submit"]' })
            }
        };

        let actions = {
            readClaimed: true,
            readBalance: false,
            readTimeLeft: false,
            readRolledNumber: false,
            preSaveResult: false,
            preRun: true
        };
        super(elements, actions);
    }

    getClaimsQty() {
        let statWidgets = document.querySelectorAll('.card.mini-stats-wid');
        if (statWidgets.length < 4) return false;

        let claimCounts = statWidgets[3].querySelector('p');
        if (!claimCounts) return false;

        claimCounts = claimCounts.innerText.split('/');
        if (claimCounts.length != 2) return false;

        return claimCounts[0];
    }

    async evalClaimsQty() {
        let current = this.getClaimsQty();

        if (current) {
            current = +current;
        } else {
            return;
        }

        let previous = await shared.getProp('tempClaimsQty') || 0;
        if (!isNaN(previous)) previous = +previous;

        if (current == previous) {
            return;
        } else if (current < previous) {
            return this.updateResult();
        } else {
            await shared.setProp('tempClaimsQty', current);
        }
    }

    readClaimed() {
        let claimed = 0.12;
        try {
            claimed = +document.querySelectorAll('.card.mini-stats-wid')[2].querySelector('p').innerText.split(' ')[0];
        } catch (err) { }
        return claimed;
    }

    async init() {
        await this.evalClaimsQty();

        if (window.location.pathname.includes('/faucet')) {
            this.run();
            return;
        } else if (window.location.pathname.includes('/firewall')) {
            this.solveFirewall();
            return;
        } else if (window.location.pathname.includes('/dashboard')) {
            window.location.href = (new URL('faucet', window.location)).href;
            return;
        } else if (window.location.pathname == '/') {
            let loginBtn = document.querySelector('.btn.btn-success');
            if (loginBtn) {
                loginBtn.click();
                return;
            } else {
                window.location.href = (new URL('login', window.location)).href;
            }
            return;
        } else if (this._url.includes('/login')) {

            let credentialsMode = this._params.credentials.mode;
            switch(credentialsMode) {
                case -1:
                    shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, 'Manual login required.');
                    break;
                case 0:
                    shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, 'Login required and autologin is not configured.');
                    break;
                default:
                    this.doLogin();
                    break;
            }
            return;
        }
    }

    async preRun() {
        return;
    }

    async solveFirewall() {
        this.closeSwal();

        this._elements.captcha.isSolved().then(() => {
            let btn = new SubmitWidget({selector: 'form:not(.p-3) button[type="submit"]'});
            btn.click();
        });
    }

    async doLogin() {
        return wait().then( () => {
            if (!this._elements.login.inputUser.isUserFriendly || !this._elements.login.inputPass.isUserFriendly || !this._elements.login.inputSubmit.isUserFriendly) {
                return this.doLogin();
            }

            let loginErrorDiv = document.querySelector('div.alert.alert-danger');
            if (loginErrorDiv) {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, loginErrorDiv.innerText);
                return;
            }

            if (this._params.credentials.mode == 1) {
                this._elements.login.inputUser.value = this._params.credentials.username;
                this._elements.login.inputPass.value = this._params.credentials.password;
            }

            if (this._elements.login.inputUser.value != '' && this._elements.login.inputPass.value != '' ) {
                this._elements.captcha.isSolved().then(() => {
                    this._elements.login.inputSubmit.click();
                    return;
                });
            } else {
                shared.closeWithError(K.ErrorType.LOGIN_ERROR, 'No credentials were provided');
                return;
            }
        });
    }

    preSaveResult() {
        this.closeSwal();
    }

    closeSwal() {
        let okButton = document.querySelector('button.swal2-confirm');
        if (okButton) {
            okButton.click();
        }
    }
}

class GRCRoll extends Faucet {
    constructor() {
        let elements = {
            countdownMinutes: new CountdownWidget({selector: '#roll_wait_text', parser: Parsers.freeGrcCountdown}),
            rollButton: new ButtonWidget({selector: 'input[id="roll_button"]'}),
            balance: new ReadableWidget({selector: '#balance', parser: Parsers.trimNaNs}),
            claimed: new ReadableWidget({selector: '#roll_comment .won', parser: Parsers.trimNaNs}),
            rolledNumber: new ReadableWidget({selector: '#roll_result', parser: Parsers.trimNaNs}),
            captcha: new NoCaptchaWidget({selector: '#roll_button'}),
            success: new ReadableWidget({selector: '#roll_result'})
        };
        let actions = {
            readTimeLeft: true,
            readRolledNumber: true
        };
        super(elements, actions);
    }

    init() {
        if (this._url.includes('#free_roll')) {
            if (document.querySelectorAll('a[href="#login"]').length > 0) {
                shared.closeWithError(K.ErrorType.NEED_TO_LOGIN, '');
                return;
            } else {
                this.run();
                return;
            }
        } else {
            return;
        }
    }

    isCountdownVisible() {
        return this._elements.countdownMinutes && this._elements.countdownMinutes.isUserFriendly && this._elements.countdownMinutes.isUserFriendly.innerText != '';
    }
}

class O24Roll extends Faucet {
    constructor() {
        let elements = {
            claimed: new ReadableWidget({selector: '#roll_comment .won', parser: Parsers.trimNaNs})
        };
        let actions = {
            readTimeLeft: true,
            readRolledNumber: false,
            readBalance: false
        };
        super(elements, actions);
    }

    init() {
        if (this.isCountdownVisible() || this.readClaimed() != 0) {
            this.updateResult();
            return;
        }

        this.solve();
    }

    getSpotsAvailable() {
        try {
            let soldSpots = document.querySelectorAll('.pos:not(.pfree)').length;
            let available = 1024-soldSpots;
            return {
                sold: '' + soldSpots,
                available: '' + available
            }
        } catch (err) {
        }
    }

    isPrime(num) {
        for(var i = 2; i < num; i++){
            if(num % i === 0){
                return false;
            }
        }
        return num > 1;
    }

    async solve() {
        let spots = this.getSpotsAvailable();
        if(!spots) {
            this.updateResult();
            return;
        }

        const findNotPrime = document.querySelector('select[name="pr"]').parentElement.innerText.includes('not a prime')
        let numbers = [...document.querySelectorAll('select[name="pr"] option[value]')].map(x => x.innerText)
        let prime = null;
        if (findNotPrime) {
            prime = numbers.find(x => {
                return !this.isPrime(x)
            });
        } else {
            prime = numbers.find(x => {
            return this.isPrime(x)
        });
        }
        if(!prime) {
            this.updateResult();
            return;
        }

        let addrInput = document.querySelector('label input[name="a"]');
        if (addrInput) {
            addrInput.value = this._params.address;
        } else {
            this.updateResult();
            return;
        }
        await wait(helpers.randomInt(1500, 3000));

        let answersList = [...document.querySelectorAll('select[name="tt"] option')].map(x => x.value);
        if (answersList.includes(spots.sold)) {
            document.querySelector('select[name="tt"]').value=spots.sold;
        } else if (answersList.includes(spots.available)) {
            document.querySelector('select[name="tt"]').value=spots.available;
        } else {
            this.updateResult();
            return;
        }

        await wait(helpers.randomInt(400, 5000));

        let primeSelect = document.querySelector('select[name="pr"]');
        helpers.triggerMouseEvent (primeSelect, "mouseenter");
        await wait(helpers.randomInt(5600, 29000));
        helpers.triggerMouseEvent (primeSelect, "mouseout");
        primeSelect.value=prime.toString()
        await wait(helpers.randomInt(1500, 5000));

        let claimForm = document.querySelector('form');
        if(claimForm) {
            claimForm.submit();
        }
    }

    isCountdownVisible() {
        let pars = [...document.querySelectorAll('p')];
        if (pars.find(x => x.innerText.includes('wait until next day'))) {
            return true;
        }

        if (pars.find(x => x.innerText.includes('PROBLEM'))) {
            return true;
        }

        return false;
    }

    readClaimed() {
        let pars = [...document.querySelectorAll('p')];
        let claimedElm = pars.find(x => x.innerText.includes('been transferred to your account'));
        if (claimedElm) {
            return claimedElm.innerText.split(' ')[0];
        } else {
            return 0;
        }
    }

    readNextRoll() {
        try {
            let pars = [...document.querySelectorAll('p')];
            if (pars.find(x => x.innerText.includes('until next day') || x.innerText.includes('ALL DAILY CLAIMS') || x.innerText.includes('You have 0 claims left'))) {
                return helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
            }

            if (pars.find(x => x.innerText.includes('PROBLEM'))) {
                return helpers.addMinutes(helpers.randomInt(6, 22));
            }

            if (pars.find(x => x.innerText.includes('You have'))) {
                return helpers.addMinutes(helpers.randomInt(6, 22));
            }
        } catch (err) { shared.devlog(`@readNextRoll: ${err}`); }
        return helpers.addMinutes(60 * 24 + helpers.randomInt(10, 160));
    }
}

class FCryptoRoll extends Faucet {
    constructor() {
        let elements = {
            countdownMinutes: new CountdownWidget({selector: '.sidebar-links .cursor-not-allowed span.notranslate', parser: Parsers.splitAndIdxToInt, options: { splitter: ':', idx: 1} }), // '00:21:28'
            rollButton: new ButtonWidget({selector: '.flex.justify-center button.inline-flex.items-center:not(.hidden)'}),
            balance: new ReadableWidget({selector: 'div.flex.badge.text-bg-yellow', parser: Parsers.trimNaNs}), // '405.81 Coins'
            claimed: new ReadableWidget({selector: 'div.ml-3.w-0 p span.text-yellow-500.font-medium', parser: Parsers.splitAndIdxTrimNaNs, options: { splitter: '(', idx: 0} }), // '25.05 Coins (12 + 13.05)'
            captcha: new HCaptchaWidget({selector: '#hcap-script > iframe'}),
            success: new ReadableWidget({selector: 'div.ml-3.w-0 p span.text-yellow-500.font-medium'})
        };
        let actions = {
            isMultiClaim: true,
            preRoll: true,
            postRun: true,
            readRolledNumber: false,
        };
        super(elements, actions);
        this._paths = {
            faucet: '/task/faucet-claim',
            dashboard: '/dashboard'
        };
        this._linkSelectors = {
            Faucet: 'a[href="https://faucetcrypto.com/task/faucet-claim"]'
        }
        this.useUrlListener();
    }

    init() {
        this._elements.captcha = new HCaptchaWidget({selector: '#hcap-script > iframe'});
        this._elements.rollButton = new ButtonWidget({selector: '.flex.justify-center button.inline-flex.items-center:not(.hidden)'});
        if (this._url.endsWith(this._paths.dashboard)) {
            return this.runDashboard();
        } else if (this._url.includes(this._paths.faucet)) {
            return wait().then( () => { this.run(); });
        }

        return;
    }

    readSections() {
        let sections = {};
        try {
            for (var l in this._linkSelectors) {
                sections[l] = {};
                sections[l].elm = document.querySelector(this._linkSelectors[l]);
                if (sections[l].elm) {
                    let qty = sections[l].elm.querySelector('span.ml-auto');
                    sections[l].qty = (qty && !isNaN(qty.innerText)) ? qty.innerText : 0;
                }
            }
        } catch {}

        this.sections = sections;
    }

    runDashboard() {
        this.readSections();

        if (this.sections['Faucet'].elm) {
            this.sections['Faucet'].elm.click();
            return;
        } else {
            return wait().then( () => { this.run(); });
        }
    }

    scrollTo() {
        let mainContainer = document.querySelector('main');
        if (mainContainer) {
            mainContainer.scrollTo(0, mainContainer.scrollHeight - mainContainer.offsetHeight);
        }
    }

    preRoll() { // search for 'You don't need to solve any captcha! The system is telling me that you are a good person :)'
        this.scrollTo();
        let checkCircleSpan = document.querySelector('p.font-medium.flex.justify-center.leading-0 span.text-green-500.mr-3 svg');
        if(checkCircleSpan) {
            if (checkCircleSpan.parentElement.parentElement.innerText.toLowerCase().includes('the system is telling me that you are a good person')) {
                this._elements.captcha = new NoCaptchaWidget({selector: '.flex.justify-center button.inline-flex.items-center:not(.hidden)'});
                return;
            }
        }
    }

    postRun() {

        if (this._url.endsWith(this._paths.dashboard) || this._oldClaimed != this._result.claimed) {
            try {
                this._elements.claimed.isUserFriendly.parentElement.parentElement.parentElement.querySelector('button');
            } catch (err) {
            }
            this._oldClaimed = null;
            this.readSections();
            if (this.sections != {}) {
                if (this.sections['Faucet'].elm) {
                    this.sections['Faucet'].elm.click();
                    return;
                } else {
                }
            } else {
            }
        } else {
        }

        this._result = shared.getProp('tempResults');
        shared.closeWindow(this._result);
        return;
    }

    async runPtcList() {
        let listItems = [...document.querySelectorAll('.grid.grid-responsive-3 .feather.feather-eye')].map(x => x.parentElement.parentElement).filter(x => x.isUserFriendly());
        if (listItems.length > 0) {
            listItems[0].click();
            return;
        } else {
            return wait().then( () => { this.runPtcList() } );
        }
    }

    runPtcSingleStart() {
        return this.run('doRoll');
    }

    runPtcSingleWait() {
        this._elements.captcha = new NoCaptchaWidget({selector: 'a.notranslate:not(.cursor-not-allowed)' });
        this._elements.rollButton = new ButtonWidget({selector: 'a.notranslate:not(.cursor-not-allowed)' });
        return this.run('doRoll');
    }
}

    let landing, instance, siteTimer;
    let useTimer;
    async function init() {
        eventer = new EventEmitter();
        persistence = new Persistence();
        shared = objectGenerator.createShared();
        useTimer = shared.getConfig()['defaults.extraInterval'];
        if (window.location.host === 'criptologico.com') {
            landing = window.location.host;
            instance = K.LOCATION.MANAGER;
            manager = objectGenerator.createManager();
            CFPromotions = objectGenerator.createCFPromotions();
            uiRenderer = new UiRenderer();
            uiRenderer.initialize();
            ui = objectGenerator.createUi();
            CFHistory = objectGenerator.createCFHistory();

            await manager.init();
            try {
                if (!document.body.classList.contains('sidebar-collapse')) document.querySelector('a[data-widget="pushmenu"]').click()
            } catch {}
            setTimeout( () => { window.stop(); }, 10000);
        } else {
            instance = K.LOCATION.UNKNOWN;
            detectWeb();
        }
    }
    init();
})();