[e-typing]累計コンボ数表示

e-typingで継続ノーミス記録を表示する

// ==UserScript==
// @name         [e-typing]累計コンボ数表示
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  e-typingで継続ノーミス記録を表示する
// @author       xyu
// @match        https://www.e-typing.ne.jp/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=e-typing.ne.jp
// @grant        none
// @license MIT
// ==/UserScript==
if(localStorage.getItem('totalCombo') == undefined){
	localStorage.setItem('totalCombo',0)
	localStorage.setItem('round',0)
	localStorage.setItem('averageWPM',0)
	localStorage.setItem('bestCombo',0)
}

let clearLineCount = 0
let typingAppModInterval
let createOptionInterval
let displayComboSwitch
let bestCombo = +localStorage.getItem('bestCombo')
let totalCombo = +localStorage.getItem("totalCombo")
let combo = 0
let wpmCountFlag = true
let scoreCheck
let typingMode = "roma"
let enteredClass = 2
if(location.href.match(/kana\.1/)){
	typingMode = "kana"
	enteredClass = 1
}else if(location.href.match(/std\.2/) || location.href.match(/lstn\.4/)){
	typingMode = "eng"
	enteredClass = 1
}else{
	typingMode = "roma"
	enteredClass = 2
}

const createOption = () => {
console.log("createOption")
const FUNC_VIEW = document.getElementById("func_view")
const DISABLE_OPTION = document.getElementById("disable-option")
	if(FUNC_VIEW){
		displayComboSwitch = localStorage.getItem("combo-option") == "false" ? false : true;
		FUNC_VIEW.style.height = document.getElementById("func_view").clientHeight + 30 + "px"
		FUNC_VIEW.insertAdjacentHTML('beforeend' , `<div>
<div><label><small>累計コンボ数表示</small><input id="combo-option" type="checkbox" style="display:none;" ${displayComboSwitch == false ? "" : "checked"}><div id="combo-btn" style="margin-left:4px;" class="switch_btn"><a class="on_btn btn show">ON</a><a class="off_btn btn" style="display:${displayComboSwitch == false ? "block" : ""};">OFF</a></div></label></div>
</div>`)

		document.getElementById("combo-option").addEventListener("change" , event => {
			localStorage.setItem("combo-option" , event.target.checked);

			if(event.target.checked){
				document.querySelector("#combo-btn .off_btn").style.display = ""
				displayComboSwitch = true;
			}else{
				document.querySelector("#combo-btn .off_btn").style.display = "block"
				displayComboSwitch = false;
			}
		})
		clearInterval(createOptionInterval)
	}
}


