Cartel Empire - Job & Expedition Progress Bars

Adds progress bars for jobs and expeditions

// ==UserScript==
// @name         Cartel Empire - Job & Expedition Progress Bars
// @namespace    http://tampermonkey.net/
// @version      1.8.2
// @description  Adds progress bars for jobs and expeditions
// @author       Baccy
// @match        https://cartelempire.online/*
// @icon         https://cartelempire.online/images/icon-white.png
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

// Below are examples of how to enter colors in the user settings as well as a few formats
let exampleOption1 = 'white';
let exampleOption2 = '#fff';
let exampleOption3 = 'rgb(255, 255, 255)';

// USER SETTINGS
let disableExpeditionProgressBar = false; // Change to true to disable a type of progress bar
let disableJobProgressBar = false;

let flashColorOn100Percent = ''; // Enter a color for a bar to alternate between it and the fill color every second at 100%
let percentageDecimal = 0; // Change the number from 0 if you want the percentages to have decimals

let expeditionBarFillColor = ''; // The color of the completed width of a progress bar
let expeditionBarTextColor = ''; // The color for the completion % text
let expeditionBarBackgroundColor = ''; // The background color for the progress bar
let expeditionTextShadowColor = ''; // Slight white glow the default bars have. Enter 'none' to remove it or another color to change it

let jobBarFillColor = '';
let jobBarTextColor = '';
let jobBarBackgroundColor = '';
let jobTextShadowColor = '';

let progressBarsFirst = false; // Changing this to true will put the bars before the reputation bar. This may look better on smaller window sizes
// USER SETTINGS END


