[YouTube-Chat] Words Typing

try to take over the world!

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         [YouTube-Chat] Words Typing
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  try to take over the world!
// @author       You
// @include        https://www.youtube.com/live_chat*
// @include        https://studio.youtube.com/live_chat?is_popout*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license MIT
// @require https://greasyfork.org/scripts/455302-youtube-auto-add-text-in-chatbox/code/%5BYouTube%5D%20Auto%20add%20Text%20in%20ChatBox.js?version=1121818
// @require https://greasyfork.org/scripts/455494-micromodal-min-js/code/micromodal-min%20js.js?version=1121802

// @grant        none
// ==/UserScript==


let wordSearch
let typingCheck
let htmlSetUp
let separator = " "
let searchTime = new Date().getTime()
let JpWords
let typeCountEvents

/**
 *@Description 単語集をロードする。
*/

class loadJpWords {

	constructor(searchBox) {
		this.txtUrls = [
			"https://dl.dropboxusercontent.com/s/hme8y6jx87fe0ri/3letter.txt?dl=0",
			"https://dl.dropboxusercontent.com/s/tyd629mownzv009/4letter.txt?dl=0",
			"https://dl.dropboxusercontent.com/s/g5zuaz6wjj4itmf/5letter.txt?dl=0",
			"https://dl.dropboxusercontent.com/s/4xfr7p4un9r38eh/6letter.txt?dl=0"
		]
	}


	async loadWords(text){
	document.getElementById("top").insertAdjacentHTML("afterend",`
<div id="load-words">loading words...</div>
<style>
  #load-words{
      margin-left: 48px;
      white-space: nowrap;
  }


</style>`)
		//here our function should be implemented

		for(let i=0;i<this.txtUrls.length;i++){
			text = await fetch(this.txtUrls[i])
			text = await text.text()
			this[(i+3)+"words"] = text.split("\r\n")

		}
		document.getElementById("load-words").style.display = "none"
		if(!htmlSetUp){
			htmlSetUp = new HtmlSetUp()
			htmlSetUp.setUp()
		}
		return;
	}

}

class HtmlSetUp {

	constructor(){
		this.wordArea
		this.addTextOption
	}