const typingAppMod = () => {

const example_container = document.getElementById("example_container") //iframe内の要素を取得


	if(example_container){

let keyJudge = event => {

		let sentenceText = document.getElementsByClassName("entered")[enteredClass]

		if(sentenceText){

			sentenceText = document.getElementsByClassName("entered")[enteredClass].nextSibling.textContent

			let char = windows_keymap[event.code] ? windows_keymap[event.code] : kana_keymap[event.key];
			if(!char || (event.key == ' ' && typingMode != "eng")){return;}

			//正タイプ
			combo++
			updateCombo()
			if(scoreCheck){
				scoreCheck.bestScorePass()
			}
			if(sentenceText.length == 1){
				//ラインクリア
				clearLineCount++
			}
			console.log(combo)
		}else{

			const startMsg = document.getElementById("start_msg")
			const countdown = document.getElementById('countdown')
			if((startMsg || countdown) && event.key == ' '){
				totalCombo = +localStorage.getItem("totalCombo")
				updateCombo()
				document.getElementById('total-combo').textContent = totalCombo
				document.getElementById('round').textContent = localStorage.getItem("round")
				missScreenObserver()
				appObserver()
				wpmCountFlag = true
				scoreCheck = new ScoreCheck()
				document.getElementById("wpm-area").classList.add('hide')
			}
		}
}

if(displayComboSwitch){
	const width = document.getElementById("example_container").clientWidth
	const scale = document.getElementById("app").style.webkitTransform.replace(/[^\d^\.]/g,'')
	document.addEventListener("keydown",keyJudge,false)
	document.getElementById("ad_frame").style.display = 'none';
	document.getElementById("ad_frame").insertAdjacentHTML('beforebegin',
`<div id="combo-container" style="width:${width * (scale ? scale:1)}px;">
<span id="combo-area" class="area" title="最高記録:${bestCombo}コンボ"><ruby><span id="total-combo">${totalCombo}</span><rt>累計</rt></ruby>
 + <ruby><span id="combo">0</span><rt>コンボ</rt></ruby>
 = <ruby><span id="all-combo" class="${bestCombo < totalCombo ? 'gold' : ''}">${totalCombo}</span><rt>継続</rt></ruby></span>
 <span id="round-area" class="area"><span id="round">${+localStorage.getItem('round')}</span>周</span>
 <span id="wpm-area" class="hide area"><ruby><span id="average-wpm_">---</span><rt>平均WPM</rt></ruby></span></div>
	<style>
	#combo-container{
    margin-left: auto;
    margin-right: auto;
	font-weight: bold;
    font-size: 2rem;
	margin-top: 2rem;
    white-space: nowrap;
	margin-bottom: 2rem;
	}
	.area{
	padding: 0.2rem 1rem;
    background: #ff9c0091;
    border-radius: 23px;
	}
    #combo-container > span:not(:first-child){
    margin-left:1rem;
	}
	#wpm-area{
	padding-top:1rem;
	}
	#combo-area{
	padding-top:1.5rem;
	}
	#all-combo{
	font-size:2.5rem;
	}
	.hide{
    visibility: hidden;
	}
	.gold{
    color: gold;
    text-shadow:
           1px 1px 0px #000, -1px -1px 0px #000,
          -1px 1px 0px #000,  1px -1px 0px #000,
           1px 0px 0px #000, -1px  0px 0px #000,
           0px 1px 0px #000,  0px -1px 0px #000;
	}
	</style>
	`)
	window.addEventListener('beforeunload', () => {
		if(combo){
			updateTotalCombo()
			updateRoundCount()
		}
	})
	if(window.parent.document.getElementsByClassName('pp_close').length){
		window.parent.document.getElementsByClassName('pp_close')[0].addEventListener('click', () => {
			updateTotalCombo()
			updateRoundCount()
		})
	}

	window.addEventListener('resize',resize)

}

		clearInterval(typingAppModInterval)
		typingAppModInterval = null
	}

		console.log("searching for example_container")
}


function updateCombo(){
	document.getElementById('combo').textContent = combo
	document.getElementById('all-combo').textContent = totalCombo + combo
}

function updateTotalCombo(){
	localStorage.setItem("totalCombo",+localStorage.getItem("totalCombo") + combo)
	combo = 0
}

class ScoreCheck{

	bestScorePass(){
		if(bestCombo < (totalCombo + combo)){
			document.getElementById('all-combo').classList.add('gold')
			scoreCheck = null
		}
	}

}

function resetCombo(){
	combo = 0
	totalCombo = 0
	localStorage.setItem("totalCombo",0)
}

function updateRoundCount(){
	let round = +localStorage.getItem('round')
	round += Math.round((clearLineCount/15) * 10) / 10;
	localStorage.setItem("round",round)
	clearLineCount = 0
}

function updateAverageWpm(){
	setTimeout( () => {
		if(document.getElementById('result') != null){
			const results = document.getElementsByClassName("result_data")[0].firstElementChild.children
			const aveWpm = +localStorage.getItem('averageWPM')
			for(let i=0;i<results.length;i++){
				if(results[i].firstElementChild.textContent == 'WPM'){
					const wpm = +results[i].lastElementChild.textContent
					putOptionSaveData(wpm)
					getAllIndexeddbData(wpm)
				}

			}
		}else{
			updateAverageWpm()
		}
	},100)

}