(function() {
    'use strict';

    let jobData = {};
    let expeditionData = {};

    let jobFlashing = false;
    let expeditionFlashing = {};

    init();

    async function init() {
        if (window.location.href.toLowerCase().includes('cartelempire.online/jobs')) {
            setInterval(processJobs, 1000);
        } else if (window.location.href.toLowerCase().includes('cartelempire.online/expedition')) {
            setInterval(processExpeditions, 1000);
        }
        
        let barElement;
        if (progressBarsFirst) barElement = document.querySelector('.col-4.col-lg-2');
        else barElement = document.querySelector('.row.justify-content-center.mb-1.text-center.gy-2');
        if (barElement) {
            await loadData();

            if (!disableJobProgressBar && Object.keys(jobData).length > 0) {
                const fillColor = jobBarFillColor || '#7c4acf';
                const textColor = jobBarTextColor || '#3a3a3a';
                const backgroundColor = jobBarBackgroundColor || '#d7d7d7';
                const textShadow = jobTextShadowColor || '#ffffff';

                const jobProgressBar = buildProgressBar(jobData, fillColor, textColor, backgroundColor, textShadow, percentageDecimal, 'job-progress-bar', 'Your job will be ready at<br>', 'job-progress-bar-fill', 'job-progress-text', 'M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v1.384l7.614 2.03a1.5 1.5 0 0 0 .772 0L16 5.884V4.5A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zm0 1h3a.5.5 0 0 1 .5.5V3H6v-.5a.5.5 0 0 1 .5-.5z', 'M0 12.5A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5V6.85L8.129 8.947a.5.5 0 0 1-.258 0L0 6.85v5.65z');
                if (progressBarsFirst) barElement.parentNode.insertBefore(jobProgressBar, barElement);
                else barElement.appendChild(jobProgressBar);
                setInterval(() => {
                    updateProgressBar('job-progress-bar', 'job-progress-text', jobData, percentageDecimal, fillColor, '#7c4acf', 'job', flashColorOn100Percent);
                }, 1000);
            }

            if (!disableExpeditionProgressBar && Object.keys(expeditionData).length > 0) {
                const fillColor = expeditionBarFillColor || '#5692e4';
                const textColor = expeditionBarTextColor || '#3a3a3a';
                const backgroundColor = expeditionBarBackgroundColor || '#d7d7d7';
                const textShadow = expeditionTextShadowColor || '#ffffff';

                for (let i = 0; i < 3; i++) {
                    if (expeditionData[i]) {
                        const expeditionProgressBar = buildProgressBar(expeditionData[i], fillColor, textColor, backgroundColor, textShadow, percentageDecimal, `expedition-progress-bar-${i}`, 'Your sicarios will return at<br>', `expedition-progress-bar-${i}-fill`, `expedition-progress-text-${i}`, 'M2.52 3.515A2.5 2.5 0 0 1 4.82 2h6.362c1 0 1.904.596 2.298 1.515l.792 1.848c.075.175.21.319.38.404.5.25.855.715.965 1.262l.335 1.679c.033.161.049.325.049.49v.413c0 .814-.39 1.543-1 1.997V13.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-1.338c-1.292.048-2.745.088-4 .088s-2.708-.04-4-.088V13.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-1.892c-.61-.454-1-1.183-1-1.997v-.413a2.5 2.5 0 0 1 .049-.49l.335-1.68c.11-.546.465-1.012.964-1.261a.807.807 0 0 0 .381-.404l.792-1.848ZM3 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm10 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM6 8a1 1 0 0 0 0 2h4a1 1 0 1 0 0-2H6ZM2.906 5.189a.51.51 0 0 0 .497.731c.91-.073 3.35-.17 4.597-.17 1.247 0 3.688.097 4.597.17a.51.51 0 0 0 .497-.731l-.956-1.913A.5.5 0 0 0 11.691 3H4.309a.5.5 0 0 0-.447.276L2.906 5.19Z');
                        if (progressBarsFirst) barElement.parentNode.insertBefore(expeditionProgressBar, barElement);
                        else barElement.appendChild(expeditionProgressBar);
                        setInterval(() => {
                            updateProgressBar(`expedition-progress-bar-${i}`, `expedition-progress-text-${i}`, expeditionData[i], percentageDecimal, fillColor, '#5692e4', 'expedition', flashColorOn100Percent, i);
                        }, 1000);
                    }
                }
            }

            setInterval(loadData, 5000);
        }
    }

    async function loadData() {
        jobData = await GM_getValue('jobData', {});
        expeditionData = await GM_getValue('expeditionData', {});
    }


    function processJobs() {
        const jobCompletionElement = document.getElementById('progressMessage');
        if (jobCompletionElement) {
            const currentTime = Date.now();
            let completionTime = parseInt(jobCompletionElement.getAttribute('data-bs-finishtime'), 10) * 1000;

            if (!jobData || !jobData.completion || jobData.completion < currentTime || jobData.completion !== completionTime) {
                const jobTypeElement = jobCompletionElement.closest('.row.g-0')?.querySelector('.card-title.text-center.mb-2');
                const jobType = jobTypeElement ? jobTypeElement.childNodes[0].nodeValue.trim() : '';
                const jobDuration = completionTime - currentTime;
                const newJobData = {
                    completion: completionTime,
                    duration: jobDuration,
                    location: jobType
                };

                if (newJobData.completion > currentTime) {
                    jobData = newJobData;
                    GM_setValue('jobData', jobData);
                }
            }
        }
    }

    function processExpeditions() {
        function convertDuration(timeString) {
            const timeParts = timeString.split(' ');
            let hours = 0, minutes = 0;

            timeParts.forEach(part => {
                if (part.includes('H')) {
                    hours = parseInt(part.replace('H', ''), 10);
                } else if (part.includes('m')) {
                    minutes = parseInt(part.replace('m', ''), 10);
                }
            });

            return (hours * 60 * 60 * 1000) + (minutes * 60 * 1000);
        }

        const completionElements = document.querySelectorAll('.remainingTime');
        const locationElements = document.querySelectorAll('.fs-6 > .fst-italic');
        const expeditionDurationElements = document.querySelectorAll('.card-text.estimatedTime');

        for (let i = 0; i < completionElements.length; i++) {
            const completionTime = parseInt(completionElements[i].getAttribute('completion'), 10) * 1000;
            const currentTime = Date.now();

            if (completionTime > currentTime) {
                const locationText = locationElements[i].innerText.trim();
                const expeditionDurationString = expeditionDurationElements[i].innerText.trim();
                const expeditionDuration = convertDuration(expeditionDurationString);
                const newExpeditionData = {
                    completion: completionTime,
                    duration: expeditionDuration,
                    location: locationText
                };

                if (expeditionData[i]?.completion === newExpeditionData.completion) continue;

                expeditionData[i] = newExpeditionData;
                GM_setValue('expeditionData', expeditionData);
            }
        }
    }

    function buildProgressBar(data, fillColor, textColor, backgroundColor, textShadow, decimalPlace, anchorID, anchorBody, fillId, textId, svgPath1, svgPath2) {
        function formatTimeToLPT(completionTimestamp) {
            if (completionTimestamp) {
                const completionDate = new Date(completionTimestamp);
                let utcTime = completionDate.toUTCString().replace('GMT', 'LPT');
                return utcTime;
            }
        }

        const currentTimestamp = Date.now();
        const remainingTime = data.completion - currentTimestamp;
        const utcTime = formatTimeToLPT(data.completion);
        const percentageRemaining = (remainingTime / data.duration) * 100;
        const progressWidth = Math.min(100 - percentageRemaining, 100);

        const progressBar = document.createElement('div');
        progressBar.classList.add('col-6', 'col-lg-3');

        const anchor = document.createElement('a');
        anchor.id = anchorID;
        anchor.setAttribute('tabindex', '0');
        anchor.setAttribute('role', 'button');
        anchor.setAttribute('data-bs-toggle', 'popover');
        anchor.setAttribute('data-bs-html', 'true');
        anchor.setAttribute('data-bs-trigger', 'hover focus');
        anchor.setAttribute('data-bs-placement', 'bottom');
        anchor.setAttribute('data-bs-sanitize', 'false');
        anchor.setAttribute('data-bs-content', `${anchorBody}${utcTime}`);
        anchor.setAttribute('data-bs-original-title', data.location);

        const elementRow = document.createElement('div');
        elementRow.classList.add('row', 'align-items-center', 'g-2');

        const iconElement = document.createElement('div');
        iconElement.classList.add('col-auto');

        const svgLabel = document.createElement('span');
        svgLabel.classList.add('lifeLabel', 'd-flex', 'align-items-center', 'mb-0');

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('class', 'bi bi-battery-full');
        svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        svg.setAttribute('width', '16');
        svg.setAttribute('height', '16');
        svg.setAttribute('fill', 'currentColor');
        svg.setAttribute('viewBox', '0 0 16 16');

        const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path1.setAttribute('d', svgPath1);
        svg.appendChild(path1);

        if (svgPath2) {
            const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            path2.setAttribute('d', svgPath2);
            svg.appendChild(path2);
        }

        svgLabel.appendChild(svg);
        iconElement.appendChild(svgLabel);

        const barParentElement = document.createElement('div');
        barParentElement.classList.add('col');

        const barElement = document.createElement('div');
        barElement.classList.add('progress', 'progressBarStat');
        barElement.style.backgroundColor = backgroundColor;

        const barProgressElement = document.createElement('div');
        barProgressElement.classList.add('progress-bar', 'progress-bar-striped');
        barProgressElement.id = fillId;
        barProgressElement.setAttribute('role', 'progressbar');
        barProgressElement.style.width = `${progressWidth}%`;
        barProgressElement.style.backgroundColor = fillColor;

        const barTextParent = document.createElement('div');
        barTextParent.classList.add('progress-bar-title', 'fw-bold');

        const textLabel = document.createElement('span');
        textLabel.textContent = `${progressWidth.toFixed(decimalPlace)}%`;
        textLabel.id = textId;
        textLabel.style.textShadow = textShadow;
        textLabel.style.color = textColor;

        barTextParent.appendChild(textLabel);
        barElement.appendChild(barProgressElement);
        barElement.appendChild(barTextParent);
        barParentElement.appendChild(barElement);
        elementRow.appendChild(iconElement);
        elementRow.appendChild(barParentElement);
        anchor.appendChild(elementRow);
        progressBar.appendChild(anchor);

        return progressBar;
    }

    function updateProgressBar(progressBarID, textId, data, decimal, fillColor, defaultColor, type, flashingColor, i) {
        const progressBarElement = document.querySelector(`#${progressBarID}`);
        const progressBar = progressBarElement.querySelector(`#${progressBarID}-fill`);
        const progressText = progressBarElement.querySelector(`#${textId}`);

        if (!progressBar) return;

        const remainingTime = data.completion - Date.now();
        const percentageRemaining = (remainingTime / data.duration) * 100;
        const percentageComplete = Math.min(100 - percentageRemaining, 100);

        if (percentageComplete >= 100 && flashingColor) {
            if (type === 'job') {
                progressBar.style.setProperty('background-color', jobFlashing ? flashingColor : fillColor || defaultColor, 'important');
                jobFlashing = !jobFlashing;
            } else {
                progressBar.style.setProperty('background-color', expeditionFlashing[i] ? flashingColor : fillColor || defaultColor, 'important');
                expeditionFlashing[i] = !expeditionFlashing[i];
            }
        } else {
            progressBar.style.setProperty('background-color', fillColor || defaultColor, 'important');
        }

        progressBar.style.width = `${percentageComplete}%`;
        progressText.textContent = `${percentageComplete.toFixed(decimal)}%`;
    }
})();