ToranSharma / Duo-Strength

Browser extension to add back the strength for each individual skill, and other useful hidden statistics.
MIT License
35 stars 6 forks source link

Duostrength for crown branch #10

Closed comproperty247 closed 5 years ago

comproperty247 commented 5 years ago

Hi again :-D

I can't upload, edit, fork and pull, or whatever in your branch for crown, I updated the js file with the last changes and add a small tweak. I put the value for example 145/330 inside the crown image. Seems nicer The "Your tree is at Level 0" I think is not working well, the array with the loop just loops ones and leave, giving level tree=0.

You can contact me if you wish via email by my username of github plus @gmail.com and I can upload the files from there.

Regards, Carlos

const GOLD = "rgb(248, 176, 45)";
const RED = "rgb(219, 62, 65)";
var languageCode = "";
var languageCodeChanged = false;
var language = "";
var languageChanged = false;
var languageLogo;

var username = "";
var userData = Object();
var newUIVersion = false;
var numBonusSkillsInTree = 0;

var rootElem;
var dataReactRoot;
var topBarDiv;
var topBarMobilePractice;

var onMainPage;

function resetLanguageFlags()
{
    // reset to be called after finished successfully displaying everything.
    // need to be ready for a change so we reset them back to false.
    languageChanged = false;
    languageCodeChanged = false;
    numBonusSkillsInTree = 0;
}

function removeStrengthBars()
{
    var bars = document.getElementsByClassName("strengthBarHolder");
    for (var bar of bars)
    {
        bar.parentNode.removeChild(bar);
    }
}

function removeNeedsStrengtheningBox()
{
    var strengthenBox = document.getElementById("strengthenBox");
    if(strengthenBox != null) // could be null if changed from old to new UI and the topOfTree div gets removed.
    {
        strengthenBox.parentNode.removeChild(strengthenBox);
    }
}

