Greasy Fork is available in English.

WME Edition Helper

Monitor editions count and shows a timer for next suggested save action

// ==UserScript==
// @name             WME Edition Helper
// @name:es          WME Ayudante de edición
// @description      Monitor editions count and shows a timer for next suggested save action
// @description:es   Monitorea el contador de ediciones y muestra un temporizador para la siguiente acción de guardado sugerida

// @author           EdwardNavarro
// @namespace        https://greasyfork.org/en/users/670818-edward-navarro
// @version          2022.11.22.01
// @license          GNU GPLv3

// @include          /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
// @exclude          https://www.waze.com/user/*
// @exclude          https://www.waze.com/*/user/*
// @require          https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @icon             https://www.edwardnavarro.com/cdn/wme/wme_eh_icon_32x32.svg
// @connect          www.waze.com
// @grant            GM_xmlhttpRequest
// @grant            GM_addElement

// @contributionURL  https://github.com/WazeDev/Thank-The-Authors
// ==/UserScript==

/* global W */
/* global toastr */
/* global $ */

/**
* ===============================================
*  This script is based on the following scripts:
*  - "Waze Edit Count Monitor" (by MapOMatic)
*  - "Waze Edit & UR Count Monitor" (by Crotalo)
* ===============================================
*/

// This function is injected into the page to allow it to run in the page's context.
function wmeEH_Injected() {
    'use strict';

    const addonVersion = '2021.10.28.03';

    let _toastr_settings = {
        timeBeforeSaving: 70,
        remindAtEditCount: 30,
        warnAtEditCount: 45,
        wasReminded: false,
        wasWarned: false
    };

    let debugLevel = 0;
    let userName = '';
    let lastTodayEditCount = 0;
    let lastYesterdayEditCount = 0;
    let lastDayBeforeEditCount = 0;
    let savesWithoutIncrease = 0;
    let showURs, showMPs;
    let totalSeconds = 0;
    let button_container, button_content_wrap, button_item_container, button_item_icon, button_item_link, button_item_content, progress_bar_wrap, progress_bar_fill, saved_timer, timer_interval;
    let tooltipText = '<b>Ediciones Diarias</b><br><small>(Clic para ver el perfil)<small>';

    // load default addon settings
    if (!localStorage.WMEEditionHelperScript) {
        let options = [null,_toastr_settings.timeBeforeSaving,_toastr_settings.remindAtEditCount,_toastr_settings.warnAtEditCount,false,false];
        localStorage.WMEEditionHelperScript = JSON.stringify(options);
    }

    function log(message, level, prefix = 'LOG', bgColor = 'darkslategrey', textColor = 'white') {
        if (message && level <= debugLevel) {
            console.log('%c%s%s', `background:${bgColor};color:${textColor};padding:5px 10px;`, `[${prefix}] WME Edition Helper >>`, message);
        }
    }

    function checkEditCount() {
        window.postMessage(JSON.stringify(['wmeEHGetCounts',userName]),'*');
        _toastr_settings.wasReminded = false;
        _toastr_settings.wasWarned = false;
        toastr.remove();
    }

    function getChangedObjectCount() {
        let count = W.changesLogController._changesLogViewModel.attributes.actionsCount;
        return count;
    }

    function pad(val) {
        let valString = val + "";
        if (valString.length < 2) {
            return "0" + valString;
        } else {
            return valString;
        }
    }

    function setTime() {
        ++totalSeconds;
        let hours = parseInt(totalSeconds / 60 / 60) % 60;
        $('#saved-timer').html(`${(hours > 0) ? `${hours}:` : ''}${pad(parseInt(totalSeconds / 60) % 60)}:${pad(totalSeconds % 60)}`);
    }

    function runTimer() {
        timer_interval = setInterval(setTime, 1000);
    }

    function updateEditCount(todayEditCount = 0, yesterdayEditCount = 0, dayBeforeEditCount = 0, noIncrement) {
        let textColor;
        let bgColor;
        let tooltipTextColor;

        // Add the counter div if it doesn't exist.
        if ($('#eh-button').length === 0) {
            button_container = $('<div>', { id: 'eh-button' });
            button_content_wrap = $('<div>', { class: 'toolbar-button toolbar-button-with-icon' });
            button_item_link = $('<a>', { href: 'https://www.waze.com/user/editor/' + userName.toLowerCase(), target: '_blank', style:'text-decoration: none;', 'data-original-title': tooltipText });
            button_item_container = $('<div>', { class: 'item-container' });
            button_item_icon = $('<div>', { class: 'toolbar-icon-eh w-icon w-icon-pencil' });
            button_item_content = $('<div>', { style: 'margin:5px 0 0 5px; line-height: 1;' });

            progress_bar_wrap = $('<div>', { style: 'width: 100%; height: 5px; background-color: #d7dadc; border:1px #fff solid; box-sizing: content-box;' });
            progress_bar_fill = $('<div>', { class: 'progress', style: 'width: 0%; height: 5px; animation-fill-mode: both; animation-name: progressBar; animation-duration:' + _toastr_settings.timeBeforeSaving + 's; animation-timing-function: ease-in-out;' });

            saved_timer = $('<div>', { id: 'saved-timer', style: 'font-size:8px; line-height:1; text-align:right; color:darkgray;' });

            button_container.append(button_content_wrap);
            button_content_wrap.append(button_item_link);
            button_item_link.append(button_item_container);
            button_item_container.append(button_item_icon);
            button_item_container.append(button_item_content);

            button_item_content.append(progress_bar_wrap);
            button_item_content.append(saved_timer);
            progress_bar_wrap.append(progress_bar_fill);

            $('#edit-buttons').prepend(button_container);

            button_item_link.tooltip({
                placement: 'auto top',
                delay: { show: 100, hide: 100 },
                html: true,
                template: '<div class="tooltip" role="tooltip" style="opacity:0.95"><div class="tooltip-arrow"></div><div class="my-tooltip-header" style="display:block;"><b></b></div><div class="my-tooltip-body tooltip-inner" style="display:block;"></div></div>'
            });
        }

        log(`EDIT COUNTS -> Today: ${todayEditCount}, Yesterday: ${yesterdayEditCount}, Day before: ${dayBeforeEditCount}`, 1, 'INFO', 'purple');

        if (lastTodayEditCount !== todayEditCount) {
            savesWithoutIncrease = 0;
        } else {
            if (!noIncrement) savesWithoutIncrease += 1;
        }

        switch (savesWithoutIncrease) {
            case 0:
            case 1:
                textColor = '#354148';
                bgColor = '';
                tooltipTextColor = 'white';
                break;
            case 2:
                textColor = '#354148';
                bgColor = 'yellow';
                tooltipTextColor = 'black';
                break;
            default:
                textColor = 'white';
                bgColor = 'red';
                tooltipTextColor = 'white';
        }

        button_container.css('background-color', bgColor);
        button_item_icon.css('color', textColor);
        button_item_content.css('color', textColor).html(`Ediciones: ${todayEditCount}`);
        button_item_content.append(progress_bar_wrap);
        button_item_content.append(saved_timer);

        let dayBeforeEditCountText = `<hr style="border:0 none; border-bottom:1px #999 solid; margin:5px 0;"/><div class="days-group"><div class="day-1"><h3>Hoy</h3><span>${todayEditCount}</span></div><div class="day-2"><h3>Ayer</h3><span>${yesterdayEditCount}</span></div><div class="day-3"><h3>Antier</h3><span>${dayBeforeEditCount}</span></div></div>`;
        let warningText = (savesWithoutIncrease > 0) ? `<div style="font-size:13px;border-radius:5px;padding:5px;margin-top:5px;color:${tooltipTextColor};background-color:${bgColor};"><b>${savesWithoutIncrease}</b> salvadas/guardadas consecutivas sin incremento en el contador.<br><span style="font-weight:bold;font-size:16px;">¿Estás estrangulado?<br>🤔👀<span></div>` : '';
        button_item_link.attr('data-original-title', tooltipText + dayBeforeEditCountText + warningText);

        lastTodayEditCount = todayEditCount;
        lastYesterdayEditCount = yesterdayEditCount;
        lastDayBeforeEditCount = dayBeforeEditCount;
        totalSeconds = 0;

        clearTimeout(timer_interval);
        runTimer();
    }

    function receiveMessage(event) {
        let msg;

        try {
            msg = JSON.parse(event.data);
        } catch (err) {
            // Do nothing
        }

        if (msg && msg[0] === 'wmeEHUpdateUi') {
            let todayEditCount = msg[1][0];
            let yesterdayEditCount = msg[1][1];
            let dayBeforeEditCount = msg[1][2];
            updateEditCount(todayEditCount, yesterdayEditCount, dayBeforeEditCount);
        }
    }

    function checkChangedObjectCount() {
        let objectEditCount = getChangedObjectCount();

        if (objectEditCount >= _toastr_settings.warnAtEditCount && !_toastr_settings.wasWarned) {
            toastr.remove();
            toastr.error('<span style="font-size:16px;">Has editado al menos <b>' + _toastr_settings.warnAtEditCount + '</b> objetos.</span><br><br> Deberías considerar guardar pronto. Si obtienes un error al guardar, necesitarás deshacer algunos cambios/acciones e intentar nuevamente.', 'Edition Helper:', {timeOut: 25000});
            _toastr_settings.wasWarned = true;
            //log('WARMED', 0, 'ALERT', 'tomato')
        } else if (objectEditCount >= _toastr_settings.remindAtEditCount && !_toastr_settings.wasReminded) {
            toastr.remove();
            toastr.warning('<span style="font-size:16px;">Has editado al menos <b>' + _toastr_settings.remindAtEditCount + '</b> objetos.</span><br><br> Deberías considerar guardar pronto.', 'Edition Helper:', {timeOut: 15000});
            _toastr_settings.wasReminded = true;
            //log('REMINDED', 0, 'ALERT', 'orange')
        } else if (objectEditCount < _toastr_settings.remindAtEditCount) {
            _toastr_settings.wasWarned = false;
            _toastr_settings.wasReminded = false;
            toastr.remove();
            //log('REMOVED', 0, 'ALERT', 'sienna')
        }
    }

    function errorHandler(callback) {
        try {
            callback();
        } catch (e) {
            console.error('%c%s%s', 'background:darkred;color:white;padding:5px 10px;', '[ERROR] WME Edition Helper >>', e);
        }
    }

    /* helper functions */
    function getElementsByClassName(classname, node) {
        if(!node) node = document.getElementsByTagName("body")[0];
        let a = [];
        let re = new RegExp('\\b' + classname + '\\b');
        let els = node.getElementsByTagName("*");
        for (let i=0, j=els.length; i<j; i++) {
            if (re.test(els[i].className)) a.push(els[i]);
        }
        return a;
    }

    function getId(node) {
        return document.getElementById(node);
    }

    function updateAddonSettings(event) {
        _toastr_settings.timeBeforeSaving = getId('_ehSavingWaitTime').value;
        _toastr_settings.remindAtEditCount = getId('_ehRememberAfter').value;
        _toastr_settings.warnAtEditCount = getId('_ehAlertAfter').value;
        showURs = getId('_ehShowURs').checked;
        showMPs = getId('_ehShowMPs').checked;

        $('.progress').css('animation-duration', `${getId('_ehSavingWaitTime').value}s`);
    }

    function init() {
        userName = W.loginManager.user.userName;
        window.addEventListener('message', receiveMessage);

        // restore saved settings
        if (localStorage.WMEEditionHelperScript) {
            let options = JSON.parse(localStorage.WMEEditionHelperScript);

            _toastr_settings.timeBeforeSaving = options[1];
            _toastr_settings.remindAtEditCount = options[2];
            _toastr_settings.warnAtEditCount = options[3];
            showURs = options[4];
            showMPs = options[5];
        }

        // check if sidebar is hidden
        let sidebar = getId('sidebar');
        if (sidebar.style.display == 'none') {
            log("Not logged in yet - will initialise at login", 0, 'WARN', 'orange');
            W.loginManager.events.register("login", null, init);
            return;
        }

        // check that user-info section is defined
        let userTabs = getId('user-info');
        if (userTabs === null) {
            log("Editor not initialised yet - trying again in a bit...", 0, 'WARN', 'orange');
            setTimeout(init, 789);
            return;
        }

        // add styleshets and styles to head section
        $('head').append(
            $('<link/>', {
                rel: 'stylesheet',
                type: 'text/css',
                href: 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css'
            }),
            $('<style type="text/css">'
              + '#toast-container {position: absolute;} '
              + '#toast-container > div {opacity: 0.95;} '
              + '.toast-top-center {top: 30px;} '
              + '#edit-buttons #eh-button { display:flex; } '
              + '.toolbar .toolbar-icon-eh { color: #484848; font-size: 24px; margin: 8px 0; position: relative; text-align: center; width: 24px; } '
              + '.progress { background-color: red; animation-fill-mode:both; } '
              + '@keyframes progressBar { 0% { width: 0; } 99% { background-color: red; } 100% { width: 100%; background-color: green; } } '
              + '.days-group { width:100%; display:flex; justify-content:space-between; align-item:center; } '
              + '.days-group div { width:30%; padding:5px; background-color:darkgray; color:white; display:flex; flex-direction:column; align-item:center; border-radius:5px; } '
              + '.days-group div h3 { font-size:12px; font-weight:bold; line-height:1; margin:5px 0; } '
              + '.days-group div span { font-size:14px; font-weight:bold; } '
              + '.days-group .day-1 { background-color:darkcyan; } '
              + '.days-group .day-2 { background-color:darkgreen; } '
              + '.days-group .day-3 { background-color:darkolivegreen; } '
              + '</style>')
        );

        // add js libraries and register events
        $.getScript('https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js', function() {
            toastr.options = {
                target:'#map',
                showDuration: 300,
                hideDuration: 1000,
                timeOut: 10000,
                extendedTimeOut: 1000,
                positionClass: 'toast-top-right',
                closeOnHover: false,
                closeButton: true,
                newestOnTop: true,
                progressBar: true,
                showEasing: 'swing',
                hideEasing: 'linear',
                showMethod: 'fadeIn',
                hideMethod: 'fadeOut',
            };

            W.model.actionManager.events.register('afterclearactions', null, () => errorHandler(checkEditCount));
            W.model.actionManager.events.register('afteraction', null, () => errorHandler(checkChangedObjectCount));
            W.model.actionManager.events.register('afterundoaction', null, () => errorHandler(checkChangedObjectCount));

            // Update the edit count first time.
            checkEditCount();
            toastr.success("Edition Helper Inicializado!");
            log('Initialized!', 0, 'SUCCESS', 'green');
        });

        // add new box to left of the map
        let navTabs = getElementsByClassName('nav-tabs', userTabs)[0];
        let tabContent = getElementsByClassName('tab-content', userTabs)[0];
        let addon = document.createElement('section');
        addon.id = "edition-helper-addon";

          // advanced options
        let section = document.createElement('p');
        section.style.paddingTop = "0px";
        section.className = 'checkbox';
        section.id = 'advancedOptions';
        section.innerHTML = '<h4><span class="fa fa-pencil" title="Edition Helper"></span> Edition Helper</h4><div style="margin:5px 0 10px 0;"><b>Configuración</b></div>'
            + '<label for="_ehSavingWaitTime">Tiempo de espera para guardar</label><br>'
            + '<input type="number" min="1" max="3600" size="4" id="_ehSavingWaitTime" style="margin: 0 0 20px 0" /> segundos'
            + '<br>'
            + '<label for="_ehRememberAfter">Recomendar guardar despues de</label><br>'
            + '<input type="number" min="1" max="5000" size="4" id="_ehRememberAfter" style="margin: 0 0 20px 0" /> cambios'
            + '<br>'
            + '<label for="_ehAlertAfter">Alertar guardar despues de</label><br>'
            + '<input type="number" min="1" max="5000" size="4" id="_ehAlertAfter" style="margin: 0 0 20px 0" /> cambios'
            + '<br>'
            + '<label><input type="checkbox" id="_ehShowURs" /> Mostrar UR\'s gestionadas</label><br>'
            + '<label><input type="checkbox" id="_ehShowMPs" /> Mostrar MP\'s gestionadas</label><br>'
        ;
        addon.appendChild(section);

        // Addon legal and credits
        addon.innerHTML += '<hr style="border:0 none; border-bottom:1px #ccc solid;">'
            + '<small><b><a href="https://greasyfork.org/en/scripts/434355-wme-edition-helper" target="_blank"><u>'
            + 'WME Edition Helper</u></a></b> &nbsp; v' + addonVersion + '</small>';

        // Add tab button and panel content
        let newtab = document.createElement('li');
        newtab.innerHTML = '<a href="#sidepanel-edition-helper" data-toggle="tab"><span class="fa fa-pencil" title="Edition Helper"></span> EH</a>';
        navTabs.appendChild(newtab);

        addon.id = "sidepanel-edition-helper";
        addon.className = "tab-pane";
        tabContent.appendChild(addon);

        getId('_ehSavingWaitTime').onchange = updateAddonSettings;
        getId('_ehRememberAfter').onchange = updateAddonSettings;
        getId('_ehAlertAfter').onchange = updateAddonSettings;
        getId('_ehShowURs').onclick = updateAddonSettings;
        getId('_ehShowMPs').onclick = updateAddonSettings;

        // restore saved settings
        if (localStorage.WMEEditionHelperScript) {
            let options = JSON.parse(localStorage.WMEEditionHelperScript);

            getId('_ehSavingWaitTime').value = options[1];
            getId('_ehRememberAfter').value = options[2];
            getId('_ehAlertAfter').value = options[3];
            getId('_ehShowURs').checked = options[4];
            getId('_ehShowMPs').checked = options[5];
        }

        // overload the WME exit function
        const saveEditionHelperOptions = function() {
            if (localStorage) {
                let options = [];

                // preserve previous options which may get lost after logout
                if (localStorage.WMEEditionHelperScript) {
                    options = JSON.parse(localStorage.WMEEditionHelperScript);
                }

                options[1] = getId('_ehSavingWaitTime').value;
                options[2] = getId('_ehRememberAfter').value;
                options[3] = getId('_ehAlertAfter').value;
                options[4] = getId('_ehShowURs').checked;
                options[5] = getId('_ehShowMPs').checked;

                localStorage.WMEEditionHelperScript = JSON.stringify(options);
            }
        }
        window.addEventListener("beforeunload", saveEditionHelperOptions, false);
    }

    function bootstrap() {
        if (W &&
            W.loginManager &&
            W.loginManager.events &&
            W.loginManager.events.register &&
            W.map &&
            W.loginManager.user) {
            log('Initializing...', 0);
            init();
        } else {
            log('Bootstrap failed. Trying again...', 0, 'ERROR', 'darkred');
            window.setTimeout(function () {
                bootstrap();
            }, 1000);
        }
    }

    bootstrap();
}


