From 5555caa963e8527d6e3f536f58fd59bd9c715843 Mon Sep 17 00:00:00 2001 From: darkmain <7@S@GRqKQE#@w2mYpQFDwn8JJv9EPB> Date: Fri, 23 Sep 2022 23:49:11 +0200 Subject: [PATCH] rename file to polyglot.user.js --- polyglot.js => polyglot.user.js | 1218 +++++++++++++++---------------- 1 file changed, 609 insertions(+), 609 deletions(-) rename polyglot.js => polyglot.user.js (97%) diff --git a/polyglot.js b/polyglot.user.js similarity index 97% rename from polyglot.js rename to polyglot.user.js index ddebcb1..5e24fce 100644 --- a/polyglot.js +++ b/polyglot.user.js @@ -1,609 +1,609 @@ -// ==UserScript== -// @name Polyglot for Codewars -// @description User script which provides some extra functionalities to Codewars -// @version 1.13.20.1 -// @downloadURL https://github.com/hobovsky/polyglot/releases/latest/download/polyglot.js -// @updateURL https://github.com/hobovsky/polyglot/releases/latest/download/polyglot.js -// @match https://www.codewars.com/* -// @grant GM_xmlhttpRequest -// @grant GM_setValue -// @grant GM_getValue -// @grant GM_addStyle -// @grant GM_setClipboard -// @connect self -// @require http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js -// @require http://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min.js -// @require https://greasyfork.org/scripts/21927-arrive-js/code/arrivejs.js?version=198809 -// @require https://rawgit.com/notifyjs/notifyjs/master/dist/notify.js -// @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js -// ==/UserScript== - -/**************************************************************** - -WHAT IS IT? ------------ - This piece of code is a Tampermonkey user script which provides some - additional per-language filtering and display capabilities, - effectively making it easier for you to obtain POLYGLOT badge. - -WHERE CAN I DOWNLOAD IT FROM? ------------------------------ - You can get the script [here](https://github.com/hobovsky/polyglot/releases/latest/download/polyglot.js). - - HOW TO INSTALL IT? ------------------- - - Install Tampermonkey extension for your browser, - - Copy&paste the script to your scripts library. - - WHAT FEATURES DOES IT PROVIDE? ------------------------------- - - You can copy content of code boxes into clipboard. - - "Spoiler" flags are visible all the time and do not dis/re-appear - in a very annoying manner. - - Contents of "Solutions" and "Past solutions" views are displayed in - tabs by language. - - Leaderboards: "Solved kata is default leaderboard (since "Overall" - ranking does not measure anything useful). Also, leaderboards are - automatically scrolled to show your score. - - Leaderboards: Rank leaderboards utilizing Codewars API, to show users - ranked by overall rank or a language rank. - - Beta kata: uses Codwewars API to fetch and present breakdown of rank votes. - - HOW TO UNINSTALL IT? --------------------- - I haven't checked. - - KNOWN ISSUES ------------- - - Yes. - - A race condition here or there. - - Selectors, hooks and listeners used are so inefficient that your local power - plant probably doubles its coal consumption. - - WHAT CAN I DO WITH THE SCRIPT? ------------------------------- - - You are allowed to use it, unless someone authoritative (CW staff?) says you can't. - - You can modify it ONLY IF your modifications are going to bring any improvement - into the way it works, AND you are going to share improved version with CW community. - - You can send all your critical remarks to `/dev/null`, unless it's something I could - learn or otherwise benefit from - in such case, you can contact me on Codewars - Discord server. - - THIS CODE IS CRAP, LOOKS LIKE CRAP, AND WORKS LIKE CRAP! WHY? -------------------------------------------------------------- - I am really sorry if this code hurts your eyes, brain, or feelings - in any way, but I am not a professional HTML developer and each and - every technique present here (JavaScript, jQuery, TamperMonkey, - CW API) I've used for the first time. - - CREDITS -------- - - Codewars - - TamperMonkey - - StackOverflow - - jQuery - - notify.js - - Chart.js - -****************************************************************/ - -/* globals jQuery, $, waitForKeyElements, App */ -var $ = window.jQuery; -const JQUERYUI_CSS_URL = '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/themes/dark-hive/jquery-ui.min.css'; - -$.noConflict(); - -const DROPDOWN_STYLE_CLASS = 'mt-1 block w-full pl-3 pr-10 py-2 text-base dark:bg-gray-800 border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-cgray-300 dark:focus:ring-cgray-600 focus:border-cgray-300 dark:focus:border-cgray-600 sm:text-sm rounded-md'; - -jQuery("head").append(``); - -function isElementInViewport(el) { - if (typeof jQuery === "function" && el instanceof jQuery) { - el = el[0]; - } - - let rect = el.getBoundingClientRect(); - let wnd = jQuery(window); - return rect.top >= 0 && rect.left >= 0 && rect.bottom <= wnd.height() && rect.right <= wnd.width(); -} - -let userName = null; -function getUserName() { - if (!userName) { - let url = jQuery("#header_profile_link").attr("href"); - url = url.split("/"); - userName = url[url.length - 1]; - console.info("Detected username: " + userName); - } - return userName; -} - -let css = ` -.glotBtnCopy { - margin-top: 1px; - margin-right: 2px; - float: right; - padding: 5px 10px 5px; - opacity: 0.7; -} -`; -GM_addStyle(css); - -function getViewedKataId() { - let currentUrl = window.location.href; - let parts = currentUrl.split('/'); - let kataPartIdx = parts.indexOf('kata'); - if(kataPartIdx == -1 || kataPartIdx == parts.length - 1) { - return null; - } - return parts[kataPartIdx + 1]; -} - -function isBetaKata() { - let rankTag = jQuery("h4").siblings('div.tag').text(); - return rankTag === 'Beta' -} - -function getRankColor(rank) { - switch(rank) { - case 1: case 2: - case 3: case 4: return 'black'; - case -1: case -2: return "purple"; - case -3: case -4: return 'blue'; - case -5: case -6: return 'yellow'; - default: return 'white'; - } -} - -function getRankValue(rank) { return Math.abs(rank) } -function getRankDenom(rank) { return rank < 0 ? "kyu" : "dan" } -function getRankLabel(rank) { return `${getRankValue(rank)} ${getRankDenom(rank)}`; } - -function fetchAborted() { - jQuery.notify("Fetch aborted.", "info"); -} -function fetchError() { - jQuery.notify("ERROR!", "error"); -} - -/******************************** - * Clipboard * - *********************************/ - -const btnCaption = "Copy to clipboard"; -function copyToClipboardFunc(codeElem) { - return function() { - let code = codeElem.text().substring(btnCaption.length); - GM_setClipboard(code, "text"); - jQuery.notify(code.length + " characters copied to clipboard.", "info"); - }; -} - -function addCopyButton(codeElem, attempt = 10) { - //TODO: syntax highlighter treats buttons added by me as regular code which should - //be highlighted, and destroys them if they are added before highlighting is completed. - //To avoid such situation, I wait 1 second to let highlighter complete its job, - //and add button after this time elapses. However, better solution might be just to listen - //for removal of the buton and simply re-add it? - setTimeout(function() { - codeElem = jQuery(codeElem); - - if (codeElem.parents("#description").length) { - return; - } - - if (codeElem.parent("pre").length && !codeElem.children("button.glotBtnCopy").length) { - if (codeElem.children("span").length) { - codeElem.prepend(""); - let btn = codeElem.children("button").first(); - btn.on("click", copyToClipboardFunc(codeElem)); - } else if (attempt) { - console.info("Highlight delay..."); - addCopyButton(codeElem, attempt - 1); - } - } - }, 1000); -} - -/******************************** - * Tabbed Languages * - *********************************/ -let tabIdSerial = 1; -function tabidizeByLanguage(solutionPanel) { - solutionPanel = jQuery(solutionPanel); - let kataTitle = solutionPanel.children("div.item-title:first").first(); - kataTitle.after('
'); - let langTabsList = kataTitle - .next("div.langTabs") - .children("ul.tabsList:first") - .first(); - - let langs = solutionPanel.children("h6"); - langs.each((i, langHeader) => { - langHeader = jQuery(langHeader); - langTabsList.append('
  • ' + langHeader.text().slice(0, -1) + "
  • "); - let contentElems = langHeader.nextUntil("h6"); - contentElems.wrapAll('
    '); - }); - langs.remove(); - let langTabs = kataTitle.next("div.langTabs"); - solutionPanel - .children("div.langTab") - .detach() - .appendTo(langTabs); - let tabsPanel = langTabs.tabs(); -} - -function tabidizePastSolutions(liElem) { - let solutionPanel = jQuery(liElem) - .children("div:first") - .first(); - - let langDivs = solutionPanel.children(); - langDivs.wrapAll('
    '); - let langTabs = solutionPanel.children("div.langTabs").first(); - langTabs.prepend(''); - let langTabsList = langTabs.children("ul.tabsList:first").first(); - - let langs = langDivs.children("p"); - langs.each((i, langHeader) => { - langHeader = jQuery(langHeader); - langTabsList.append('
  • ' + langHeader.text() + "
  • "); - langHeader.parent().attr("id", "langTab-" + tabIdSerial++); - }); - langs.remove(); - let tabsPanel = langTabs.tabs(); -} - -/******************************** - * Rank Leaderboards * - *********************************/ -function makeRankBadge(rank) { - return `
    ${getRankLabel(rank)}
    ` -} - -function getCurrentPlayerClass(leaderboardEntry) { - return leaderboardEntry.username == getUserName() ? 'class="is-current-player"' : "" -} - -function createLeaderboardRow(boardEntry) { - - let row = `#${boardEntry.position}${makeRankBadge(boardEntry.rank)} ${boardEntry.username}${boardEntry.score}` - return row; -} - -function fillLeaderboardRows(collected) { - const leaderboardRows = jQuery('tr[data-username]'); - leaderboardRows.remove(); - - var rowsHtml = collected.sort((r1, r2)=>r2.score-r1.score).map(createLeaderboardRow).join(''); - jQuery('div.leaderboard table tbody').append(rowsHtml); -} - -function leaderboardDownloaded(resp) { - if (resp.readyState !== 4) return; - let cwResp = resp.response; - const leaderboardEntries = cwResp.data; - const {lang, collected, page} = resp.context; - collected.push(...leaderboardEntries); - - if(page < 10 && leaderboardEntries && leaderboardEntries.length) { - buildLeaderboard(lang, collected, page+1); - } else { - fillLeaderboardRows(collected); - } -} - -function buildLeaderboard(lang, collected, page=1) { - - let url = `/api/v1/leaders/ranks/${lang}?page=${page}`; - - let opts = { - method: "GET", - url: url, - onreadystatechange: leaderboardDownloaded, - onabort: fetchAborted, - onerror: fetchError, - context: {lang: lang, collected: collected, page: page}, - responseType: "json" - }; - GM_xmlhttpRequest(opts); - jQuery.notify(`Fetching leaderboard page ${page}...`, "info"); -} - -function languageChanged() { - const selectedLanguage = jQuery('#glotLanguagesDropdown').val(); - buildLeaderboard(selectedLanguage, [], 1); -} - -function buildLanguagesDropdown() { - - let url = "/api/v1/languages"; - - function makeLangItems(langItems) { - return langItems.map(({id, name}) => ``) - } - - function languagesDownloaded(resp) { - if (resp.readyState !== 4) return; - jQuery('div.leaderboard').prepend(``); - jQuery('div.leaderboard').prepend('

    Note from Polyglot script: The feature of rank leaderboards is still work in progress. Expect it to be buggy or incomplete.

    ') - jQuery('#glotLanguagesDropdown').change(languageChanged); - } - - let opts = { - method: "GET", - url: url, - onreadystatechange: languagesDownloaded, - onabort: fetchAborted, - onerror: fetchError, - responseType: "json" - }; - GM_xmlhttpRequest(opts); -} - -function buildLanguagesLeaderboardTab() { - jQuery('dl.tabs').append('
    Rank
    '); - jQuery('#glotRankLeaderboardLink').click(function() { - if(jQuery('#glotLanguagesDropdown').length) return; - jQuery('dd.is-active').removeClass('is-active'); - jQuery('#glotLanguagesLeaderboardTab').addClass('is-active'); - - let tbody = jQuery('div.leaderboard table tbody'); - let tr = tbody.children('tr')[0]; - let th = tr.children[3]; - jQuery(th).text('Rank'); - - buildLanguagesDropdown(); - buildLeaderboard('overall', [], 1); - }); -} - -/******************************** - * Rank assessments * - *********************************/ - -function addRankAssessmentBreakdown(breakdown, elem) { - - if(!jQuery('#glotBreakdownRow').length) { - let canvas = ''; - jQuery(elem).parent().after(canvas); - } - - let allRanks = {}; - for(let b of breakdown) { - allRanks[b.rank] = b.count; - } - - let ranks = Array.from({length: 8}, (_,idx) => -8 + idx); - let labels = ranks.map(getRankLabel); - let yValues = ranks.map(r => allRanks[r] || 0); - - let helperComputedStyles = window.getComputedStyle(document.body); - function getBarColor(rank) { - let rankColor = getRankColor(rank); - return helperComputedStyles.getPropertyValue(`--color-rank-${rankColor}`) || rankColor; - } - var barColors = ranks.map(getBarColor); - - new Chart("glotChartBreakdown", { - type: "bar", - data: { - labels: labels, - datasets: [{ - backgroundColor: barColors, - data: yValues, - label: '' - }] - }, - options: { - legend: { display: false } } - }); -} - - -function rankAssessmentsBreakdownDownloaded(resp) { - if (resp.readyState !== 4) return; - let cwResp = resp.response; - const breakdownEntries = cwResp.data; - const {kataId, elem} = resp.context; - addRankAssessmentBreakdown(breakdownEntries, elem); -} - -function fetchRankAssessmentBreakdown(kataId, elem) { - - let url = `/api/v1/code-challenges/${kataId}/assessed-ranks`; - let opts = { - method: "GET", - url: url, - onreadystatechange: rankAssessmentsBreakdownDownloaded, - onabort: fetchAborted, - onerror: fetchError, - context: {kataId: kataId, elem: elem }, - responseType: "json" - }; - GM_xmlhttpRequest(opts); - jQuery.notify(`Fetching rank assessments breakdown...`, "info"); -} - -function toggleRankAssessmentsBreakdown() { - jQuery('#glotBreakdownRow').slideToggle({duration: 400}); -} - -function addRankAssessmentsUi(elem) { - - elem = elem.parent().find('table')[1]; - let allCells = jQuery(elem).find('td'); - - let leftCell = jQuery(allCells[8]); - - if(!jQuery('#glotToggleBreakdown').length) { - leftCell.append(' (see breakdown)'); - jQuery('#glotToggleBreakdown').click(toggleRankAssessmentsBreakdown); - } - - let kataId = getViewedKataId(); - fetchRankAssessmentBreakdown(kataId, leftCell); -} - - -/******************************** - * Settings * - *********************************/ -const checkBoxes = [ - {name: 'showSolutionsTabs', label: 'Show solutions in your profile in tabs'}, - {name: 'showastSolutionsTabs', label: 'Show previous solutions in the trainer in tabs'}, - {name: 'showCopyToClipboardButtons', label: 'Show "Copy to Clipboard" button'}, - {name: 'preferCompletedKataLeaderboard', label: 'Prefer "Completed kata" leaderboard'}, - {name: 'scrollLeaderboard', label: 'Auto-scroll leaderboards to show your position'}, - {name: 'showRankLeaderboards', label: 'Show "Rank" leaderboards'}, - {name: 'alwaysShowSpoilerFlag', label: 'Always show "Spoiler" flag'}, - {name: 'showRankAssessments', label: 'Show rank assessments breakdown'} -]; - -const glotSettingsKey = 'glot.settings'; - -function getOrCreateSettingsObj() { - let configObj = GM_getValue(glotSettingsKey); - if(!configObj) { - configObj = {}; - checkBoxes.forEach(({name})=>{configObj[name] = true }); - } - return configObj; -} - -// new features are enabled by default -function glotGetOption(optionName) { - let valueSet = getOrCreateSettingsObj()[optionName]; - if(valueSet === undefined) { - valueSet = true; - } - return valueSet; -} - -function glotSetOption(optionName) { - let settingsObj = getOrCreateSettingsObj(); - settingsObj[optionName] = jQuery(`#glotSetting_${optionName}`).prop('checked'); - GM_setValue(glotSettingsKey, settingsObj); -} - -var dialog = undefined; - -function buildConfigDialog() { - function makeBox({name, label}) { - const cbId = `glotSetting_${name}`; - return `` - } - - function makeHtmlBoxes(boxes) { - return boxes.map(makeBox).join(''); - } - - function attachBoxListeners(boxes) { - boxes.forEach(({ name }) => { - const cbId = `glotSetting_${name}`; - const cbox = jQuery('#'+ cbId); - cbox.prop("checked", glotGetOption(name)); - cbox.change(function() { glotSetOption(name) }); - }); - } - - jQuery('body').append(` -
    -
    -
    - - ${makeHtmlBoxes(checkBoxes)} -
    -
    -
    -

    Note: some settings are applied after refresh.

    -
    `); - - attachBoxListeners(checkBoxes); - - return jQuery('#glotSettingsDialog').dialog({ - autoOpen: false, - // height: 400, - width: 500, - modal: true, - buttons: [ - { - text: "OK", - click: function() { jQuery(this).dialog("close"); } - } - ] - }); -} - - -function getPolyglotConfigDialog() { - if(!dialog) { - dialog = buildConfigDialog(); - } - return dialog; -} - -function buildPolyglotConfigMenu(menu) { - jQuery(menu).append(`
  • Polyglot Settings
  • `); - jQuery('#glotSettingsLink').click(function() { getPolyglotConfigDialog().dialog('open'); }); -} - - -/******************************** - * DOM Listeners * - *********************************/ - -const spoilerFlagOpacityChange=function(){ - jQuery(this).css("opacity", "1"); -} - -const leaderboardRedirection=function(){ - let elem = jQuery(this); - elem.attr("href", "/users/leaderboard/kata"); -} - -const languageLeaderboards=function(){ - if(jQuery(this).text() == 'Leaderboards') { - buildLanguagesLeaderboardTab(); - } -} - -const leaderboardScrollView=function(){ - if (!isElementInViewport(this)) { - this.scrollIntoView({ behavior: "smooth", block: "center" }); - } -} - -const processRankAssessments=function(){ - let elem = jQuery(this); - if(elem.text() !== 'Stats:') { - return; - } - addRankAssessmentsUi(elem); -} - -const existing=true, onceOnly=false; - -const LISTENERS_CONFIG = [ - [tabidizeByLanguage, "div.list-item-solutions", {existing, onceOnly}, ['showSolutionsTabs']], - [tabidizePastSolutions, 'li[data-tab="solutions"]', {existing, onceOnly}, ['showPastSolutionsTabs']], - [spoilerFlagOpacityChange,'li.is-auto-hidden', {existing}, ['alwaysShowSpoilerFlag']], - [addCopyButton, 'code', {existing}, ['showCopyToClipboardButtons']], - [leaderboardRedirection, 'a[title="Leaders"]', {existing}, ['preferCompletedKataLeaderboard']], - [languageLeaderboards, 'h1.page-title', {existing}, ['showRankLeaderboards']], - [leaderboardScrollView, 'tr.is-current-player', {existing}, ['scrollLeaderboard']], - [processRankAssessments, 'h3', {existing}, ['showRankAssessments']], - [buildPolyglotConfigMenu, 'a.js-sign-out', {existing}, []], -]; - - -for(const [func, target, options, conditions] of LISTENERS_CONFIG){ - jQuery(document).arrive(target, options, function() { - if(conditions.length && conditions.every(prop => !glotGetOption(prop))){ - return; - } - const arg = func !== buildPolyglotConfigMenu ? this : this.parentElement.parentElement; - func.call(this, arg) - }); -} +// ==UserScript== +// @name Polyglot for Codewars +// @description User script which provides some extra functionalities to Codewars +// @version 1.13.20.1 +// @downloadURL https://github.com/hobovsky/polyglot/releases/latest/download/polyglot.js +// @updateURL https://github.com/hobovsky/polyglot/releases/latest/download/polyglot.js +// @match https://www.codewars.com/* +// @grant GM_xmlhttpRequest +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_addStyle +// @grant GM_setClipboard +// @connect self +// @require http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js +// @require http://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min.js +// @require https://greasyfork.org/scripts/21927-arrive-js/code/arrivejs.js?version=198809 +// @require https://rawgit.com/notifyjs/notifyjs/master/dist/notify.js +// @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js +// ==/UserScript== + +/**************************************************************** + +WHAT IS IT? +----------- + This piece of code is a Tampermonkey user script which provides some + additional per-language filtering and display capabilities, + effectively making it easier for you to obtain POLYGLOT badge. + +WHERE CAN I DOWNLOAD IT FROM? +----------------------------- + You can get the script [here](https://github.com/hobovsky/polyglot/releases/latest/download/polyglot.js). + + HOW TO INSTALL IT? +------------------ + - Install Tampermonkey extension for your browser, + - Copy&paste the script to your scripts library. + + WHAT FEATURES DOES IT PROVIDE? +------------------------------ + - You can copy content of code boxes into clipboard. + - "Spoiler" flags are visible all the time and do not dis/re-appear + in a very annoying manner. + - Contents of "Solutions" and "Past solutions" views are displayed in + tabs by language. + - Leaderboards: "Solved kata is default leaderboard (since "Overall" + ranking does not measure anything useful). Also, leaderboards are + automatically scrolled to show your score. + - Leaderboards: Rank leaderboards utilizing Codewars API, to show users + ranked by overall rank or a language rank. + - Beta kata: uses Codwewars API to fetch and present breakdown of rank votes. + + HOW TO UNINSTALL IT? +-------------------- + I haven't checked. + + KNOWN ISSUES +------------ + - Yes. + - A race condition here or there. + - Selectors, hooks and listeners used are so inefficient that your local power + plant probably doubles its coal consumption. + + WHAT CAN I DO WITH THE SCRIPT? +------------------------------ + - You are allowed to use it, unless someone authoritative (CW staff?) says you can't. + - You can modify it ONLY IF your modifications are going to bring any improvement + into the way it works, AND you are going to share improved version with CW community. + - You can send all your critical remarks to `/dev/null`, unless it's something I could + learn or otherwise benefit from - in such case, you can contact me on Codewars + Discord server. + + THIS CODE IS CRAP, LOOKS LIKE CRAP, AND WORKS LIKE CRAP! WHY? +------------------------------------------------------------- + I am really sorry if this code hurts your eyes, brain, or feelings + in any way, but I am not a professional HTML developer and each and + every technique present here (JavaScript, jQuery, TamperMonkey, + CW API) I've used for the first time. + + CREDITS +------- + - Codewars + - TamperMonkey + - StackOverflow + - jQuery + - notify.js + - Chart.js + +****************************************************************/ + +/* globals jQuery, $, waitForKeyElements, App */ +var $ = window.jQuery; +const JQUERYUI_CSS_URL = '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/themes/dark-hive/jquery-ui.min.css'; + +$.noConflict(); + +const DROPDOWN_STYLE_CLASS = 'mt-1 block w-full pl-3 pr-10 py-2 text-base dark:bg-gray-800 border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-cgray-300 dark:focus:ring-cgray-600 focus:border-cgray-300 dark:focus:border-cgray-600 sm:text-sm rounded-md'; + +jQuery("head").append(``); + +function isElementInViewport(el) { + if (typeof jQuery === "function" && el instanceof jQuery) { + el = el[0]; + } + + let rect = el.getBoundingClientRect(); + let wnd = jQuery(window); + return rect.top >= 0 && rect.left >= 0 && rect.bottom <= wnd.height() && rect.right <= wnd.width(); +} + +let userName = null; +function getUserName() { + if (!userName) { + let url = jQuery("#header_profile_link").attr("href"); + url = url.split("/"); + userName = url[url.length - 1]; + console.info("Detected username: " + userName); + } + return userName; +} + +let css = ` +.glotBtnCopy { + margin-top: 1px; + margin-right: 2px; + float: right; + padding: 5px 10px 5px; + opacity: 0.7; +} +`; +GM_addStyle(css); + +function getViewedKataId() { + let currentUrl = window.location.href; + let parts = currentUrl.split('/'); + let kataPartIdx = parts.indexOf('kata'); + if(kataPartIdx == -1 || kataPartIdx == parts.length - 1) { + return null; + } + return parts[kataPartIdx + 1]; +} + +function isBetaKata() { + let rankTag = jQuery("h4").siblings('div.tag').text(); + return rankTag === 'Beta' +} + +function getRankColor(rank) { + switch(rank) { + case 1: case 2: + case 3: case 4: return 'black'; + case -1: case -2: return "purple"; + case -3: case -4: return 'blue'; + case -5: case -6: return 'yellow'; + default: return 'white'; + } +} + +function getRankValue(rank) { return Math.abs(rank) } +function getRankDenom(rank) { return rank < 0 ? "kyu" : "dan" } +function getRankLabel(rank) { return `${getRankValue(rank)} ${getRankDenom(rank)}`; } + +function fetchAborted() { + jQuery.notify("Fetch aborted.", "info"); +} +function fetchError() { + jQuery.notify("ERROR!", "error"); +} + +/******************************** + * Clipboard * + *********************************/ + +const btnCaption = "Copy to clipboard"; +function copyToClipboardFunc(codeElem) { + return function() { + let code = codeElem.text().substring(btnCaption.length); + GM_setClipboard(code, "text"); + jQuery.notify(code.length + " characters copied to clipboard.", "info"); + }; +} + +function addCopyButton(codeElem, attempt = 10) { + //TODO: syntax highlighter treats buttons added by me as regular code which should + //be highlighted, and destroys them if they are added before highlighting is completed. + //To avoid such situation, I wait 1 second to let highlighter complete its job, + //and add button after this time elapses. However, better solution might be just to listen + //for removal of the buton and simply re-add it? + setTimeout(function() { + codeElem = jQuery(codeElem); + + if (codeElem.parents("#description").length) { + return; + } + + if (codeElem.parent("pre").length && !codeElem.children("button.glotBtnCopy").length) { + if (codeElem.children("span").length) { + codeElem.prepend(""); + let btn = codeElem.children("button").first(); + btn.on("click", copyToClipboardFunc(codeElem)); + } else if (attempt) { + console.info("Highlight delay..."); + addCopyButton(codeElem, attempt - 1); + } + } + }, 1000); +} + +/******************************** + * Tabbed Languages * + *********************************/ +let tabIdSerial = 1; +function tabidizeByLanguage(solutionPanel) { + solutionPanel = jQuery(solutionPanel); + let kataTitle = solutionPanel.children("div.item-title:first").first(); + kataTitle.after('
      '); + let langTabsList = kataTitle + .next("div.langTabs") + .children("ul.tabsList:first") + .first(); + + let langs = solutionPanel.children("h6"); + langs.each((i, langHeader) => { + langHeader = jQuery(langHeader); + langTabsList.append('
    • ' + langHeader.text().slice(0, -1) + "
    • "); + let contentElems = langHeader.nextUntil("h6"); + contentElems.wrapAll('
      '); + }); + langs.remove(); + let langTabs = kataTitle.next("div.langTabs"); + solutionPanel + .children("div.langTab") + .detach() + .appendTo(langTabs); + let tabsPanel = langTabs.tabs(); +} + +function tabidizePastSolutions(liElem) { + let solutionPanel = jQuery(liElem) + .children("div:first") + .first(); + + let langDivs = solutionPanel.children(); + langDivs.wrapAll('
      '); + let langTabs = solutionPanel.children("div.langTabs").first(); + langTabs.prepend('
        '); + let langTabsList = langTabs.children("ul.tabsList:first").first(); + + let langs = langDivs.children("p"); + langs.each((i, langHeader) => { + langHeader = jQuery(langHeader); + langTabsList.append('
      • ' + langHeader.text() + "
      • "); + langHeader.parent().attr("id", "langTab-" + tabIdSerial++); + }); + langs.remove(); + let tabsPanel = langTabs.tabs(); +} + +/******************************** + * Rank Leaderboards * + *********************************/ +function makeRankBadge(rank) { + return `
        ${getRankLabel(rank)}
        ` +} + +function getCurrentPlayerClass(leaderboardEntry) { + return leaderboardEntry.username == getUserName() ? 'class="is-current-player"' : "" +} + +function createLeaderboardRow(boardEntry) { + + let row = `#${boardEntry.position}${makeRankBadge(boardEntry.rank)} ${boardEntry.username}${boardEntry.score}` + return row; +} + +function fillLeaderboardRows(collected) { + const leaderboardRows = jQuery('tr[data-username]'); + leaderboardRows.remove(); + + var rowsHtml = collected.sort((r1, r2)=>r2.score-r1.score).map(createLeaderboardRow).join(''); + jQuery('div.leaderboard table tbody').append(rowsHtml); +} + +function leaderboardDownloaded(resp) { + if (resp.readyState !== 4) return; + let cwResp = resp.response; + const leaderboardEntries = cwResp.data; + const {lang, collected, page} = resp.context; + collected.push(...leaderboardEntries); + + if(page < 10 && leaderboardEntries && leaderboardEntries.length) { + buildLeaderboard(lang, collected, page+1); + } else { + fillLeaderboardRows(collected); + } +} + +function buildLeaderboard(lang, collected, page=1) { + + let url = `/api/v1/leaders/ranks/${lang}?page=${page}`; + + let opts = { + method: "GET", + url: url, + onreadystatechange: leaderboardDownloaded, + onabort: fetchAborted, + onerror: fetchError, + context: {lang: lang, collected: collected, page: page}, + responseType: "json" + }; + GM_xmlhttpRequest(opts); + jQuery.notify(`Fetching leaderboard page ${page}...`, "info"); +} + +function languageChanged() { + const selectedLanguage = jQuery('#glotLanguagesDropdown').val(); + buildLeaderboard(selectedLanguage, [], 1); +} + +function buildLanguagesDropdown() { + + let url = "/api/v1/languages"; + + function makeLangItems(langItems) { + return langItems.map(({id, name}) => ``) + } + + function languagesDownloaded(resp) { + if (resp.readyState !== 4) return; + jQuery('div.leaderboard').prepend(``); + jQuery('div.leaderboard').prepend('

        Note from Polyglot script: The feature of rank leaderboards is still work in progress. Expect it to be buggy or incomplete.

        ') + jQuery('#glotLanguagesDropdown').change(languageChanged); + } + + let opts = { + method: "GET", + url: url, + onreadystatechange: languagesDownloaded, + onabort: fetchAborted, + onerror: fetchError, + responseType: "json" + }; + GM_xmlhttpRequest(opts); +} + +function buildLanguagesLeaderboardTab() { + jQuery('dl.tabs').append('
        Rank
        '); + jQuery('#glotRankLeaderboardLink').click(function() { + if(jQuery('#glotLanguagesDropdown').length) return; + jQuery('dd.is-active').removeClass('is-active'); + jQuery('#glotLanguagesLeaderboardTab').addClass('is-active'); + + let tbody = jQuery('div.leaderboard table tbody'); + let tr = tbody.children('tr')[0]; + let th = tr.children[3]; + jQuery(th).text('Rank'); + + buildLanguagesDropdown(); + buildLeaderboard('overall', [], 1); + }); +} + +/******************************** + * Rank assessments * + *********************************/ + +function addRankAssessmentBreakdown(breakdown, elem) { + + if(!jQuery('#glotBreakdownRow').length) { + let canvas = ''; + jQuery(elem).parent().after(canvas); + } + + let allRanks = {}; + for(let b of breakdown) { + allRanks[b.rank] = b.count; + } + + let ranks = Array.from({length: 8}, (_,idx) => -8 + idx); + let labels = ranks.map(getRankLabel); + let yValues = ranks.map(r => allRanks[r] || 0); + + let helperComputedStyles = window.getComputedStyle(document.body); + function getBarColor(rank) { + let rankColor = getRankColor(rank); + return helperComputedStyles.getPropertyValue(`--color-rank-${rankColor}`) || rankColor; + } + var barColors = ranks.map(getBarColor); + + new Chart("glotChartBreakdown", { + type: "bar", + data: { + labels: labels, + datasets: [{ + backgroundColor: barColors, + data: yValues, + label: '' + }] + }, + options: { + legend: { display: false } } + }); +} + + +function rankAssessmentsBreakdownDownloaded(resp) { + if (resp.readyState !== 4) return; + let cwResp = resp.response; + const breakdownEntries = cwResp.data; + const {kataId, elem} = resp.context; + addRankAssessmentBreakdown(breakdownEntries, elem); +} + +function fetchRankAssessmentBreakdown(kataId, elem) { + + let url = `/api/v1/code-challenges/${kataId}/assessed-ranks`; + let opts = { + method: "GET", + url: url, + onreadystatechange: rankAssessmentsBreakdownDownloaded, + onabort: fetchAborted, + onerror: fetchError, + context: {kataId: kataId, elem: elem }, + responseType: "json" + }; + GM_xmlhttpRequest(opts); + jQuery.notify(`Fetching rank assessments breakdown...`, "info"); +} + +function toggleRankAssessmentsBreakdown() { + jQuery('#glotBreakdownRow').slideToggle({duration: 400}); +} + +function addRankAssessmentsUi(elem) { + + elem = elem.parent().find('table')[1]; + let allCells = jQuery(elem).find('td'); + + let leftCell = jQuery(allCells[8]); + + if(!jQuery('#glotToggleBreakdown').length) { + leftCell.append(' (see breakdown)'); + jQuery('#glotToggleBreakdown').click(toggleRankAssessmentsBreakdown); + } + + let kataId = getViewedKataId(); + fetchRankAssessmentBreakdown(kataId, leftCell); +} + + +/******************************** + * Settings * + *********************************/ +const checkBoxes = [ + {name: 'showSolutionsTabs', label: 'Show solutions in your profile in tabs'}, + {name: 'showastSolutionsTabs', label: 'Show previous solutions in the trainer in tabs'}, + {name: 'showCopyToClipboardButtons', label: 'Show "Copy to Clipboard" button'}, + {name: 'preferCompletedKataLeaderboard', label: 'Prefer "Completed kata" leaderboard'}, + {name: 'scrollLeaderboard', label: 'Auto-scroll leaderboards to show your position'}, + {name: 'showRankLeaderboards', label: 'Show "Rank" leaderboards'}, + {name: 'alwaysShowSpoilerFlag', label: 'Always show "Spoiler" flag'}, + {name: 'showRankAssessments', label: 'Show rank assessments breakdown'} +]; + +const glotSettingsKey = 'glot.settings'; + +function getOrCreateSettingsObj() { + let configObj = GM_getValue(glotSettingsKey); + if(!configObj) { + configObj = {}; + checkBoxes.forEach(({name})=>{configObj[name] = true }); + } + return configObj; +} + +// new features are enabled by default +function glotGetOption(optionName) { + let valueSet = getOrCreateSettingsObj()[optionName]; + if(valueSet === undefined) { + valueSet = true; + } + return valueSet; +} + +function glotSetOption(optionName) { + let settingsObj = getOrCreateSettingsObj(); + settingsObj[optionName] = jQuery(`#glotSetting_${optionName}`).prop('checked'); + GM_setValue(glotSettingsKey, settingsObj); +} + +var dialog = undefined; + +function buildConfigDialog() { + function makeBox({name, label}) { + const cbId = `glotSetting_${name}`; + return `` + } + + function makeHtmlBoxes(boxes) { + return boxes.map(makeBox).join(''); + } + + function attachBoxListeners(boxes) { + boxes.forEach(({ name }) => { + const cbId = `glotSetting_${name}`; + const cbox = jQuery('#'+ cbId); + cbox.prop("checked", glotGetOption(name)); + cbox.change(function() { glotSetOption(name) }); + }); + } + + jQuery('body').append(` +
        +
        +
        + + ${makeHtmlBoxes(checkBoxes)} +
        +
        +
        +

        Note: some settings are applied after refresh.

        +
        `); + + attachBoxListeners(checkBoxes); + + return jQuery('#glotSettingsDialog').dialog({ + autoOpen: false, + // height: 400, + width: 500, + modal: true, + buttons: [ + { + text: "OK", + click: function() { jQuery(this).dialog("close"); } + } + ] + }); +} + + +function getPolyglotConfigDialog() { + if(!dialog) { + dialog = buildConfigDialog(); + } + return dialog; +} + +function buildPolyglotConfigMenu(menu) { + jQuery(menu).append(`
      • Polyglot Settings
      • `); + jQuery('#glotSettingsLink').click(function() { getPolyglotConfigDialog().dialog('open'); }); +} + + +/******************************** + * DOM Listeners * + *********************************/ + +const spoilerFlagOpacityChange=function(){ + jQuery(this).css("opacity", "1"); +} + +const leaderboardRedirection=function(){ + let elem = jQuery(this); + elem.attr("href", "/users/leaderboard/kata"); +} + +const languageLeaderboards=function(){ + if(jQuery(this).text() == 'Leaderboards') { + buildLanguagesLeaderboardTab(); + } +} + +const leaderboardScrollView=function(){ + if (!isElementInViewport(this)) { + this.scrollIntoView({ behavior: "smooth", block: "center" }); + } +} + +const processRankAssessments=function(){ + let elem = jQuery(this); + if(elem.text() !== 'Stats:') { + return; + } + addRankAssessmentsUi(elem); +} + +const existing=true, onceOnly=false; + +const LISTENERS_CONFIG = [ + [tabidizeByLanguage, "div.list-item-solutions", {existing, onceOnly}, ['showSolutionsTabs']], + [tabidizePastSolutions, 'li[data-tab="solutions"]', {existing, onceOnly}, ['showPastSolutionsTabs']], + [spoilerFlagOpacityChange,'li.is-auto-hidden', {existing}, ['alwaysShowSpoilerFlag']], + [addCopyButton, 'code', {existing}, ['showCopyToClipboardButtons']], + [leaderboardRedirection, 'a[title="Leaders"]', {existing}, ['preferCompletedKataLeaderboard']], + [languageLeaderboards, 'h1.page-title', {existing}, ['showRankLeaderboards']], + [leaderboardScrollView, 'tr.is-current-player', {existing}, ['scrollLeaderboard']], + [processRankAssessments, 'h3', {existing}, ['showRankAssessments']], + [buildPolyglotConfigMenu, 'a.js-sign-out', {existing}, []], +]; + + +for(const [func, target, options, conditions] of LISTENERS_CONFIG){ + jQuery(document).arrive(target, options, function() { + if(conditions.length && conditions.every(prop => !glotGetOption(prop))){ + return; + } + const arg = func !== buildPolyglotConfigMenu ? this : this.parentElement.parentElement; + func.call(this, arg) + }); +}