function addStrengths(strengths) // Adds strength bars and percentages under each skill in the tree.
{
    /*
        The structure of skill tree is as follows:
        <div class="_2GJb6">                                                <-- container for row of skills, has classes _1H-7I and _1--zr if bonus skill row
            <a class="Af4up" href="javascript:;">                           <-- container for individuale skill
                <div class="_2albn">
                    <div>                                                       <-- possibly new container as of 2019-03-01 holds skill icon and progress ring
                        <div class="_3zkuO _39IKr">                             <-- progress ring container
                            <div class="_2xGPj">                                <-- progress ring container
                                <svg>...</svg>                                  <-- progress ring svg
                            </div>
                        </div>
                        <span class="_1z_vo _3hKMG ewiWc _2vstG">               <-- skill icon background
                            <span class="..."></span>                           <-- skill icon
                            <div clas ="_26l3y">...</div>                       <-- skill crowns logo and number
                        </span>
                    </div>
                    <div>                                                       <-- another possibly new container as of 2019-03-01 holds skill name
                        ####################################################    <-- Strength Bar to be inserted here
                        <span class="_378Tf _3qO9M _33VdW">Skill Name</span>    <-- Skill name
                    </div>

                    ####### when skill clicked on new div below is appended ##########
                    <div class="_2EYQL _2HujR _1ZY-H gqrCU ewiWc">              <-- popup box backgorund
                        <div>...</div>                                          <-- popup box info container
                        ::after                                                 <-- popup box 'speach bubble' style arrow at top
                    </div>
                </div>
            </a>
        </div>
    */

    var skillElements = document.getElementsByClassName('Af4up'); // Af4up is class of skill containing element, may change.
    var skills = Array();
    /*
        Each element of skills array will be an array with the following information:
        0:  skill icon element (unused currently).
        1:  skill name element.
        2:  skill strength between 0 and 1.0.
    */
    var bonusElementsCount = 0;
    for (var i=0; i<skillElements.length; i++)
    {

         var elementContents = [
            skillElements[i].childNodes[0].childNodes[0],
            skillElements[i].childNodes[0].childNodes[1].getElementsByClassName("_33VdW")[0]
         ];

        /* old way of finding name element before new containers

        // name is a span element, normally it is the last element but if the skill is clicked then a new div is created with the start lesson button etc below the name plate. So need to find the correct span element.
        for (var spanElement of Array.from(skillElements[i].childNodes[0].getElementsByTagName('span')))
        {
            if (spanElement.parentNode == elementContents[0].parentNode)
            {
                elementContents.push(spanElement);
            }
        }
        */

        if (skillElements[i].parentNode.classList.contains("_1H-7I") || skillElements[i].parentNode.classList.contains("_1--zr"))
        {
            // these skill elements are in the bonus skill section.
            elementContents.push(strengths[1][bonusElementsCount]);

            bonusElementsCount ++;

            skills.push(elementContents);
        } else
        {
            // Normal skill
            elementContents.push(strengths[0][i - bonusElementsCount]);

            skills.push(elementContents);
        }
    }

    numBonusSkillsInTree += bonusElementsCount; // update number of bonus elements that were in the tree for use in strengthenBox.

    var numBarsAdded = 0

    for (var i = 0; i< skills.length; i++)
    {
        var iconElement = skills[i][0];
        var nameElement = skills[i][1];
        var name = nameElement.innerHTML;
        var strength = skills[i][2]*1.0;

        if(document.getElementsByClassName("strengthBarHolder").length == numBarsAdded) // if we have only the number of bars added this time round, keep adding new ones.
        {
            var strengthBarHolder = document.createElement("div");
            strengthBarHolder.className = "strengthBarHolder";
            strengthBarHolder.style['width'] = "100%";
            strengthBarHolder.style['padding'] = "0 5%";

            nameElement.parentNode.insertBefore(strengthBarHolder, nameElement);

            var strengthBar = document.createElement("div");
            strengthBar.className = "strengthBar";
            strengthBar.id = name + "StrengthBar";
            strengthBar.style['display'] = "inline-block";
            strengthBar.style['width'] = (strength*75)+"%";
            strengthBar.style['height'] = "1em";
            strengthBar.style['backgroundColor'] = (strength == 1.0 ? GOLD : RED);
            strengthBar.style['borderRadius'] = "0.5em";
            strengthBar.style['verticalAlign'] = "text-top";

            var strengthValue = document.createElement("div");
            strengthValue.className = "strengthValue";
            strengthValue.id = name + "StrengthValue";
            strengthValue.style['display'] = "inline-block";
            strengthValue.style['width'] = ((1-strength)*75+25)+"%";
            strengthValue.style['textAlign'] = "right";
            strengthValue.innerHTML = strength*100 + "%";

            strengthBarHolder.appendChild(strengthBar);
            strengthBarHolder.appendChild(strengthValue);

            numBarsAdded ++; // added a bar so increment counter.

        } else // we already have the elements made prerviously, just update their values.
        {
            var strengthBar = document.getElementById(name + "StrengthBar");
            strengthBar.style['width'] = (strength*75)+"%";
            strengthBar.style['backgroundColor'] = (strength == 1.0 ? GOLD : RED);

            var strengthValue = document.getElementById(name + "StrengthValue");
            strengthValue.style['width'] = ((1-strength)*75+25)+"%";
            strengthValue.innerHTML = strength*100 + "%";
        }
    }
}

