TypingClub layout switcher

Auto-switch keyboard layouts on TypingClub website

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TypingClub layout switcher
// @namespace    Winand
// @version      25.0
// @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
// @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 = url_api + "program2/"; // e.g. https://www.edclub.com/api/v1.1/program2/3/?edclub=true&_=1239959050370
    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

    (function(open, send) {
        // https://stackoverflow.com/a/56499250
        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_program) && !url.includes("/game/") && this.status == 200) {
                    const resp = JSON.parse(this.responseText);
                    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(program_id)
                            } else console.log("SWITCH FAILED WITH STATUS", response.status);
                        });
                    }
                } 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 + "/") {
                // FIXME: 'keyboard' may be undefined if other property is being changed
                keyboard_pending = JSON.parse(body).keyboard || keyboard_pending;
            }
            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);
})();