	setUp(){
		const fontSizeFlag = localStorage.getItem('words-font-size') == null && !isNaN(+localStorage.getItem('words-font-size'))


		const shortcutKeys = `
Esc or Space: Word skip
Tab: Switch Text Box
`
		wordSearch = new WordSearch();
		document.getElementById("top").insertAdjacentHTML("afterend",`
<div id="minimum-dictionary">
  <div id="word-table" class='words-typing-mode'>#<span id="wordarea">${wordSearch.result.join(separator).toLowerCase()}</span></div>
  <div id="word-tools">
    <input class='words-typing-mode' id="word-search-box" maxlength="6" autocomplete="off" value="" placeholder="[?] Search">
    <span class='words-typing-mode'><span id="word-match">0</span> word</span>
    <span data-micromodal-trigger="modal-1" id="shortcut-keys" title="${shortcutKeys}">⌨</span>
    <span><span id="typing-count">${+sessionStorage.getItem('liveTypingCount') ? +sessionStorage.getItem('liveTypingCount') : 0}</span> types</span>
    <span class='words-typing-mode'><span id="typing-speed">0.0</span> k/s</span>
  </div>
</div>
<style>
  #minimum-dictionary{
      margin-left: 48px;
      white-space: nowrap;
  }
  #word-search-box{
  background: rgb(0 0 0 / 10%);
      border: #000000 thin;
      border-top: none;
      outline: solid thin #ffffffa6;
      color: rgb(255 255 255 / 87%);
      width: 8rem;
  }
  #minimum-dictionary > div {
    margin-bottom: 1rem;
  }
  #word-tools > *{
    margin-right: 0.9rem;
  }
  #shortcut-keys:hover{
    text-decoration: underline;
    cursor: help;
  }
</style>

<style id="words-typing">
  .words-typing-mode{
   display:${localStorage.getItem('enable-words-typing') != 'false' ? '' : 'none'};
  }
  #shortcut-keys{
   color:${localStorage.getItem('enable-words-typing') != 'false' ? 'gold' : ''};
  }

  #word-tools{
    margin-top:${localStorage.getItem('enable-words-typing') != 'false' ? '' : '0.5rem'};
  }
</style>

<style id="words-font">
  #word-table{
   font-size:${fontSizeFlag ? 13 : +localStorage.getItem('words-font-size')}px;
  }

</style>
`)
		document.body.insertAdjacentHTML("afterend",`
<div id="modal-1" aria-hidden="false" class="is-open">

        <div class="modal__overlay" tabindex="-1" data-micromodal-close="">

          <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
            <header class="modal__header">
              <h1 id="modal-1-title" class="modal__title">
                Words Typing Option
              </h1>
            </header>
            <div id="modal-1-content" class="modal__content">
              <label><input type="checkbox" id="enable-words-typing" ${localStorage.getItem('enable-words-typing') != 'false' ? 'checked' : ''}>Enable Words Typing</label>
              <label><input type="checkbox" id="miss-word-skip" ${localStorage.getItem('miss-word-skip') != 'true' ? '' : 'checked'}>Miss Word Skip</label>
              <label>font-size<input type="number" min="13" max="30" id="words-font-size" value='${fontSizeFlag ? 13 : +localStorage.getItem('words-font-size')}'>px</label>
              <label>Add Text<input type="text" id="add-text" value='${localStorage.getItem('add-text') == null ? '#' : localStorage.getItem('add-text')}'></label>
            </div>
          </div>
        </div>
      </div>
<style>
  #modal-1 {
    display: none;
  }
  #modal-1.is-open {
    display: block;
    color: rgb(255 255 255 / 90%);
  }
  .modal__container {
    background-color: #212121;
    padding: 30px;
    margin-right: 20px;
    margin-left: 20px;
    max-width: 640px;
    max-height: 100vh;
    width: 100%;
    border-radius: 4px;
    overflow-y: auto;
    box-sizing: border-box;
  }
.modal__overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    background: rgba(0,0,0,0.6);
    display: flex;
    justify-content: center;
    align-items: center;
}
  .modal__content {
    margin-top: 2rem;
    margin-bottom: 2rem;
    line-height: 1.5;
    font-size: 1.5rem;
    display: flex;
    flex-direction: column;
  }
  #words-font-size{
    width:30px;
  }
</style>`)
		this.wordArea = document.getElementById("wordarea")
		this.addTextOption = document.getElementById("add-text")
		document.getElementById("modal-1-content").addEventListener('change', event => {
			switch(event.target.type){
				case 'checkbox' :
					localStorage.setItem(event.target.id,event.target.checked)

					if(event.target.id == 'enable-words-typing'){
						document.getElementById("words-typing").innerText =
							`.words-typing-mode{
   display:${event.target.checked ? '' : 'none'};
}

  #shortcut-keys{
   color:${event.target.checked ? 'gold' : ''};
  }
  #word-tools{
    margin-top:${event.target.checked ? '' : '0.5rem'};
  }`

						if(!event.target.checked){
							addText("")
						}else if(!JpWords){
							JpWords = new loadJpWords()
							JpWords.loadWords()
						}
					}
					break;
				case 'number' :
					localStorage.setItem(event.target.id,event.target.value)
					document.getElementById("words-font").innerText =
						`#word-table{
   font-size:${typeof event.target.value == 'number' ? 13 : +event.target.value}px;
  }`
					break;
				case 'text' :
					localStorage.setItem(event.target.id,event.target.value)
					break;

			}
		})

		MicroModal.init()
		MicroModal.close()
		typingCheck = new TypingCheck(isNaN(+sessionStorage.getItem('liveTypingCount')) ? 0 : +sessionStorage.getItem('liveTypingCount'))
		typeCountEvents = new TypeCountEvents();

		window.addEventListener('beforeunload',typeCountEvents.setSessionStorageTypingCount);


		chat.addEventListener("input", typingCheck.typeCheck.bind(typingCheck))
		chat.addEventListener("keydown", typingCheck.enterSubmitWord.bind(typingCheck))
		chat.addEventListener("focus", e => {
			if(!e.target.textContent && localStorage.getItem('enable-words-typing') != 'false'){
				addText(htmlSetUp.addTextOption.value)
			}else{
				moveEndCaret(chat)
			}
		})

	 document.getElementById("word-search-box").addEventListener("keydown",wordSearch.Search.bind(wordSearch))
	 document.getElementById("word-search-box").addEventListener("focus",e => {
		 e.target.value = ""
	 })
	 setInterval(typeCountEvents.updateTypingSpeed,1000)
 }


}



class TypingCheck {

	constructor(sessionTypeCount) {
		this.typelog = "";
		this.roundTypeCounter = sessionTypeCount
		this.typeCounter = sessionTypeCount
	}


    /**
     *@Description inputイベントで入力ワードを比較する。
    */
	typeCheck(event){

		const c = new RegExp(`^${chat.textContent.slice(1)}`,"i")
		const match = wordSearch.result[0] ? wordSearch.result[0].match(c) : ""
		const matchLength = (match ? match[0].length : 0)

		if(event.data && /insertCompositionText|insertText/.test(event.inputType) && this.typelog.length < event.target.textContent.length){
			document.getElementById("typing-count").textContent = ++this.typeCounter;
			typeCountEvents.updateTypingSpeed()
		}

		this.typelog = event.target.textContent || "";

		if(match){
			htmlSetUp.wordArea.textContent = (wordSearch.result[0].slice(matchLength) + separator + wordSearch.result.slice(1,10).join(separator)).toLowerCase()
        }else if(!chat.textContent){
            htmlSetUp.wordArea.textContent = wordSearch.result.slice(0,10).join(separator).toLowerCase()
        }

	}