function displayNeedsStrengthening(needsStrengthening) // adds clickable list of skills that need strengthening to top of the tree.
{
    /* Old version where newUI had Part headings. Also mAsUf no longer seems to be used.
    var topOfTree;
    if(newUIVersion)
    {
        topOfTree = document.getElementsByClassName('_2GJb6')[0]; // top of tree is first row which has part 1.
        topOfTree.childNodes[0].style['marginBottom'] = "1em"; // reduced margin between part 1 heading an strengthenBox;
    } else
    {
        // old UI version.
        if(document.getElementsByClassName('mAsUf').length != 0)
        {
            // mAsUf is class of the container element just above tree with language name and shop button, may change.
            topOfTree = document.getElementsByClassName('mAsUf')[0].childNodes[1];
        } else
        {
            // body hasn't loaded yet so element not there.
            setTimeout(displayNeedsStrengthening(needsStrengthening), 500);
            return false;
        }
    }
    */

    // Found as of 2019-03-15 new UI version no longer has Part 1, Part 2 etc. headings
    // Trees seem to be consistant so longer need for newUIversion detection

    if(
            document.getElementsByClassName("i12-l").length != 0 &&
            document.getElementsByClassName("w8Lxd").length != 0 &&
            document.getElementsByClassName("_2GJb6").length != 0
        ) // Has the tree loaded from a page change
    {
        var skillTree = document.getElementsByClassName("i12-l")[0];
        var topOfTree = document.getElementsByClassName("w8Lxd")[0];
        // or var topOfTree = skillTree.childNodes[0]
        var firstSkillRow = document.getElementsByClassName("_2GJb6")[0];
    }
    else
    {
        // body hasn't loaded yet so element not there, lets try again after a small wait, but only if we are still on the main page.
        if(onMainPage)
        {
            setTimeout(displayNeedsStrengthening(needsStrengthening), 500);
        }
        else
        {
            // swtiched away before we got a chance to try again.
        }
        return false;
    }

    var strengthenBox; // will be a div to hold list of skills that need strengthenening
    var needToAddBox = false;
    if (document.getElementById("strengthenBox") == null) // if we haven't made the box yet, make it
    {
        needToAddBox = true;
        strengthenBox = document.createElement("div");
        strengthenBox.id = "strengthenBox";
        strengthenBox.style['textAlign'] = "left";
        strengthenBox.style['marginBottom'] = "2em";
    }
    else
    {
        strengthenBox = document.getElementById("strengthenBox");
    }

    var numSkillsToBeStrengthened = needsStrengthening[0].length +needsStrengthening[1].length;

    strengthenBox.innerHTML = "The following " + numSkillsToBeStrengthened +
                                ((needsStrengthening[0].length + numBonusSkillsInTree != 1) ? " skills need": " skill needs") +
                                " strengthening: <br/>";

    for (var i = 0; i < numSkillsToBeStrengthened - 1; i++)
    {
        if (i < needsStrengthening[0].length)
        {
            // index is in normal skill range
            strengthenBox.innerHTML += "<a href='/skill/" +
                                    languageCode + "/" +
                                    needsStrengthening[0][i]['url_title'] +
                                    ((needsStrengthening[0][i]['progress_v3']['level'] == 5)? "/practice'>":"'>" ) + // 5 crown skill doesn't decay AFAIK so needless but included JIC.
                                    needsStrengthening[0][i]['title'] + "</a>, ";
        } else
        {
            // index has past normal skills so doing bonus skills now.
            bonusSkillIndex = i - needsStrengthening[0].length;
            strengthenBox.innerHTML += "<a href='/skill/" +
                                    languageCode + "/" +
                                    needsStrengthening[1][bonusSkillIndex]['url_title'] +
                                    ((needsStrengthening[1][bonusSkillIndex]['progress_v3']['level'] == 1)? "/practice'>":"'>" ) + // 1 crown bonus skill does decay but is on practice not lessons.
                                    needsStrengthening[1][bonusSkillIndex]['title'] + "</a>, ";
        }

    }
    strengthenBox.innerHTML = strengthenBox.innerHTML.substring(0, strengthenBox.innerHTML.length - 2);
    strengthenBox.innerHTML += (numSkillsToBeStrengthened > 1) ? " & ": "";
    if(needsStrengthening[1].length > 0)
    {
        // last skill to be displayed is a bonus skill
        strengthenBox.innerHTML += "<a href='/skill/" +
                                        languageCode + "/" +
                                        needsStrengthening[1][needsStrengthening[1].length - 1]['url_title'] +
                                        ((needsStrengthening[1][needsStrengthening[1].length - 1]['progress_v3']['level'] == 1)? "/practice'>":"'>" ) +
                                        needsStrengthening[1][needsStrengthening[1].length - 1]['title'] + "</a>";
    } else
    {
        // last skill to be displayed is a normal skill
        strengthenBox.innerHTML += "<a href='/skill/" +
                                        languageCode + "/" +
                                        needsStrengthening[0][needsStrengthening[0].length -1]['url_title'] +
                                        ((needsStrengthening[0][needsStrengthening[0].length -1]['progress_v3']['level'] == 5)? "/practice'>":"'>" ) +
                                        needsStrengthening[0][needsStrengthening[0].length -1]['title'] + "</a>";
    }
    if(needToAddBox)
    {
        skillTree.insertBefore(strengthenBox, firstSkillRow);
    }
}