function reset(){

	if(bestCombo < (totalCombo + combo)){
		bestCombo = (totalCombo + combo)
		localStorage.setItem('bestCombo',totalCombo + combo)
		document.getElementById('combo-area').title = `最高記録:${bestCombo}コンボ`
	}
	document.getElementById('all-combo').classList.remove('gold')
	resetCombo()
	updateCombo()
	clearData()
	document.getElementById('total-combo').textContent = totalCombo
	localStorage.setItem("round",0)
	clearLineCount = 0
	document.getElementById('round').textContent = 0
	wpmCountFlag = false
	scoreCheck = new ScoreCheck()
}


const createEventInTypingApp = (() => {

	// https://www.e-typing.ne.jp/app/jsa_std/typing.asp タイピング画面のiframe URL
	if( location.href.match(/app\/jsa_/)){

		typingAppModInterval = setInterval(typingAppMod , 50)
		createOptionInterval = setInterval(createOption , 100)
	}

})()

function resize(){
	if(document.getElementById("example_container") == null){return;}
	const width = document.getElementById("example_container").clientWidth
	const scale = document.getElementById("app").style.webkitTransform.replace(/[^\d^\.]/g,'')
	document.getElementById('combo-container').style.width = (width * (scale ? scale:1)) + "px"
}

function missScreenObserver(){

		const target = document.getElementById('miss_type_screen'); // body要素を監視
		const observer_a = new MutationObserver(function (mutations) {
			// observer.disconnect(); // 監視を終了
			if(combo){
				reset()
			}
		});

		// 監視を開始
		observer_a.observe(target, {
        attributes: true // 属性変化の監視
		});

}


function appObserver(){

		const target = document.getElementById('app'); // body要素を監視
		const observer_b = new MutationObserver(function (mutations) {
			observer_b.disconnect(); // 監視を終了
			updateTotalCombo()
			updateRoundCount()
			if(wpmCountFlag){
				updateAverageWpm()
			}
		});

		// 監視を開始
		observer_b.observe(target, {
        childList: true
		});

}



let kana_keymap = {
					0: ["わ"],
					1: ["ぬ"],
					"!": ["ぬ"],
					2: ["ふ"],
					3: ["あ"],
					4: ["う"],
					5: ["え"],
					6: ["お"],
					7: ["や"],
					8: ["ゆ"],
					9: ["よ"],
					"-": ["ほ","-"],
					"q": ["た"],
					"Q": ["た"],
					"w": ["て"],
					"W": ["て"],
					"e": ["い"],
					"E": ["い"],
					"r": ["す"],
					"R": ["す"],
					"t": ["か"],
					"T": ["か"],
					"y": ["ん"],
					"Y": ["ん"],
					"u": ["な"],
					"U": ["な"],
					"i": ["に"],
					"I": ["に"],
					"o": ["ら"],
					"O": ["ら"],
					"p": ["せ"],
					"P": ["せ"],
					"a": ["ち"],
					"A": ["ち"],
					"s": ["と"],
					"S": ["と"],
					"d": ["し"],
					"D": ["し"],
					"f": ["は"],
					"F": ["は"],
					"g": ["き"],
					"G": ["き"],
					"h": ["く"],
					"H": ["く"],
					"j": ["ま"],
					"J": ["ま"],
					"k": ["の"],
					"K": ["の"],
					"l": ["り"],
					"L": ["り"],
					"z": ["つ"],
					"Z": ["つ"],
					"x": ["さ"],
					"X": ["さ"],
					"c": ["そ"],
					"C": ["そ"],
					"v": ["ひ"],
					"V": ["ひ"],
					"b": ["こ"],
					"B": ["こ"],
					"n": ["み"],
					"N": ["み"],
					"m": ["も"],
					"M": ["も"],
					",": ["ね",","],
					"<": ["、"],
					".": ["る","."],
					">": ["。"],
					"/": ["め","/"],
					"?": ["・"],
					"#": ["ぁ"],
					"$": ["ぅ"],
					"%": ["ぇ"],
					"'": ["ゃ","’","'"],
					"^": ["へ"],
					"~": ["へ"],
					"&": ["ぉ"],
					"(": ["ゅ"],
					")": ["ょ"],
					'|': ["ー"],
					"_": ["ろ"],
					"=": ["ほ"],
					"+": ["れ"],
					";": ["れ"],
					'"': ["ふ","”","“","\""],
					"@": ["゛"],
					'`': ["゛"],
					"[": ["゜"],
					']': ["む"],
					"{": ["「"],
					'}': ["」"],
					":": ["け"],
					"*": ["け"]
				}
