Duolingo input language switcher

This script allows you to type letters appropriate for current challenge without changing keyboard layout. Similar to Punto Switcher.

  1. // ==UserScript==
  2. // @name Duolingo input language switcher
  3. // @namespace https://www.duolingo.com/IVrL9
  4. // @author T1mL3arn
  5. // @match https://www.duolingo.com/*
  6. // @match https://*.duolingo.com/*
  7. // @version 3.0.1
  8. // @description This script allows you to type letters appropriate for current challenge without changing keyboard layout. Similar to Punto Switcher.
  9. // @description:ru Скрипт дает возможность выполнять упражнения не отвлекаясь на переключение раскладки клавиатуры. Похоже на Punto Switcher.
  10. // @run-at document-start
  11. // @grant none
  12. // @icon https://www.androidpolice.com/wp-content/uploads/2014/03/nexusae0_Duolingo-Thumb.png
  13. // @license GPL-3.0-only
  14. // @homepageURL https://github.com/T1mL3arn/Duolingo-input-language-switcher
  15. // @supportURL https://greasyfork.org/en/scripts/37693-duolingo-input-language-switcher/feedback
  16. // ==/UserScript==
  17.  
  18. (function ($global) { "use strict";
  19. var Lambda = function() { };
  20. Lambda.find = function(it,f) {
  21. var v = $getIterator(it);
  22. while(v.hasNext()) {
  23. var v1 = v.next();
  24. if(f(v1)) {
  25. return v1;
  26. }
  27. }
  28. return null;
  29. };
  30. var Main = function() {
  31. this.CHALLENGE_TYPES = ["listen_complete","complete_reverse_translation","reverse_translate","partial_reverse_translate","reverse_tap","listen","tap","name","listen_tap"];
  32. this.document = window.document;
  33. this.console = $global.console;
  34. this.initLanguages();
  35. if(this.document.readyState == "interactive" || this.document.readyState == "complete") {
  36. this.onready();
  37. } else {
  38. this.document.addEventListener("DOMContentLoaded",$bind(this,this.onready));
  39. }
  40. };
  41. Main.main = function() {
  42. new Main();
  43. };
  44. Main.prototype = {
  45. initLanguages: function() {
  46. this.keyCodes = ["Backquote","Digit1","Digit2","Digit3","Digit4","Digit5","Digit6","Digit7","Digit8","Digit9","Digit0","Minus","Equal","Backslash","KeyQ","KeyW","KeyE","KeyR","KeyT","KeyY","KeyU","KeyI","KeyO","KeyP","BracketLeft","BracketRight","KeyA","KeyS","KeyD","KeyF","KeyG","KeyH","KeyJ","KeyK","KeyL","Semicolon","Quote","KeyZ","KeyX","KeyC","KeyV","KeyB","KeyN","KeyM","Comma","Period","Slash"];
  47. this.languages = { };
  48. this.languages.ru = "ё1234567890-=\\йцукенгшщзхъфывапролджэячсмитьбю.Ё!\"№;%:?*()_+/ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ,";
  49. this.languages.en = "`1234567890-=\\qwertyuiop[]asdfghjkl;'zxcvbnm,./~!@#$%^&*()_+|QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?";
  50. var len = this.languages.ru.length;
  51. var _g = 0;
  52. var _g1 = Reflect.fields(this.languages);
  53. while(_g < _g1.length) {
  54. var f = _g1[_g];
  55. ++_g;
  56. var act = this.languages[f].length;
  57. if(act != len) {
  58. this.console.error("LangString test failed: expected len " + len + "; actual len " + act + "; lang name " + f);
  59. this.console.error(this.languages[f]);
  60. return;
  61. }
  62. if(act != this.keyCodes.length * 2) {
  63. this.console.error("KeyCodes and LangString test failed: expected lang string len " + this.keyCodes.length * 2 + "; actual len " + act + "; lang name " + f);
  64. return;
  65. }
  66. }
  67. }
  68. ,onready: function(e) {
  69. var _gthis = this;
  70. this.document.removeEventListener("DOMContentLoaded",$bind(this,this.onready));
  71. var mode = "";
  72. this.console.log("Duolingo input switcher is ready" + mode);
  73. window.document.body.addEventListener("keydown",$bind(this,this.onKeyDown));
  74. new MutationObserver(function(changes) {
  75. var path = window.location.pathname;
  76. var isPracticePage = StringTools.startsWith(path,"/practice") || StringTools.startsWith(path,"/lesson");
  77. var someAdded = Lambda.find(changes,function(c) {
  78. return c.addedNodes.length > 0;
  79. }) != null;
  80. if(isPracticePage && someAdded) {
  81. var elt = _gthis.document.querySelector("._3x0ok");
  82. if(elt == null) {
  83. _gthis.console.log("Not a practice page, reset current challenge");
  84. _gthis.currentChallengeType = null;
  85. return;
  86. }
  87. var props = _gthis.getReactProps(elt);
  88. _gthis.challenge = props.children[0]._owner.stateNode.props.currentChallenge;
  89. if(_gthis.challenge == null) {
  90. _gthis.console.log("Not a practice page, reset current challenge");
  91. _gthis.currentChallengeType = null;
  92. return;
  93. }
  94. var sourcelang = _gthis.challenge.metadata.source_language;
  95. var targetlang = _gthis.challenge.metadata.target_language;
  96. var specType = _gthis.challenge.metadata.specific_type;
  97. var genType = _gthis.challenge.metadata.type;
  98. _gthis.sourceLanguage = sourcelang;
  99. _gthis.targetLanguage = targetlang != null ? targetlang : sourcelang;
  100. _gthis.currentChallengeType = specType;
  101. _gthis.console.log(specType,sourcelang,targetlang,genType);
  102. }
  103. }).observe(this.document.body,{ childList : true, subtree : true});
  104. }
  105. ,getReactProps: function(elt) {
  106. var _g = 0;
  107. var _g1 = Reflect.fields(elt);
  108. while(_g < _g1.length) {
  109. var propName = _g1[_g];
  110. ++_g;
  111. if(StringTools.startsWith(propName,"__reactProps")) {
  112. return Reflect.field(elt,propName);
  113. }
  114. }
  115. return null;
  116. }
  117. ,onKeyDown: function(e) {
  118. if(this.currentChallengeType == null) {
  119. return;
  120. }
  121. if(e.ctrlKey) {
  122. return;
  123. }
  124. var pressedKeyCodeIndex = this.keyCodes.indexOf(e.code);
  125. if(pressedKeyCodeIndex == -1) {
  126. return;
  127. }
  128. if(this.CHALLENGE_TYPES.indexOf(this.currentChallengeType) == -1) {
  129. return;
  130. }
  131. var elt = e.target;
  132. if(!(elt.hasAttribute("contenteditable") || elt.tagName == "INPUT" || elt.tagName == "TEXTAREA")) {
  133. return;
  134. }
  135. e.preventDefault();
  136. this.replaceLetter(this.getLanguageLetter(this.targetLanguage,pressedKeyCodeIndex,e.shiftKey),elt);
  137. }
  138. ,getLanguageLetter: function(language,letterIndex,isUppercase) {
  139. var letters = this.languages[language];
  140. if(isUppercase) {
  141. return letters.charAt(letterIndex + this.keyCodes.length);
  142. } else {
  143. return letters.charAt(letterIndex);
  144. }
  145. }
  146. ,replaceLetter: function(letter,elt) {
  147. switch(elt.tagName) {
  148. case "SPAN":
  149. var s = window.getSelection();
  150. if(s.anchorNode != s.focusNode) {
  151. return false;
  152. }
  153. var start = s.anchorOffset;
  154. var end = s.focusOffset;
  155. var text = s.anchorNode.textContent;
  156. if(start != end) {
  157. s.deleteFromDocument();
  158. }
  159. start = s.anchorOffset;
  160. end = s.focusOffset;
  161. elt.textContent = text.substring(0,start) + letter + text.substring(end);
  162. elt.dispatchEvent(new InputEvent("input",{ bubbles : true}));
  163. s.collapse(elt.childNodes[0],start + 1);
  164. return true;
  165. case "INPUT":case "TEXTAREA":
  166. var elt1 = elt;
  167. var start = elt1.selectionStart;
  168. var phrase = elt1.value;
  169. phrase = phrase.substring(0,start) + letter + phrase.substring(elt1.selectionEnd);
  170. elt1.value = phrase;
  171. elt1.setSelectionRange(start + 1,start + 1);
  172. this.callReactEventHandler(elt1,"onChange",{ type : "change", target : elt1});
  173. return true;
  174. default:
  175. return false;
  176. }
  177. }
  178. ,callReactEventHandler: function(elt,methodName,event) {
  179. var _g = 0;
  180. var _g1 = Reflect.fields(elt);
  181. while(_g < _g1.length) {
  182. var fieldName = _g1[_g];
  183. ++_g;
  184. if(fieldName.indexOf("__reactProps") != -1) {
  185. var reactProps = Reflect.field(elt,fieldName);
  186. reactProps[methodName](event);
  187. return;
  188. }
  189. }
  190. this.console.error("Cannot find react " + methodName + " handler on",elt);
  191. }
  192. };
  193. var Reflect = function() { };
  194. Reflect.field = function(o,field) {
  195. try {
  196. return o[field];
  197. } catch( _g ) {
  198. return null;
  199. }
  200. };
  201. Reflect.fields = function(o) {
  202. var a = [];
  203. if(o != null) {
  204. var hasOwnProperty = Object.prototype.hasOwnProperty;
  205. for( var f in o ) {
  206. if(f != "__id__" && f != "hx__closures__" && hasOwnProperty.call(o,f)) {
  207. a.push(f);
  208. }
  209. }
  210. }
  211. return a;
  212. };
  213. var StringTools = function() { };
  214. StringTools.startsWith = function(s,start) {
  215. if(s.length >= start.length) {
  216. return s.lastIndexOf(start,0) == 0;
  217. } else {
  218. return false;
  219. }
  220. };
  221. var haxe_iterators_ArrayIterator = function(array) {
  222. this.current = 0;
  223. this.array = array;
  224. };
  225. haxe_iterators_ArrayIterator.prototype = {
  226. hasNext: function() {
  227. return this.current < this.array.length;
  228. }
  229. ,next: function() {
  230. return this.array[this.current++];
  231. }
  232. };
  233. function $getIterator(o) { if( o instanceof Array ) return new haxe_iterators_ArrayIterator(o); else return o.iterator(); }
  234. var $_;
  235. function $bind(o,m) { if( m == null ) return null; if( m.__id__ == null ) m.__id__ = $global.$haxeUID++; var f; if( o.hx__closures__ == null ) o.hx__closures__ = {}; else f = o.hx__closures__[m.__id__]; if( f == null ) { f = m.bind(o); o.hx__closures__[m.__id__] = f; } return f; }
  236. $global.$haxeUID |= 0;
  237. Main.main();
  238. })(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this);