function displayCrownsBreakdown(crownLevelCount, maxCrownCount)
{
    /*
        Side bar HTML structure:
        <div class="_2_lzu">                            <-- Side bar container div
            <div class="aFqnr _1E3L7">                  <-- Crown Level Box container div
                <h2>Crown Level</h2>
                <div class="_1kQ6y">                    <-- Crown image and count container div
                    <img class="_2vQZX" src="..." />    <-- Crown image svg
                    <div class="nh1S1">CROWNCOUNT</div> <-- Crown count text
                    ################################### <-- /Maximum crown count will be put here.
                </div>
                ####################################### <-- Crown Level breakdown will be put here.
            </div>
            <div class="-X3R5 _2KDjt">...</div>         <-- Advert/ disable ad blocker
            <div class="_21w25 _1E3L7">...</div>        <-- Daily Goal, xp graph & practice button
            <div class="_2SCNP _1E3L7">...</div>        <-- Achievements
            <div class="a5SW0">...</div>                <-- Friends list
            <div class="a5SW0">...</div>                <-- Duolingo Social Media links
        </div>

        Note a5SW0 seems to correspond with clear background, and _1E3L7 with whtie background.
        _1E3L7 is in class name of main tree, which has a white background.
    */

    var treeLevel = 0;
/*NEEDS IMPROVEMENT!!! treeLevel is 0  *********************************/
    var i = 0;
    while (crownLevelCount[i] == 0 && i < 6)
    {
        treeLevel++;
        i++;
    }
/****************************************************/

    var crownLevelContainer = document.getElementsByClassName('aFqnr _1E3L7')[0];
    var crownTotalContainer = crownLevelContainer.childNodes[1];

    var maximumCrownCountContainer = document.createElement("span");
    maximumCrownCountContainer.id = "maxCrowns";
    maximumCrownCountContainer.innerHTML = "/" + maxCrownCount;
/*
    maximumCrownCountContainer.style =  "position:absolute;"
                                    +   "top: 50%;"
                                    +   "right: 0;"
                                    +   "margin-top: 5px;"
                                    +   "font-size: 36px;"
                                    +   "font-weight: 700;"
                                    +   "transform: translateY(-50%);";
*/

    var breakdownContainer = document.createElement("div");
    breakdownContainer.id = "crownLevelBreakdownContainer";
    breakdownContainer.style = "margin-top: 1em; text-align: left;";

    var treeLevelContainer = document.createElement("div");
    treeLevelContainer.id = "treeLevel";
    treeLevelContainer.style = "display: inline-block";
    treeLevelContainer.innerHTML = treeLevel;

    var breakdownList = document.createElement("ul");

    var imgContainer = document.createElement("div");
    imgContainer.style = "position: relative;"
                        +"vertical-align: middle;"
                        +"display: inline-block;"
                        +"height: 1.3em;"
                        +"width: 1.3em;"
                        +"top: -0.15em;";

    var levelContainer = document.createElement("div");
    levelContainer.style =  "position: absolute;"
                        +   "top: 50%;"
                        +   "left: 50%;"
                        +   "transform: translateX(-50%) translateY(-50%);"
                        +   "margin-top: 10%;"
                        +   "z-index: 2;"
                        +   "font-size: 0.75em;"
                        +   "color: white;";

    var crownImg = document.createElement("img");
    crownImg['alt'] = "crown";
    // Class name _2PyWM used for other samll crowns on skills. Corresponds to height & width 100% and z-index 1.
    crownImg.style = "height: 100%; width: 100%; z-index: 1;";
    crownImg['src'] = "//d35aaqx5ub95lt.cloudfront.net/images/crown-small.svg";

    imgContainer.appendChild(crownImg);
    imgContainer.appendChild(levelContainer);

    if(document.getElementsByClassName("crownLevelItem").length == 0) // We haven't added the breakdown data yet, so lets add it.
    {
        for(var crownLevel = 0; crownLevel < crownLevelCount.length; crownLevel++)
        {
            var skillCount = crownLevelCount[crownLevel];
            var crownCount = skillCount * crownLevel;

            levelContainer.id = "crownLevel" + crownLevel + "Count";
            levelContainer.innerHTML = crownLevel;

            var breakdownListItem = document.createElement("li");
            breakdownListItem.className = "crownLevelItem";
            breakdownListItem.innerHTML = skillCount + " skill"+ ((skillCount == 1 )?"":"s") + " at ";

            breakdownListItem.appendChild(imgContainer);

            breakdownListItem.innerHTML += " = " + crownCount + " crown" + ((crownCount == 1 )?"":"s");

            breakdownList.appendChild(breakdownListItem);
        }
//lest define the font and the MAx level count
var crownTotalContainer = document.getElementsByClassName('nh1S1')[0];
crownTotalContainer.style.fontSize="22px";

        crownTotalContainer.appendChild(maximumCrownCountContainer);

        breakdownContainer.appendChild(document.createElement("p"))
        breakdownContainer.lastChild.style = "text-align: center;";
        breakdownContainer.lastChild.innerHTML = "Your tree is at Level ";
        breakdownContainer.lastChild.appendChild(treeLevelContainer);
        breakdownContainer.appendChild(breakdownList);
        crownLevelContainer.appendChild(breakdownContainer);
    }
    else // We have already added the breakdown data, just update it.
    {
        for(var crownLevel = 0; crownLevel < crownLevelCount.length; crownLevel++)
        {
            levelContainerElement = document.getElementById("crownLevel" + crownLevel + "Count");
            levelContainerElement.innerHTML = crownLevel;
        }

        document.getElementById("maxCrowns").innerHTML = "/" + maxCrownCount;
        document.getElementById("treeLevel").innerHTML = treeLevel;
    }
}

