// ==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
};
};