NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Duolingo Hide Completed Skills // @namespace http://tampermonkey.net/ // @version 1.3 // @description A Duolingo userscripts that toggle hide/show completed skills // @author Ponsheng // @match https://www.duolingo.com/learn // @match https://preview.duolingo.com/learn // @grant none // @require http://code.jquery.com/jquery-3.4.1.min.js // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // ==/UserScript== /* jshint esversion: 6 */ /* globals $, jQuery */ var IsHiding = true; var IsDebug = false; var IsDP = false; // debug print // TODO There is a bug when complete a skill, it just disapear with another incomplete skill. // > seems not happened for I times // TODO Reduce monitor sensitivity, reduce check call // Selectors const section_selector = "div[data-test='tree-section']"; const skill_selector = "div[data-test='skill']"; const skill_tree_selector = "div[data-test='skill-tree']"; const global_practice_selector = "a[data-test='global-practice']"; const circle_selector = "div > svg > > path"; // under skill icon const skill_crown_selector = " div[data-test='level-crown']"; const skill_icon_selector = " div[data-test='skill-icon']"; const skill_icon_child_seltr = skill_icon_selector + " > div"; const skill_static_icon_seltr = skill_icon_selector + " div > svg[viewBox] > image"; const complete_circle_color = "#ffd900"; // gold color, gray for incomplete ones const selectors = [ 'section_selector', section_selector, 'skill_selector', skill_selector, 'skill_crown_selector', skill_selector + skill_crown_selector, 'skill_icon_selector', skill_selector + skill_icon_selector, 'skill_tree_selector', skill_tree_selector, 'global_practice_selector', global_practice_selector ]; const completed_ico_cls_set = new Set(); const page_url = "/learn"; // Options for the observer const observer_config = { childList: true, subtree: true }; // Target to search skills var target_body; // Newest skill node // FIXME what's this for var newest_skill; function HideNode(node) { node.hide(); } function ShowNode(node) { node.show(); } function mylog(s) { console.log("[HCS] " + s); } function DP(s) { if (IsDP) { mylog(s); } } function Toggle(target) { //var t0 = performance.now() var ret; if (IsHiding) { ret = ActOnTarget(target, HideNode); mylog("Hide " + ret + " nodes"); } else { ret = ActOnTarget(target, ShowNode); mylog("Show " + ret + " nodes"); } //var t1 = performance.now() //mylog("Call to HCS Toggle took " + (t1 - t0) + " milliseconds.") return ret; } function IsAllSkillComplete(node) { var skills = node.find(skill_selector); var completed_count = 0; var total_count = skills.length; /* if ((skills.length != skill_crowns.length) || (skills.length != skill_icons)) { mylog("Error: count of skills, crowns, icons are not matched"); return false; }*/ skills.each(function(index, element) { // For debug: Print name of the skill. (beware of the floating class name) //var name = $(this).find("div._2OhdT").last().html(); var classes = $(element).find(skill_icon_child_seltr).first().attr("class"); // Check if there is crown level var crown = $(element).find(skill_crown_selector); if (crown.length == 0) { // Invalid skill return false; } // Complete skill if level = 5 var level = crown.html(); if (level == 5) { // Max level completed_ico_cls_set.add(classes); completed_count += 1; return; } // Complete skill if surrounded by gold circle (deprecated) // Check the outter circle color: complete -> gold, incomplete -> gray var circle = $(element).find(circle_selector).first(); var circle_color = circle.attr('fill'); //DP("Skill " +name + " has circle of color " + circle_color); if (circle_color == complete_circle_color) { completed_count += 1; completed_ico_cls_set.add(classes); return; } // Complete skill if there is a viewbox image var icon = $(element).find(skill_static_icon_seltr); if (icon.length > 0) { // Check if class match if (completed_ico_cls_set.has(classes)) { completed_count += 1; } } }); /* if (completed_count != 0) { mylog("skills: " + total_count + ", crowns:" + completed_count); }*/ if (total_count == completed_count) { return true; } return false; } // Show / hide completed sections depends on 'IsHiding' function ActOnTarget(target, action) { // TODO Add timing var total_count = 0; // If newest skill in target found var newest_found = false; // Check sections var sections = $(section_selector); if (sections == null) { mylog("Error, section not found"); } DP("Start checking sections"); sections.each(function(index, element) { DP("Section check section: " + index); // Remove all hline var hlines = $(this).find("div > hr"); hlines.each(function(index, element) { action($(this)); }); if (IsAllSkillComplete($(this))) { action($(this)); total_count += 1; DP("Section " + index + "all done"); return; } // Hide parent (row) if parent's child are all completed var all_skills = $(this).find(skill_selector); all_skills.each(function(index, element) { var parent_row = $(this).parent(); if (IsAllSkillComplete(parent_row)) { action(parent_row); DP("Row completed"); } total_count += 1; }); }); return total_count; } function ToggleBtnClickAction(BtnEvent) { if (IsHiding) { IsHiding = false; } else { IsHiding = true; } Toggle(target_body); } function InsertBtn() { if ($('#HCSToggleBtn').length != 0) { // Inserted return; } var gp_btn = $(global_practice_selector); var floating_div = gp_btn.parent(); var btn = $('<button>HCS</button>') .attr('class', 'HCSBtn') .attr('id', 'HCSToggleBtn') .attr('type', 'button'); floating_div.append(btn); var height = gp_btn.outerHeight(); var height2 = btn.outerHeight(); var scale = (height / height2 + 1) / 2; btn.css('transform', 'scale(' + scale + ')'); // get size of global-practice btn $("#HCSToggleBtn").on("click", ToggleBtnClickAction); mylog("Inserted buttons"); // Scroll to top if (IsHiding) { $('html, body').animate({ scrollTop: 0 }, 'fast'); mylog("Scroll Top"); } } // Callback function to execute when mutations are observed const ObsvrAction = function(mutationsList, observer) { var AddNew = false; // Use traditional 'for loops' for IE 11 for (let mutation of mutationsList) { if (mutation.type === 'childList') { // A child node has been added or removed if (mutation.addedNodes.length > 0) { AddNew = true; } if (mutation.removedNodes) { // TODO detect the node is deleted // https://stackoverflow.com/questions/44935865/detect-when-a-node-is-deleted-or-removed-from-the-dom-because-a-parent-was } } } if (!AddNew) { return; } // Use url to determine toggle or not if (window.location.pathname.localeCompare(page_url) != 0) { return; } if (Toggle(target_body) > 0) { InsertBtn(); } }; // TODO auto scroll after exit from class? var observer = new MutationObserver(ObsvrAction); /* position: fixed; bottom:100px; left: 60px; z-index: 9999"; */ // Button css var styles = ` .HCSBtn { display: inline-block; margin-left: 30px; padding: 6px 8px; font-size: 17px; font-weight: bold; cursor: pointer; text-align: center; text-decoration: none; outline: none; color: #fff; background-color: #1cb0f6; border: none; border-radius: 5px; box-shadow: 0 5px #1899d6; } .HCSBtn:hover {background-color: #51c5fc;} .HCSBtn:active { background-color: #1899d6; box-shadow: 0 5px #126e99; transform: translateY(4px); } `; var debug_callback; function CheckSelector() { if (window.location.pathname.localeCompare(page_url) != 0) { return; } var i; var err = false; for (i = 0; i < selectors.length; i += 2) { var name = selectors[i]; var selector = selectors[i + 1]; var count = $(selector).length; if (count == 0) { mylog(name + " selector not found"); err = true; } } if (err == true) { clearInterval(debug_callback); mylog("End debug_callback"); } else { mylog("Selectors seems fine"); } } //$(document).ready(function() { $(window).on("load", function() { mylog("Duolingo HCS enabled"); target_body = $('body')[0]; /* * FIXME observe skill_tree_selector, * the node changed after a lesson * FIX ObsvrAction first */ /* body = $('body'); var skill_tree = body.find(skill_tree_selector); // Fail safe if (skill_tree.length == 0) { mylog("no skill tree found" + skill_tree_selector); mylog("Abort!"); return; } target_body = body.find(skill_tree_selector).get(0); var hide_count = Toggle(target_body); if (hide_count > 0) { InsertBtn(); } */ // Start observing the target node for configured mutations observer.observe(target_body, observer_config); var styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); // Check selector changed if (IsDebug) { debug_callback = setInterval(CheckSelector, 60000); mylog("HCS debuging css selector"); } });