function httpGetAsync(url, responseHandler)
{
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function()
    { 
        if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
            responseHandler(xmlHttp.responseText);
    }
    xmlHttp.open("GET", url, true); // true for asynchronous 
    xmlHttp.send(null);
}

function getStrengths() // parses the data from duolingo.com/users/USERNAME and extracts strengths and skills that need strengthening
{
    /*
        Data comes formatted as such:
        {
            'language_data': {
                'es': {
                    ...
                    skills          : [...],
                    bonus_skills    : [...],
                    ...
                }
            }
        }
        each skill in either skills or bonus_skills has a number or properties including 'strength', 'title', 'url_title', 'coords_x', 'coords_y'.
    */

    var strengths = [[],[]];    // will hold array of the strength values for each skill in tree in order top to bottom, left to right and array of strengths of bonus skills. values between 0 and 1.0 in 0.25 steps.
    var needsStrengthening = [[],[]]; // will hold the objects for the skills that have strength < 1.0 and the bonus skills that have strength < 1.0.
    var crownLevelCount = Array(6).fill(0); // will hold number skills at each crown level, index 0 : crown 0 (not finished), index 1 : crown 1, etc.

    languageCode = Object.keys(userData['language_data'])[0]; // only one child of 'language_data', a code for active language.

    var skills = userData['language_data'][languageCode]['skills']; // skills appear to be inconsistantly ordered so need sorting for ease of use.
    var bonusSkills = userData['language_data'][languageCode]['bonus_skills'];
//var bonusSkills = userData['language_data'][Object.keys(userData['language_data'])[0]]['bonus_skills'];

    function sortSkills(skill1,skill2)
    {
        if (skill1['coords_y'] < skill2['coords_y']) // x above y give x
        {
            return -1;
        } else if (skill1['coords_y'] > skill2['coords_y'])// x below y give y
        {
            return 1;
        } else // x and y on same level
        {
            if (skill1['coords_x'] < skill2['coords_x']) // x to left of y give x
            {
                return -1;
            } else // x to right of y give y
            {
                return 1;
            }
        }
    }

    skills.sort(sortSkills);
    bonusSkills.sort(sortSkills);

    for (var skill of skills)
    {
        strengths[0].push(skill['strength']);

        if(skill['strength'] != 1 && skill['strength'] != 0)
        {
            //Add to needs strengthening if nog at 100% and not at 0% i.e. not started
            needsStrengthening[0].push(skill);
        }

        crownLevelCount[skill['progress_v3']['level']]++;
    }

    for (var bonusSkill of bonusSkills)
    {
        strengths[1].push(bonusSkill['strength']);
        if(bonusSkill['strength'] != 1 && bonusSkill['strength'] != 0)
        {
            //Add to needs strengthening if nog at 100% and not at 0% i.e. not started
            needsStrengthening[1].push(bonusSkill);
        }

        crownLevelCount[bonusSkill['progress_v3']['level']]++;
    }

    addStrengths(strengths); // call function to add these strengths under the skills

    if (needsStrengthening[0].length+needsStrengthening[1].length !=0)
    {
        displayNeedsStrengthening(needsStrengthening); // if there are skills needing to be strengthened, call function to display this list
    }

    displayCrownsBreakdown(crownLevelCount, skills.length*5 + bonusSkills.length); // call function to add breakdown of crown levels under crown total.

    // All done displaying what needs doing so let reset and get ready for another change.
    resetLanguageFlags();
}

function handleDataResponse(responseText)
{
    userData = JSON.parse(responseText); // store response text as JSON object.
    newDataLanguageCode = Object.keys(userData['language_data'])[0];
    if((!languageCodeChanged) && languageChanged && newDataLanguageCode == languageCode)
    {
        // languageCode hasn't been changed yet but we have changed langauge but the data isn't up to date yet.
        // so request the data again after a little wait, but only if still on the main page.
        if(onMainPage)
        {
            setTimeout(function() {httpGetAsync(encodeURI(window.location+"users/"+username), handleDataResponse);}, 100);
            //setTimeout(function() {httpGetAsync("/users/"+ username, handleDataResponse);}, 100);
        }
    }
    else {
        languageCodeChanged = true;
        getStrengths(); // actual processing of the data.
    }
}

