多邻国选词快捷键

使用快捷键刷多邻国. 在主页面使用l键快速开始学习;在学习页使用ctrl键播放语音, 使用回车键提交答案时为选词添加序号,退格键删除选词,删除键删除全部选词. 如果官方和脚本的快捷键无法正常使用, 需要在`vimium-c`等快捷键相关插件中排除多邻国网站

目前为 2024-05-07 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 多邻国选词快捷键
  3. // @namespace http://tampermonkey.net/
  4. // @version 2024-05-07
  5. // @description 使用快捷键刷多邻国. 在主页面使用l键快速开始学习;在学习页使用ctrl键播放语音, 使用回车键提交答案时为选词添加序号,退格键删除选词,删除键删除全部选词. 如果官方和脚本的快捷键无法正常使用, 需要在`vimium-c`等快捷键相关插件中排除多邻国网站
  6. // @author v
  7. // @match https://www.duolingo.cn/*
  8. // @match https://www.duolingo.com/*
  9. // @license MIT
  10. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  11. // @require https://cdn.staticfile.org/jquery/3.3.1/jquery.min.js
  12. // @require http://cdn.staticfile.org/jquery/1.8.3/jquery.min.js
  13. // @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
  14. // @grant GM_log
  15. // @grant GM_addStyle
  16. // ==/UserScript==
  17.  
  18. // 序号样式
  19. // todo GM_addStyle(...) is not a function
  20. // GM_addStyle(".p_item_tip {position: absolute; color: dodgerblue; background-color: greenyellow; border-radius: 5px; !important;}")
  21.  
  22. ;(function () {
  23. 'use strict'
  24. // 选词键顺序
  25. var chars = 'abcdefghijklnopqrstuvxyz1234567890-=[],./'
  26. // 题目区元素相关数据对象
  27. // type -1: 无效 0: 选择题(自带[数字]快捷键) 1: 组句题 2: 配对题(自带[数字]快捷键)
  28. // 3: 填空题(自带[首字母]快捷键) 4: 听写题(不需要处理) 5: 听写填空题(不需要处理) 6: 小故事
  29. // el: 主要题目区元素
  30. // el2: 次要题目区元素
  31. var question = { type: -1 }
  32. // 始初化题目数据对象方法
  33. var init_question = function () {
  34. if (question.type > -1) {
  35. return question
  36. }
  37.  
  38. // 小故事
  39. question.el = document.getElementsByClassName('kbjat')
  40. if (question.el.length) {
  41. question.el = question.el[0].children
  42. question.type = 6
  43. // 每段数据所在属性名
  44. question.prop_field = Object.keys(question.el[0]).find(p =>
  45. p.startsWith('__reactFiber')
  46. )
  47. return
  48. }
  49. // 听写填空题(不需要处理)
  50. question.el = document.querySelector(
  51. 'div[data-test="challenge challenge-listenComplete"]'
  52. )
  53. if (question.el) {
  54. question.type = 5
  55. return
  56. }
  57. // 听写题(不需要处理)
  58. question.el = document.querySelector(
  59. 'div[data-test="challenge challenge-listenTap"]'
  60. )
  61. if (question.el) {
  62. question.type = 4
  63. return
  64. }
  65. // 填空题(自带,不需要处理)
  66. question.el = document.querySelector(
  67. 'div[data-test="challenge challenge-tapComplete"]'
  68. )
  69. if (question.el) {
  70. question.type = 3
  71. return
  72. }
  73. // 配对题(自带,不需要处理)
  74. question.el = document.querySelector(
  75. 'div[data-test="challenge challenge-listenMatch"]'
  76. )
  77. if (question.el) {
  78. question.el = question.el.children[0].children[1].children[0]
  79. question.type = 2
  80. return
  81. }
  82. // 组句题
  83. question.el = document.querySelector('div[data-test="word-bank"]')
  84. if (question.el) {
  85. question.type = 1
  86. question.el2 =
  87. question.el.parentElement.previousElementSibling.children[0].children[0].children[1]
  88. return
  89. }
  90. // 选择题(自带,不需要处理)
  91. question.el = document.querySelector('div[aria-label="choice"]')
  92. if (question.el) {
  93. question.type = 0
  94. return
  95. }
  96. // 未知题型
  97. question.type = -1
  98. }
  99.  
  100. // 防抖方法
  101. function debounce (func, delay) {
  102. let timeout
  103. return function () {
  104. const _this = this
  105. const args = [...arguments]
  106. if (timeout) {
  107. clearTimeout(timeout)
  108. }
  109. timeout = setTimeout(() => {
  110. func.apply(_this, args)
  111. }, delay)
  112. }
  113. }
  114.  
  115. // 为单词/短语添加序号方法
  116. var process_order = function () {
  117. var play_btn = document.querySelector('button[data-test="player-next"]')
  118. if (!play_btn || play_btn.getAttribute('aria-disabled') != 'true') {
  119. return
  120. }
  121. init_question()
  122. if (question.type == 1) {
  123. for (var i = 0; i < question.el.children.length; i++) {
  124. var item = question.el.children[i]
  125. // 修改按钮padding, 加宽以防止序号覆盖文字
  126. item
  127. .querySelector('button')
  128. .setAttribute('style', '--web-ui_button-padding: 8px 4px 8px 10px;')
  129. // 添加序号
  130. let new_el = document.createElement('span')
  131. new_el.textContent = chars.charAt(i)
  132. // new_el.className = 'p_item_tip'
  133. new_el.setAttribute(
  134. 'style',
  135. 'position: absolute; color: dodgerblue; background-color: greenyellow; border-radius: 5px;'
  136. )
  137. item.appendChild(new_el)
  138. }
  139. }
  140. }
  141. // 为单词/短语添加序号方法(防抖)
  142. var process_order_debounce = debounce(process_order, 500)
  143.  
  144. // 按键事件监听
  145. document.addEventListener('keyup', function (event) {
  146. // GM_log('按键:' + event.key)
  147. // 当前页
  148. var page_name = window.location.pathname
  149. // 在主页
  150. if (page_name == '/learn') {
  151. // 按l键直接学习(跳转/lesson页)
  152. if (event.key == 'l') {
  153. window.location.href = '/lesson'
  154. }
  155.  
  156. // 按回车键直接学习(跳转/lesson页)
  157. // 官方回车键失效?
  158. if (event.key == 'Enter') {
  159. setTimeout(function () {
  160. var el = document.querySelector('a[href="/lesson"]')
  161. if (el) {
  162. el.click()
  163. }
  164. }, 150)
  165. }
  166. return
  167. }
  168.  
  169. // 在学习页
  170. if (page_name.startsWith('/lesson')) {
  171. // 初始化题目区数据
  172. init_question()
  173. // 回车键: 延时为下一题单词/短语添加序号
  174. if (event.key == 'Enter') {
  175. question.type = -1
  176.  
  177. process_order_debounce()
  178. return
  179. }
  180. // 退格键, 删除最后一个选词
  181. if (event.key == 'Backspace') {
  182. if (question.el2) {
  183. var selects = question.el2.children
  184. var cnt = selects.length
  185. var last_select = selects[cnt - 1]
  186. last_select.querySelector('button').click()
  187. }
  188. return
  189. }
  190. // 删除键, 删除所有选词
  191. if (event.key == 'Delete') {
  192. if (question.el2) {
  193. var selects = question.el2.children
  194. for (var i = selects.length; i > 0; i--) {
  195. var select = selects[i - 1]
  196. select.querySelector('button').click()
  197. }
  198. }
  199. return
  200. }
  201. // 没有题目区时, 按z键时如果页面有"不,谢谢"按钮, 就点击它
  202. if (event.key == 'z' && question.type == -1) {
  203. // 跳过按钮
  204. var skip_el = document.querySelector(
  205. 'button[data-test="plus-no-thanks"], button[data-test="practice-hub-ad-no-thanks-button"]'
  206. )
  207. if (skip_el) {
  208. skip_el.click()
  209. return
  210. }
  211. // 挑战传奇按钮
  212. var legendary_el = document.querySelector(
  213. 'a[data-test="legendary-start-button"]'
  214. )
  215. if (legendary_el) {
  216. // 找到"继续"按钮并点击
  217. legendary_el.parentElement.nextElementSibling.children[0].children[1].click()
  218. return
  219. }
  220. }
  221.  
  222. // Control键, 点击扬声器按钮播放语音
  223. if (event.key == 'Control') {
  224. if (question.type == 6) {
  225. // 小故事, 找最后一个已读的音频
  226. var last_listen
  227. for (var i = 0; i < question.el.length; i++) {
  228. // 当前遍历的元素
  229. var el = question.el[i]
  230. // 当前遍历的元素包含的类列表
  231. var class_list = Array.from(el.classList)
  232. // 当前元素的音频是否已听过
  233. var flag = el[question.prop_field].flags
  234. // 只有一个类, 答题区, 忽略
  235. if (class_list.length == 1) {
  236. continue
  237. }
  238. // 有两个类
  239. if (class_list.length == 2) {
  240. // flag>0时已经听过, flag=0时为标题, 设为最后听过的音频元素
  241. last_listen = el
  242. continue
  243. }
  244. // 有三个类
  245. if (class_list.length == 3) {
  246. if (flag == 0) {
  247. // 没有听过, 结束循环
  248. break
  249. } else {
  250. // 有三个类但flag>0的情况, 第三个类不同于常规音频,
  251. // 表明该元素非音频, 属于有四个类(听力组句题)的在答情况
  252. continue
  253. }
  254. }
  255. // 有四个类, 听力组句题, flag=0时未答, flag>0时已答
  256. if (class_list.length == 4) {
  257. continue
  258. }
  259. }
  260. if (last_listen) {
  261. last_listen.querySelector('div[data-test="audio-button"]').click()
  262. }
  263. } else {
  264. // 常规题, 找第一个播放按钮
  265. var els = document.getElementsByClassName('fs-exclude')
  266. if (els) {
  267. els[0].click()
  268. }
  269. }
  270.  
  271. return
  272. }
  273.  
  274. // 到这里已经处理完特殊按键事件
  275. // 剩下按字母/数字键的情况
  276. // 将按键转为序号, 未找到时不处理
  277. var idx = chars.indexOf(event.key)
  278. if (idx < 0) {
  279. return
  280. }
  281.  
  282. // 小故事
  283. if (question.type == 6) {
  284. for (var i = 0; i < question.el.length; i++) {
  285. var el = question.el[i]
  286. var class_list = Array.from(el.classList)
  287. if (
  288. class_list.length == 1 &&
  289. el.children.length > 0 &&
  290. i < question.el.length - 1
  291. ) {
  292. // 当前处于答题状态
  293. // 除了最后的配对题和选项是汉字的选择题外其余题几乎都是可以首字母匹配的
  294. // 因此除了最后的配对题不做处理外其余添加使用数字作选择
  295. var btn_list = el.querySelectorAll('button')
  296. var no = Number(event.key)
  297. if (!isNaN(no) && no <= btn_list.length) {
  298. btn_list[no - 1].click()
  299. }
  300. break
  301. }
  302. }
  303. return
  304. }
  305.  
  306. // 选择题
  307. if (question.type == 0) {
  308. if (question.el.children.length >= idx) {
  309. question.el.children[idx].click()
  310. }
  311. return
  312. }
  313.  
  314. // 组句题
  315. if (question.type == 1) {
  316. if (question.el.children.length >= idx) {
  317. var el = question.el.children[idx]
  318. var item = el.children[0].children[0]
  319. if (item.getAttribute('aria-disabled') != 'true') {
  320. item.click()
  321. return
  322. }
  323. var text = item.querySelector(
  324. 'span[data-test="challenge-tap-token-text"'
  325. ).innerHTML
  326. var selects = question.el2.querySelectorAll(
  327. 'span[data-test="challenge-tap-token-text"]'
  328. )
  329. for (var i = 0; i < selects.length; i++) {
  330. var select = selects[i]
  331. if (select.innerHTML == text) {
  332. select.parentElement.parentElement.click()
  333. return
  334. }
  335. }
  336. }
  337. }
  338.  
  339. // 配对题(自带,不需要处理)
  340. // if (question.type == 2) {
  341. // var no = Number(event.key)
  342. // if (!isNaN(no)) {
  343. // idx = no - 1
  344. // var length = question.el.children.length
  345. // if (idx >=0 && idx < length) {
  346. // var el = question.el.children[idx].getElementsByClassName('fs-exclude')
  347. // if (el) {
  348. // el.click()
  349. // return
  350. // }
  351. // el = question.el.children[idx].querySelector('button').click()
  352. // }
  353. // }
  354. // return
  355. // }
  356. }
  357. })
  358.  
  359. // 页面加载后第一个题就是组句题的情况, 添加序号
  360. setTimeout(process_order, 6000)
  361. })()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址