let windows_keymap = {
					'IntlYen': ["ー","¥","\\"],
					"IntlRo": ["ろ","¥","\\"],
					"Space": [" "],
					"Numpad1": [],
					"Numpad2": [],
					"Numpad3": [],
					"Numpad4": [],
					"Numpad5": [],
					"Numpad6": [],
					"Numpad7": [],
					"Numpad8": [],
					"Numpad9": [],
					"Numpad0": [],
					"NumpadDivide": [],
					"NumpadMultiply": [],
					"NumpadSubtract": [],
					"NumpadAdd": [],
					"NumpadDecimal": []
				}



let db;
let indexedDB = window.indexedDB || window.mozIndexedDB || window.msIndexedDB;
let OptionDatabaseObject = {}
const STORE_NAME = "wpmDB";
const STORE_KEYPATH = ["wpm"];

//filterDBにアクセス
(function accessIndexedDB(){
	const OPEN_REQUEST = indexedDB.open(STORE_NAME, 1.0);

	//データベースストア新規作成。(初回アクセス時)
	OPEN_REQUEST.onupgradeneeded = function(event) {
		// データベースのバージョンに変更があった場合(初めての場合もここを通ります。)
		db = event.target.result;
		const CREATE_filterList = db.createObjectStore("wpm", { keyPath:STORE_KEYPATH[0]});
  }
	//データベースストアアクセス成功時。
	OPEN_REQUEST.onsuccess = function(event) {
		db = event.target.result;
	}
})();


function getAllIndexeddbData(wpm) {
	//トランザクション
	var transaction = db.transaction(STORE_KEYPATH, 'readonly');
	//オブジェクトストアにアクセスします。
	var listObjectStore = transaction.objectStore(STORE_KEYPATH[0]);
	//全件取得
	var listRequest = listObjectStore.getAllKeys()
	//取得が成功した場合の関数宣言
	listRequest.onsuccess = function (event) {
	  const result = event.currentTarget.result
	  let sum = result.reduce(function (acc, cur) {
		  return acc + cur;
	  });
		document.getElementById("average-wpm_").textContent = ((sum+wpm) / (result.length+1)).toFixed(2)
		document.getElementById("wpm-area").classList.remove('hide')
	};
  }

//データを保存
function putOptionSaveData(Data){
	const SEND_DATA = {[STORE_KEYPATH[0]] : Data};
	const OPEN_REQ = window.indexedDB.open(STORE_NAME);

	OPEN_REQ.onsuccess = function(event){
		var db = event.target.result;
		var trans = db.transaction(STORE_KEYPATH[0], 'readwrite');
		var store = trans.objectStore(STORE_KEYPATH[0]);
		var putReq = store.put(SEND_DATA);
	}
}

//削除
function clearData() {
  // open a read/write db transaction, ready for clearing the data
  const transaction = db.transaction(STORE_KEYPATH, "readwrite");

  // create an object store on the transaction
  const objectStore = transaction.objectStore(STORE_KEYPATH[0]);

  // Make a request to clear all the data out of the object store
  const objectStoreRequest = objectStore.clear();

  objectStoreRequest.onsuccess = (event) => {
    // report the success of our request
  };
};