function requestData() // requests data for actively logged in user.
{
    if (!(Object.keys(userData).length === 0 && userData.constructor === Object) && (!languageChanged))
    {
        // If there is already userData and not changing language, display current data while requesting new data.
        getStrengths(userData);
    }
    if(document.getElementsByClassName("_2R9gT").length != 0) // Check if there is a username element
    {
        username = document.getElementsByClassName("_2R9gT")[0].innerHTML;
        httpGetAsync(encodeURI(window.location+"users/"+username), handleDataResponse); // asks for data and async calls handle function when ready.
    } else
    {
        // user not logged in.
    }
}

function checkUIVersion(){
    if (document.getElementsByClassName('_1bcgw').length != 0)
    {
        // Seem to be using new version of tree with the pentagonal checkpoint nodes
        newUIVersion = true;
    } else
    {
        newUIVersion = false;
    }
}

// detect changes to class using mutation of attributes, may trigger more than necessary but it catches what we need.
var childListMutationHandle = function(mutationsList, observer)
{
    for (var mutation of mutationsList)
    {
        if(mutation.target == rootElem)
        {
            // root child list has changed so dataReactRoot has probably been replaced, lets redefine it.
            dataReactRoot = rootElem.childNodes[0];
        }
        if(dataReactRoot.childNodes[1].className ==  "_6t5Uh")
        {
            // Top bar div has class for main tree page or words page.
            topBarDiv = dataReactRoot.childNodes[1]
            languageChanged = false; // language hasn't changed this update
            init();
        }
    }
};

var classNameMutationHandle = function(mutationsList, observer)
{
    for (var mutation of mutationsList)
    {
        if(topBarDiv.className == "_6t5Uh" && document.getElementsByClassName("_2XW92").length == 0) // _6t5Uh means we are on body on main page or words page. no _2XW92 means not on words page. So we are on main page.
        {
            onMainPage = true;
            if (language != "")
            {
                // language has previously been set so not first time on home page.
                 if (language != document.getElementsByClassName("_3I51r _2OF7V")[0].childNodes[1].innerHTML)
                {
                    // language has just changed so set flag to true
                    languageChanged = true;
                    language = document.getElementsByClassName("_3I51r _2OF7V")[0].childNodes[1].innerHTML;
                    // as the language has just changed, need to wipe the slate clean so no old data is shown after change.
                    removeStrengthBars();
                    removeNeedsStrengtheningBox();
                } else
                {
                    // language hasn't just changed set flag to false
                    languageChanged = false;
                }
                checkUIVersion(); // here for case of switching language with different UI versions
                requestData(); // call on attribute change
            } else
            {
                //language had not been previously set so first time on homepage
                init();
            }
        }
        else
        {
            onMainPage = false;
        }
    }
};

var classNameObserver = new MutationObserver(classNameMutationHandle);
var childListObserver = new MutationObserver(childListMutationHandle);

function init()
{
    rootElem = document.getElementById("root"); // When logging in child list is changed.
    dataReactRoot = rootElem.childNodes[0]; // When entering or leaving a lesson children change there is a new body so need to detect that to know when to reload the bars.

    childListObserver.observe(rootElem,{childList: true});
    childListObserver.observe(dataReactRoot,{childList: true});

    if(dataReactRoot.childNodes.length == 1) // If there is only one child of dataReactRoot then we are on the log in page.
    {
        // On login page so cannot continue to turn rest of init process.
        onMainPage = false;
        return false;
    }

    var mainBodyElemIn3rd = !dataReactRoot.childNodes[1].classList.contains("_3MLiB") && dataReactRoot.childNodes[2].classList.contains("_3MLiB");
    // Main body container element has class _3MLiB. If in second place, there is no topbar Div, if it is in thrid place, then second should be topBarDiv.

    if(mainBodyElemIn3rd) // If main body element is in 3rd place then we are not in a lesson.
    {
        topBarDiv = dataReactRoot.childNodes[1];

        // topBarMobilePractice either has class _2XW92 or _3IyDY _1NQPL.
        if(topBarDiv.getElementsByClassName("_3IyDY _1NQPL").length != 0)
        {
            topBarMobilePractice = topBarDiv.getElementsByClassName("_3IyDY _1NQPL")[0]; // Class for when on homepage, store and labs
        }
        else if(topBarDiv.getElementsByClassName("_2XW92").length != 0)
        {
            topBarMobilePractice = topBarDiv.getElementsByClassName("_2XW92")[0]; // Class for when on words page
        }
        else
        {
            // Element not found or has a class we are not expecting. It should be the 2nd child of topBarDiv so lets just set it as that and hope for the best.
            topBarMobilePractice = topBarDiv.childNodes[1];
        }

        classNameObserver.observe(topBarDiv,{attributes: true}); // Observing to see if class of topBarDiv changes to tell if we have switched between main or words page and store or labs page.
        classNameObserver.observe(topBarMobilePractice,{attributes: true}); // Observing to see if class of topBarMobilePractice changes to tell if we have swtiched between main and words page.

        if(topBarDiv.className == "_6t5Uh" && document.getElementsByClassName("_2XW92").length == 0) // if we are on the homepage. _2XW92 is class of mobile view practice button when on words page. On home page it is _3IyDY _1NQPL.
        {
            onMainPage = true;
            if(languageLogo != document.getElementsByClassName("_3I51r _2OF7V")[0].childNodes[0])
            {
                languageLogo = document.getElementsByClassName("_3I51r _2OF7V")[0].childNodes[0];
                classNameObserver.observe(languageLogo,{attributes: true});
            }
            language = document.getElementsByClassName("_3I51r _2OF7V")[0].childNodes[1].innerHTML;
            checkUIVersion();
            requestData();
        }
        else if(topBarDiv.className == "_6t5Uh" && document.getElementsByClassName("_2XW92").length != 0) // if we are on the words page
        {
            // We are on the words page. Don't need to do anything further, we have set up the observer to see if we go back to the main page from here.
            onMainPage = false;
        }
    } else
    {
        // page we are on is most likely a lesson, and we got here from a link in the strengthenBox.
        onMainPage = false;
    }
}