    /**
     *@Description チャットテキストボックスのkeydownイベント。

     *@note
     *Enterで送信時、入力したワードを評価。
     *Escでワードスキップ
	 *Tabでテキストボックスフォーカス切り替え
    */
	enterSubmitWord(event){

        if(event.key == "Enter"){

            if(chat.textContent.slice(1) && wordSearch.result[0].toLowerCase() == chat.textContent.slice(1).toLowerCase() || localStorage.getItem('miss-word-skip') == 'true'){
                wordSearch.result = wordSearch.result.slice(1)
				document.getElementById("word-match").textContent = wordSearch.result.length
            }else if(chat.textContent == htmlSetUp.addTextOption.value || !chat.textContent){
				wordSkip()
			}

			if(chat.textContent != htmlSetUp.addTextOption.value && chat.textContent){
				document.getElementById("typing-count").textContent = ++this.typeCounter;
				typeCountEvents.updateTypingSpeed()
			}

            htmlSetUp.wordArea.textContent = wordSearch.result.slice(0,10).join(separator).toLowerCase()

        }else if(event.key == "Escape" && localStorage.getItem('enable-words-typing') != 'false'){

			wordSkip()

        }else if(event.key == "Tab" && localStorage.getItem('enable-words-typing') != 'false'){

			document.getElementById("word-search-box").focus()
			event.preventDefault()

		}
	}

}

function wordSkip(){
	wordSearch.result = wordSearch.result.slice(1)
	document.getElementById("word-match").textContent = wordSearch.result.length
	htmlSetUp.wordArea.textContent = wordSearch.result.slice(0,10).join(separator).toLowerCase()
}

class WordSearch {

	constructor(searchBox) {
		this.result = []
	}

	async Search(event){

		//検索ボックスにフォーカスしている状態でEnterを押した
		if(event.key == "Enter"){
			//Enterを押した検索ボックスの要素
			this.searchBox = event.target

			if(/?/.test(this.searchBox.value)){
				separator = " "
				//文字列で正規表現を作成
				const RegText = `^${this.searchBox.value.replace(/[?]/g, "\\D")}$`
				//文字列を正規表現に変換
				const Reg = new RegExp(RegText ,"i")
				//正規表現にマッチする単語のみを絞り込む
				this.result = JpWords[`${this.searchBox.value.length}words`].filter(word => Reg.test(word)).slice(0,100);

			}else{
				separator = " "
				this.result = await this.getEngWords()
			}


			//結果を出力
			htmlSetUp.wordArea.textContent = this.result.slice(0,10).join(separator).toLowerCase()

			//結果件数を表示
			document.getElementById("word-match").textContent = this.result.length

			//チャットにフォーカス
			chat.focus()
			moveEndCaret(chat)

			//打鍵速度計測用時間・測定用打鍵数を設定
			searchTime = new Date().getTime()
			typingCheck.roundTypeCounter = new Number(typingCheck.typeCounter)
			document.getElementById("typing-speed").textContent = (0).toFixed(1)

		}else if(event.key == "Tab"){

			chat.focus()

			event.preventDefault()

		}
	}





	async getEngWords(html){
		//here our function should be implemented
		let result = []

		for(let i=1;i<=100;i+=100){
			let html = await fetch(`https://www.onelook.com//?w=${this.searchBox.value}&ssbp=1&first=${i}`)
			html = await html.text()

			//wordの前後のいちを取得
			const start = html.search(/<td width=20% valign=top>/)
			const end = html.search(/<\/TR><\/TABLE>/)


			//word要素のみを取り出す
			const wordsElement = html.slice(start,end)

			//取得したワードを配列に結合
			result = result.concat(wordsElement.match(/(?<=\>)[a-zA-Z]+(?=\<)/g))
		}

		return result;

	}
}


class TypeCountEvents {


	/**
     *@Description 打鍵速度を更新する
    */
	updateTypingSpeed(){
		document.getElementById("typing-speed").textContent = ((typingCheck.typeCounter - typingCheck.roundTypeCounter) / ((new Date().getTime()-searchTime)/1000)).toFixed(1)
	}

	/**
     *@Description ページを離れるときに打件数をSessionStorageに保存する
    */
	setSessionStorageTypingCount(){
		sessionStorage.setItem('liveTypingCount',typingCheck.typeCounter);
	}
}


if(localStorage.getItem('enable-words-typing') != 'false'){
	JpWords = new loadJpWords()
	JpWords.loadWords()
}else{
	htmlSetUp = new HtmlSetUp()
	htmlSetUp.setUp()
}