[YouTube-Chat] Words Typing

try to take over the world!

  1. // ==UserScript==
  2. // @name [YouTube-Chat] Words Typing
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.9
  5. // @description try to take over the world!
  6. // @author You
  7. // @include https://www.youtube.com/live_chat*
  8. // @include https://studio.youtube.com/live_chat?is_popout*
  9. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  10. // @license MIT
  11. // @grant unsafeWindow
  12.  
  13. // @require https://greasyfork.org/scripts/455494-micromodal-min-js/code/micromodal-min%20js.js?version=1121802
  14.  
  15. // @grant none
  16. // ==/UserScript==
  17.  
  18. const chat = document.querySelector("#input > #input")
  19. if (window.trustedTypes && window.trustedTypes.createPolicy) {
  20. window.trustedTypes.createPolicy('default', {
  21. createHTML: (string) => string
  22. });
  23. }
  24.  
  25.  
  26. let wordSearch
  27. let typingCheck
  28. let htmlSetUp
  29. let separator = " "
  30. let searchTime = new Date().getTime()
  31. let JpWords
  32. let typeCountEvents
  33.  
  34. /**
  35. *@Description 単語集をロードする。
  36. */
  37.  
  38. class loadJpWords {
  39.  
  40. constructor(searchBox) {
  41. this.txtUrls = [
  42. "https://dl.dropboxusercontent.com/s/hme8y6jx87fe0ri/3letter.txt?dl=0",
  43. "https://dl.dropboxusercontent.com/s/tyd629mownzv009/4letter.txt?dl=0",
  44. "https://dl.dropboxusercontent.com/s/g5zuaz6wjj4itmf/5letter.txt?dl=0",
  45. "https://dl.dropboxusercontent.com/s/4xfr7p4un9r38eh/6letter.txt?dl=0"
  46. ]
  47. }
  48.  
  49.  
  50. async loadWords(text){
  51.  
  52. document.getElementById("panel-pages").insertAdjacentHTML('afterend', window.trustedTypes.defaultPolicy.createHTML(`
  53. <div id="load-words">loading words...</div>
  54. <style>
  55. #load-words{
  56. margin-left: 48px;
  57. white-space: nowrap;
  58. }
  59.  
  60.  
  61. </style>`));
  62. //here our function should be implemented
  63.  
  64. for(let i=0;i<this.txtUrls.length;i++){
  65. text = await fetch(this.txtUrls[i])
  66. text = await text.text()
  67. this[(i+3)+"words"] = text.split("\r\n")
  68.  
  69. }
  70. document.getElementById("load-words").style.display = "none"
  71. if(!htmlSetUp){
  72. htmlSetUp = new HtmlSetUp()
  73. htmlSetUp.setUp()
  74. }
  75. return;
  76. }
  77.  
  78. }
  79.  
  80. class HtmlSetUp {
  81.  
  82. constructor(){
  83. this.wordArea
  84. this.addTextOption
  85. }
  86.  
  87. setUp(){
  88. const fontSizeFlag = localStorage.getItem('words-font-size') == null && !isNaN(+localStorage.getItem('words-font-size'))
  89.  
  90.  
  91. const shortcutKeys = `
  92. Esc or Space: Word skip
  93. Tab: Switch Text Box
  94. `
  95. wordSearch = new WordSearch();
  96. document.getElementById("top").insertAdjacentHTML("afterend",window.trustedTypes.defaultPolicy.createHTML(`
  97. <div id="minimum-dictionary">
  98. <div id="word-table" class='words-typing-mode'>#<span id="wordarea">${wordSearch.result.join(separator).toLowerCase()}</span></div>
  99. <div id="word-tools">
  100. <input class='words-typing-mode' id="word-search-box" maxlength="6" autocomplete="off" value="" placeholder="[?] Search">
  101. <span class='words-typing-mode'><span id="word-match">0</span> word</span>
  102. <span data-micromodal-trigger="modal-1" id="shortcut-keys" title="${shortcutKeys}">⌨</span>
  103. <span><span id="typing-count">${+sessionStorage.getItem('liveTypingCount') ? +sessionStorage.getItem('liveTypingCount') : 0}</span> types</span>
  104. <span class='words-typing-mode'><span id="typing-speed">0.0</span> k/s</span>
  105. </div>
  106. </div>
  107. <style>
  108. #minimum-dictionary{
  109. margin-left: 48px;
  110. white-space: nowrap;
  111. }
  112. #word-search-box{
  113. background: rgb(0 0 0 / 10%);
  114. border: #000000 thin;
  115. border-top: none;
  116. outline: solid thin #ffffffa6;
  117. color: rgb(255 255 255 / 87%);
  118. width: 8rem;
  119. }
  120. #minimum-dictionary > div {
  121. margin-bottom: 1rem;
  122. }
  123. #word-tools > *{
  124. margin-right: 0.9rem;
  125. }
  126. #shortcut-keys:hover{
  127. text-decoration: underline;
  128. cursor: help;
  129. }
  130. </style>
  131.  
  132. <style id="words-typing">
  133. .words-typing-mode{
  134. display:${localStorage.getItem('enable-words-typing') != 'false' ? '' : 'none'};
  135. }
  136. #shortcut-keys{
  137. color:${localStorage.getItem('enable-words-typing') != 'false' ? 'gold' : ''};
  138. }
  139.  
  140. #word-tools{
  141. margin-top:${localStorage.getItem('enable-words-typing') != 'false' ? '' : '0.5rem'};
  142. }
  143. </style>
  144.  
  145. <style id="words-font">
  146. #word-table{
  147. font-size:${fontSizeFlag ? 13 : +localStorage.getItem('words-font-size')}px;
  148. }
  149.  
  150. </style>
  151. `))
  152. document.body.insertAdjacentHTML("afterend",window.trustedTypes.defaultPolicy.createHTML(`
  153. <div id="modal-1" aria-hidden="false" class="is-open">
  154.  
  155. <div class="modal__overlay" tabindex="-1" data-micromodal-close="">
  156.  
  157. <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
  158. <header class="modal__header">
  159. <h1 id="modal-1-title" class="modal__title">
  160. Words Typing Option
  161. </h1>
  162. </header>
  163. <div id="modal-1-content" class="modal__content">
  164. <label><input type="checkbox" id="enable-words-typing" ${localStorage.getItem('enable-words-typing') != 'false' ? 'checked' : ''}>Enable Words Typing</label>
  165. <label><input type="checkbox" id="miss-word-skip" ${localStorage.getItem('miss-word-skip') != 'true' ? '' : 'checked'}>Miss Word Skip</label>
  166. <label>font-size<input type="number" min="13" max="30" id="words-font-size" value='${fontSizeFlag ? 13 : +localStorage.getItem('words-font-size')}'>px</label>
  167. <label>Add Text<input type="text" id="add-text" value='${localStorage.getItem('add-text') == null ? '' : localStorage.getItem('add-text')}'></label>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. <style>
  173. #modal-1 {
  174. display: none;
  175. }
  176. #modal-1.is-open {
  177. display: block;
  178. color: rgb(255 255 255 / 90%);
  179. }
  180. .modal__container {
  181. background-color: #212121;
  182. padding: 30px;
  183. margin-right: 20px;
  184. margin-left: 20px;
  185. max-width: 640px;
  186. max-height: 100vh;
  187. width: 100%;
  188. border-radius: 4px;
  189. overflow-y: auto;
  190. box-sizing: border-box;
  191. }
  192. .modal__overlay {
  193. position: fixed;
  194. top: 0;
  195. left: 0;
  196. right: 0;
  197. bottom: 0;
  198. z-index: 2;
  199. background: rgba(0,0,0,0.6);
  200. display: flex;
  201. justify-content: center;
  202. align-items: center;
  203. }
  204. .modal__content {
  205. margin-top: 2rem;
  206. margin-bottom: 2rem;
  207. line-height: 1.5;
  208. font-size: 1.5rem;
  209. display: flex;
  210. flex-direction: column;
  211. }
  212. #words-font-size{
  213. width:30px;
  214. }
  215. </style>`))
  216. this.wordArea = document.getElementById("wordarea")
  217. this.addTextOption = document.getElementById("add-text")
  218. document.getElementById("modal-1-content").addEventListener('change', event => {
  219. switch(event.target.type){
  220. case 'checkbox' :
  221. localStorage.setItem(event.target.id,event.target.checked)
  222.  
  223. if(event.target.id == 'enable-words-typing'){
  224. document.getElementById("words-typing").innerText =
  225. `.words-typing-mode{
  226. display:${event.target.checked ? '' : 'none'};
  227. }
  228.  
  229. #shortcut-keys{
  230. color:${event.target.checked ? 'gold' : ''};
  231. }
  232. #word-tools{
  233. margin-top:${event.target.checked ? '' : '0.5rem'};
  234. }`
  235.  
  236. if(!event.target.checked){
  237. //addText("")
  238. }else if(!JpWords){
  239. JpWords = new loadJpWords()
  240. JpWords.loadWords()
  241. }
  242. }
  243. break;
  244. case 'number' :
  245. localStorage.setItem(event.target.id,event.target.value)
  246. document.getElementById("words-font").innerText =
  247. `#word-table{
  248. font-size:${typeof event.target.value == 'number' ? 13 : +event.target.value}px;
  249. }`
  250. break;
  251. case 'text' :
  252. localStorage.setItem(event.target.id,event.target.value)
  253. break;
  254.  
  255. }
  256. })
  257.  
  258. MicroModal.init()
  259. MicroModal.close()
  260. typingCheck = new TypingCheck(isNaN(+sessionStorage.getItem('liveTypingCount')) ? 0 : +sessionStorage.getItem('liveTypingCount'))
  261. typeCountEvents = new TypeCountEvents();
  262.  
  263. window.addEventListener('beforeunload',typeCountEvents.setSessionStorageTypingCount);
  264.  
  265.  
  266. chat.addEventListener("input", typingCheck.typeCheck.bind(typingCheck))
  267. chat.addEventListener("keydown", typingCheck.enterSubmitWord.bind(typingCheck))
  268. chat.addEventListener("focus", e => {
  269. if(!e.target.textContent && localStorage.getItem('enable-words-typing') != 'false'){
  270. //addText(htmlSetUp.addTextOption.value)
  271. }else{
  272. //moveEndCaret(chat)
  273. }
  274. })
  275.  
  276. document.getElementById("word-search-box").addEventListener("keydown",wordSearch.Search.bind(wordSearch))
  277. document.getElementById("word-search-box").addEventListener("focus",e => {
  278. e.target.value = ""
  279. })
  280. setInterval(typeCountEvents.updateTypingSpeed,1000)
  281. }
  282.  
  283.  
  284. }
  285.  
  286.  
  287.  
  288. class TypingCheck {
  289.  
  290. constructor(sessionTypeCount) {
  291. this.typelog = "";
  292. this.roundTypeCounter = sessionTypeCount
  293. this.typeCounter = sessionTypeCount
  294. }
  295.  
  296.  
  297. /**
  298. *@Description inputイベントで入力ワードを比較する。
  299. */
  300. typeCheck(event){
  301.  
  302.  
  303. const c = new RegExp(`^${chat.textContent}`,"i")
  304. const match = wordSearch.result[0] ? wordSearch.result[0].match(c) : ""
  305. const matchLength = (match ? match[0].length : 0)
  306.  
  307. if(event.data && /insertCompositionText|insertText/.test(event.inputType) && this.typelog.length < event.target.textContent.length){
  308. document.getElementById("typing-count").textContent = ++this.typeCounter;
  309. typeCountEvents.updateTypingSpeed()
  310. }
  311.  
  312. this.typelog = event.target.textContent || "";
  313.  
  314. if(match){
  315. htmlSetUp.wordArea.textContent = (wordSearch.result[0].slice(matchLength) + separator + wordSearch.result.slice(1,10).join(separator)).toLowerCase()
  316. }else if(!chat.textContent){
  317. htmlSetUp.wordArea.textContent = wordSearch.result.slice(0,10).join(separator).toLowerCase()
  318. }
  319.  
  320. }
  321.  
  322.  
  323.  
  324. /**
  325. *@Description チャットテキストボックスのkeydownイベント。
  326.  
  327. *@note
  328. *Enterで送信時、入力したワードを評価。
  329. *Escでワードスキップ
  330. *Tabでテキストボックスフォーカス切り替え
  331. */
  332. enterSubmitWord(event){
  333.  
  334. if(event.key == "Enter" && chat.textContent){
  335.  
  336. if(chat.textContent && wordSearch.result[0].toLowerCase() == chat.textContent.toLowerCase() || localStorage.getItem('miss-word-skip') == 'true'){
  337. wordSearch.result = wordSearch.result.slice(1)
  338. document.getElementById("word-match").textContent = wordSearch.result.length
  339. }else if(chat.textContent == htmlSetUp.addTextOption.value || !chat.textContent){
  340. wordSkip()
  341. }
  342.  
  343. if(chat.textContent != htmlSetUp.addTextOption.value && chat.textContent){
  344. document.getElementById("typing-count").textContent = ++this.typeCounter;
  345. typeCountEvents.updateTypingSpeed()
  346. }
  347.  
  348. htmlSetUp.wordArea.textContent = wordSearch.result.slice(0,10).join(separator).toLowerCase()
  349.  
  350. }else if(event.key == "Escape" && localStorage.getItem('enable-words-typing') != 'false'){
  351.  
  352. wordSkip()
  353.  
  354. }else if(event.key == "Tab" && localStorage.getItem('enable-words-typing') != 'false'){
  355.  
  356. if(document.activeElement.id === "input"){
  357. document.getElementById("word-search-box").focus()
  358. }else{
  359. chat.focus()
  360. }
  361. event.preventDefault()
  362.  
  363. }
  364. }
  365.  
  366. }
  367.  
  368. function wordSkip(){
  369. wordSearch.result = wordSearch.result.slice(1)
  370. document.getElementById("word-match").textContent = wordSearch.result.length
  371. htmlSetUp.wordArea.textContent = wordSearch.result.slice(0,10).join(separator).toLowerCase()
  372. }
  373.  
  374. class WordSearch {
  375.  
  376. constructor(searchBox) {
  377. this.result = []
  378. }
  379.  
  380. async Search(event){
  381.  
  382. //検索ボックスにフォーカスしている状態でEnterを押した
  383. if(event.key == "Enter"){
  384. //Enterを押した検索ボックスの要素
  385. this.searchBox = event.target
  386.  
  387. if(/?/.test(this.searchBox.value)){
  388. separator = " "
  389. //文字列で正規表現を作成
  390. const RegText = `^${this.searchBox.value.replace(/[?]/g, "\\D")}$`
  391. //文字列を正規表現に変換
  392. const Reg = new RegExp(RegText ,"i")
  393. //正規表現にマッチする単語のみを絞り込む
  394. this.result = JpWords[`${this.searchBox.value.length}words`].filter(word => Reg.test(word)).slice(0,100);
  395.  
  396. }else{
  397. separator = " "
  398. this.result = await this.getEngWords()
  399. }
  400.  
  401.  
  402. //結果を出力
  403. htmlSetUp.wordArea.textContent = this.result.slice(0,10).join(separator).toLowerCase()
  404.  
  405. //結果件数を表示
  406. document.getElementById("word-match").textContent = this.result.length
  407.  
  408. //チャットにフォーカス
  409. chat.focus()
  410. //moveEndCaret(chat)
  411.  
  412. //打鍵速度計測用時間・測定用打鍵数を設定
  413. searchTime = new Date().getTime()
  414. typingCheck.roundTypeCounter = new Number(typingCheck.typeCounter)
  415. document.getElementById("typing-speed").textContent = (0).toFixed(1)
  416.  
  417. }else if(event.key == "Tab"){
  418.  
  419. chat.focus()
  420.  
  421. event.preventDefault()
  422.  
  423. }
  424. }
  425.  
  426.  
  427.  
  428.  
  429.  
  430. async getEngWords(html){
  431. //here our function should be implemented
  432. let result = []
  433.  
  434. for(let i=1;i<=100;i+=100){
  435. let html = await fetch(`https://www.onelook.com//?w=${this.searchBox.value}&ssbp=1&first=${i}`)
  436. html = await html.text()
  437.  
  438. //wordの前後のいちを取得
  439. const start = html.search(/<td width=20% valign=top>/)
  440. const end = html.search(/<\/TR><\/TABLE>/)
  441.  
  442.  
  443. //word要素のみを取り出す
  444. const wordsElement = html.slice(start,end)
  445.  
  446. //取得したワードを配列に結合
  447. result = result.concat(wordsElement.match(/(?<=\>)[a-zA-Z]+(?=\<)/g))
  448. }
  449.  
  450. return result;
  451.  
  452. }
  453. }
  454.  
  455.  
  456. class TypeCountEvents {
  457.  
  458.  
  459. /**
  460. *@Description 打鍵速度を更新する
  461. */
  462. updateTypingSpeed(){
  463. document.getElementById("typing-speed").textContent = ((typingCheck.typeCounter - typingCheck.roundTypeCounter) / ((new Date().getTime()-searchTime)/1000)).toFixed(1)
  464. }
  465.  
  466. /**
  467. *@Description ページを離れるときに打件数をSessionStorageに保存する
  468. */
  469. setSessionStorageTypingCount(){
  470. sessionStorage.setItem('liveTypingCount',typingCheck.typeCounter);
  471. }
  472. }
  473.  
  474.  
  475. if(localStorage.getItem('enable-words-typing') != 'false'){
  476. JpWords = new loadJpWords()
  477. JpWords.loadWords()
  478. }else{
  479. htmlSetUp = new HtmlSetUp()
  480. htmlSetUp.setUp()
  481. }
  482.