document.body.onload = init(); // call function to start display sequence on first load

//observer.disconnet(); can't disconnect as always needed while page is loaded.
ToranSharma commented 5 years ago

Thanks for the suggestion about putting the maxCrownCount in the crown itself. I had thought about it but wasn't sure it would look a bit cramped. Here a side by side of the original, your version with it in the crown, a larger crown with the font kept its original size, and a slightly larger crown with the total below (more like a traditional fraction).

crowns breakdown versions

As for the crown level, I will have to have look at what could be causing it to report level 0 incorrectly. The only problems I can possibly foresee are how bonus skill crowns are counted in the breakdown, they only go up to 1 crown for some reason. It may be the case that a bonus skill that hasn't been bought yet will be counted as crown 0. I will make sure that bonus skills are handled separately.

comproperty247 commented 5 years ago

Hi Toran,

Why don't you create the options page and add those several layouts to extension and let the user decide what is best for him? ;-)

Just found now, out some changes on DL, the class "nh1S1" doesn't exist anymore for Crown count text. is like this now

200

Perhaps is better to use class "_2eJB1" for now on

About the crown level.. I really don't know what is the main idea you want with this. Because is just crowns on DL...

The issue is like this, I have this value: crownLevelCount=[36, 1, 2, 1, 0, 28]

In the while loop will be False in the first value of the array and will finished and give 0 while (crownLevelCount[i] == 0 && i < 6)

Hope this helps you in some how

Regards, Carlos

comproperty247 commented 5 years ago

I can't edit my own response so I will add the rest of the answer here :-/

If you look on the data return from DL, there is a value which shows the number of levels of each skill "progress_v3.level" as you use and another that is "num_levels".

"num_levels" is the quantity of crowns, USSUALY are 5 but perhaps in bonus skill is another value.

So this can be used to make the math correctly and not use the skills.length5 but by "num_levels".

But please check if is this value "num_levels", I looked on the data some day ago.

Just for curiosity, which Language learning has the bonus skill? Italian seems not to have

Regards, Carlos

ToranSharma commented 5 years ago

Hi Carlos,

Why don't you create the options page and add those several layouts to extension and let the user decide what is best for him? ;-)

It was my intention to give the user the option of enabling any of the features of the extension, so giving a choice of the layout could be included in that. Currently I am using the layout you suggested, the second option in the image I posted previously.

Just found now, out some changes on DL, the class "nh1S1" doesn't exist anymore for Crown count text. is like this now

200 Perhaps is better to use class "_2eJB1" for now on

I had noticed the class change too, bb08809d37c6d614dd36748748270ba71abd0412 & c60153dd059d7e9cf9e1770a847cce43cb31d224 adress this.

About the crown level.. I really don't know what is the main idea you want with this. Because is just crowns on DL...

My aim with the crowns breakdown in to give a sense of what to crown progression is throughout the tree. For example, it shows how many skills you have left at 1 crown, maybe you want to focus on getting those up to 2 crowns. While the crown level may not be important to many people, it does represent the progress of a users tree. Currently, all that is shown is the total, and you would have to scroll down though the whole tree to get a sense of how many skills are at each level. The breakdown just displays this under the total at the top of the page.

