// ==UserScript==
// @name TypingClub layout switcher
// @namespace Winand
// @version 23.329
// @description Auto-switch keyboard layouts on TypingClub website
// @homepageURL https://github.com/Winand/tc_layoutswitcher
// @author Winand
// @license MIT
// @match https://www.typingclub.com/*
// @match https://www.edclub.com/sportal/*
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const url_api = window.location.origin + "/api/v1.1/";
const url_student = url_api + "student/";
const url_tokens = window.location.origin + "/auth/refresh_tokens/";
const url_program = "/m/build/lessonplans/"; // e.g. https://static.typingclub.com/m/build/lessonplans/54.json
const url_program_page = (program_id) => `${window.location.origin}/sportal/program-${program_id}.game`; //https://stackoverflow.com/a/75611091
// in case there's no saved layout and current program doesn't define one either
const default_layout = "en,british-pc";
var token; // authorization token is needed to change layout
var student_id; // current user numeric id
var program_id; // current program numeric id
var program_kbd; // keyboard layout defined in current program
var keyboard; // current layout
var keyboard_pending; // layout is being set
/* Get pathname from URL. Protocol can be omitted: //website.name/pathname */
const getPathname = (url) => new URL((url.startsWith("//") ? window.location.protocol : "") + url).pathname;
/* Get the final path component, without its suffix.
https://stackoverflow.com/a/66939312 URL
https://stackoverflow.com/a/6941653 protocol
https://stackoverflow.com/a/45587081 rsplit
*/
const getStem = (fileName) => getPathname(fileName).split("/").pop().split(".").slice(0, -1).join('.');
(function(open, send, window_fetch) {
// https://stackoverflow.com/a/56499250
unsafeWindow.fetch = async (resource, ...args) => {
// https://stackoverflow.com/a/64961272/1119602
const response = await window_fetch(resource, ...args);
if(getPathname(resource).startsWith(url_program) && response.ok) {
response.clone().json().then(resp => {
program_id = resp.id;
console.log("LESSON PLANS LOADED FOR PROGRAM", program_id);
program_kbd = resp.keyboard == null ? default_layout : resp.keyboard;
var target_kbd = GM_getValue("lang." + program_id);
if(target_kbd == undefined) {
// set program's default layout
GM_setValue("lang." + program_id, program_kbd);
target_kbd = program_kbd;
}
console.log("PROGRAM KBD", program_kbd, "CURRENT", keyboard, "TARGET", target_kbd);
if(target_kbd !== keyboard) {
console.log("SWITCH TO", target_kbd, "TOKEN", token);
fetch(url_student + student_id + "/", {
method: 'POST',
body: '{"keyboard":"' + target_kbd + '"}',
headers: {
'authorization': 'Token ' + token,
'Content-type': 'application/json',
'x-http-method-override': 'PATCH'
}
}).then(response => {
if(response.status == 202) {
//https://stackoverflow.com/q/1226714#comment60232404_1226718
window.location = url_program_page(getStem(resource))
} else console.log("SWITCH FAILED WITH STATUS", response.status);
});
}
}).catch(err => console.error(err));
}
return response;
};
// unsafeWindow.fetch.toString = () => "function fetch() { [native code] }";
XMLHttpRequest.prototype.open = function(method, url) {
this._url = window.location.origin + url
this.addEventListener("load", function() {
var url = this.responseURL;
if(url == url_tokens && this.status == 200) {
if(this.responseText) {
const resp = JSON.parse(this.responseText)[0];
token = resp.token;
console.log("TOKEN REFRESHED", token);
} else console.log("TOKEN NOT REFRESHED");
} else if(url.startsWith(url_student + "me/") && this.status == 200) {
const resp = JSON.parse(this.responseText);
student_id = resp.id;
keyboard = resp.keyboard;
} else if(url == url_student + student_id + "/" && keyboard_pending !== undefined) {
if(this.status == 202) {
keyboard = keyboard_pending;
GM_setValue("lang." + program_id, keyboard);
console.log("MANUAL SWITCH TO", keyboard, "FOR PROGRAM", program_id);
} else console.log("MANUAL SWITCH FROM", keyboard, "TO", keyboard_pending, "FOR PROGRAM", program_id, "FAILED");
}
}, false);
open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
if(this._url == url_student + student_id + "/") {
// 'keyboard' may be undefined if other property is being changed
keyboard_pending = JSON.parse(body).keyboard;
}
send.apply(this, arguments);
};
if("prototype" in send) {
console.log("LOADED TOO LATE, TRYING TO REFRESH TOKEN");
var poll_timer = setInterval(() => {
if(!token) {
console.log("RESTART...");
start(); // TypingClub entry point
} else clearTimeout(poll_timer);
}, 1000);
};
})(XMLHttpRequest.prototype.open, XMLHttpRequest.prototype.send, window.fetch);
})();