// Code that is NOT injected into the page.
// Note that jQuery may or may not be available, so don't rely on it in this part of the script.
(function(){
    'use strict';

    function getEditorProfileFromSource(source) {
        let match = source.match(/gon.data=({.*?});gon.env=/i);
        return JSON.parse(match[1]);
    }

    function getEditCountFromProfile(profile) {
        let editingActivity = profile.editingActivity;
        return editingActivity[editingActivity.length-1];
    }

    function getEditCountByTypeFromProfile(profile, type) {
        let edits = profile.editsByType.find(edits => edits.key === type);
        return edits ? edits.value : -1;
    }

    function getEditCountByDayFromProfile(profile, day) {
        let editingActivity = profile.editingActivity;
        return editingActivity[editingActivity.length-day];
    }

    // Handle messages from the page.
    function receiveMessage(event) {
        let msg;

        try {
            msg = JSON.parse(event.data);
        }
        catch (err) {
            // Ignore errors
        }

        if (msg && msg[0] === 'wmeEHGetCounts') {
            let userName = msg[1];
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://www.waze.com/user/editor/${userName}`,
                onload: function(res) {
                    let profile = getEditorProfileFromSource(res.responseText);
                    window.postMessage(JSON.stringify(['wmeEHUpdateUi',[getEditCountFromProfile(profile), getEditCountByDayFromProfile(profile,2), getEditCountByDayFromProfile(profile,3)]]),'*');
                }
            });
        }
    }

    let wmeEH_Injected_script = GM_addElement('script', {
        textContent: "" + wmeEH_Injected.toString() + " \n wmeEH_Injected();"
    });

    // Listen for events coming from the page script.
    window.addEventListener('message', receiveMessage);
})();