Again, if a user is not interested or thinks it unhelpful, they will be able to disable it. I just need to implement these options.

The issue is like this, I have this value: crownLevelCount=[36, 1, 2, 1, 0, 28]

In the while loop will be False in the first value of the array and will finished and give 0 while (crownLevelCount[i] == 0 && i < 6)

As a first note, the crownLevelCount array has be changed slightly as of e89a933018b7de8d128a1d64614d292fb7597856. The structure is now, [array for normal skills, array for bonus skills]. So your new array I believe would be [[36, 1, 2, 1, 0, 28], [0,0]].

The tree level represents the minimum crown level of every skill in the tree. For example, your normal skills are in crownLevelCount[0] = [36, 1, 2, 1, 0, 28], means that you have 36 skills a crown level 0 (i.e. unfinished), 1 skill at crown 1, 2 skills at crown 2, 1 skill at crown 3, no skills at crown 4, and 28 skills at crown 5. This gives a minimum of crown level 0 as you have 36 skills at level 0.

Another example would be my Swedish tree: crownLevelCount[0] = [0, 0, 0, 37, 22, 7]. Again this means, 0 skills at crown levels 0, 1 and 2; 37 skills at crown level 3, 22 at level 4 and 7 at level 5. This means that the Tree level is 3 as that is the minimum crown level that I have any skills at.

This metric mirrors what is shown on duome.eu, such as on the leaderboards for each language, see here for example: https://duome.eu/sv. In the legend on the right it shows the different tree levels, L2, L3, L4 etc.

The code to work out this level is correct I believe, so for your tree, if you have indeed not finished every skill, then the tree is at level 0 as shown.

If you look on the data return from DL, there is a value which shows the number of levels of each skill "progress_v3.level" as you use and another that is "num_levels".

"num_levels" is the quantity of crowns, USSUALY are 5 but perhaps in bonus skill is another value.

So this can be used to make the math correctly and not use the skills.length5 but by "num_levels".

But please check if is this value "num_levels", I looked on the data some day ago.

Thanks for the tip about num_levels, I had not seen this. Looking at the users/ToranSharma data for my spanish tree, it does show that num_levels for bonus skills is 1, i.e. the maximum crown level for a bonus skill is just 1 not 5 like normal skills.

I will consider using num_levels instead of 5 and 1 manually. They currently they will give the same results, and unless duolingo significantly changes how the crowns system works, the added adaptability of using num_levels is fairly low priority.

Just for curiosity, which Language learning has the bonus skill? Italian seems not to have

I am not sure for definite which languages have the bonus skills. I think it is most of the duolingo created tress, including Spanish, French and German. The skills are purchasable from the lingot store. Swedish does not have any either, I have to use a Spanish tree to test how the extension skills handle the bonus skills.

I have changed it so that the tree level does not include the bonus skills, and will make sure that bonus skill breakdowns are not displayed for languages without bonus skills.

Thanks again for your help,

Toran

comproperty247 commented 5 years ago

Hi Toran,

Ok so now I understand the crown level, so mine is 0 until I have at least first level concluded on all skills. As I am doing skill by skill will be zero almost until the end of the tree :-/ At first sight, I thought it was about the sections, but they are just 5. So, your loop with array is correct.

Yesterday I found out that Duolingo, changes the layout,sincerely I didnt like the fonts neither the 3D buttons.

I make a small change in duoStrength.js I am using on my PC. Around Line 400,I added a span element which makes the the sentence until equal sign with same width. See if you like it


            var breakdownListItem = document.createElement("li");
            breakdownListItem.className = "crownLevelItem";
//Changed line bellow
            breakdownListItem.innerHTML = '<span class="half_left" style="min-width: 95px;display: inline-block;">' + skillCount+ " skill"+ ((skillCount == 1 )?"":"s") + " at " + '</span>' ;

Regards, Carlos

comproperty247 commented 5 years ago

PS: as I can not edit again or even paste code with "insert code" button. My posts with code become a little fuzzy to understand :-/

Regards

ToranSharma commented 5 years ago

I never addressed the style change you made the the crowns break down list.

It is worth noting that I have since moved to a git-flow like branching model, and the unreleased features, including the crowns breakdown now live in the develop branch.

In the intervening time, I have changed the display of the list quite substantially. It now uses a grid layout to form regular columns and rows, in a table like manor. I also spent some time experimenting with different text aligns and settled on the current incarnation which looks as follows:

I am quite happy with how this looks.

Regards,

Toran