FDU_Class_Schedule_Generator

Generate an .ics format document from eHall(https://my.fudan.edu.cn/list/bks_xx_kcb)

目前為 2020-07-15 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name FDU_Class_Schedule_Generator
// @name:zh 复旦大学课表生成器
// @description Generate an .ics format document from eHall(https://my.fudan.edu.cn/list/bks_xx_kcb)
// @description:zh 从eHall个人信息(https://my.fudan.edu.cn/list/bks_xx_kcb)生成ics格式的课表
// @version 0.1.9
// @include https://my.fudan.edu.cn/list/bks_xx_kcb
// @supportURL [email protected]
// @namespace https://greasyfork.org/users/666994
// ==/UserScript==


function chooseSemester() {
    //get semesters id
    const nav = document.getElementsByClassName("nav")[0];
    semesters = {};
    for (const child of nav.children) {
        const semesterNavReg = /nav-(\d{10})/;
        if (semesterNavReg.test(child.id))
            semesters[RegExp.$1] = child.firstElementChild.innerHTML;
    }

    //generate choose form
    let formHTML = "";
    for (const semestersId in semesters) {
        formHTML += `
<div class="radio">
  <label>
    <input type="radio" name="semesterOption" id="${semestersId}" value="${semestersId}">
    ${semesters[semestersId]}
  </label>
</div>
`
    }

    //include bootstrap
    let headElement = document.getElementsByTagName('head')[0];
    headElement.innerHTML += `
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
    `;

    //generate choose modal
    let bodyElement = document.getElementsByTagName('body')[0];
    bodyElement.innerHTML += `
    <!-- Modal -->
    <div class="modal fade" id="chooseModal" tabindex="-1" role="dialog" aria-labelledby="chooseModalLabel">
    <div class="modal-dialog" role="document">
    <div class="modal-content">
    <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <h4 class="modal-title" id="chooseModalLabel">选择学期</h4>
    </div>
    <form name="chooseForm" class="modal-body">
    ${formHTML}
    </form>
    <div class="modal-footer">
    <button class="btn btn-primary" id="chooseModalSubmit" data-dismiss="modal">生成课表</button>
    </div>
    </div>
    </div>
    </div>
    `;
    document.getElementById("chooseModalSubmit")
        .addEventListener("click", generateClassSchedule);
    $('#chooseModal').modal('show');
}

function chooseFirstDate(semesterId) {
    let bodyElement = document.getElementsByTagName('body')[0];
    bodyElement.innerHTML += `
    <!-- Modal -->
    <div class="modal fade" id="chooseFirstDateModal" tabindex="-1" role="dialog" aria-labelledby="chooseFirstDateModalLabel">
    <div class="modal-dialog" role="document">
    <div class="modal-content">
    <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    <h4 class="modal-title" id="chooseFirstDateModalLabel">选择这个学期的起始</h4>
    </div>
    <div class="modal-body">
    <p>请从<a href="http://www.jwc.fudan.edu.cn/">教务处</a>找到这个学期的起始,即第0周的星期日</p>
    <form name="chooseFirstDateForm">
    <input type="datetime-local" name="fisrtDate" id="firstDate">
    </form>
    </div>
    <div class="modal-footer">
    <button class="btn btn-primary" id="chooseFirstDateModalSubmit" data-dismiss="modal">选择</button>
    </div>
    </div>
    </div>
    </div>
    `;
    document.getElementById("chooseFirstDateModalSubmit")
        .addEventListener("click", () => getFirstDate(semesterId));
    $('#chooseFirstDateModal').modal('show');
}

function getFirstDate(semesterId) {
    const firstDate = document.chooseFirstDateForm.fisrtDate.value;
    FirstdateOfSemester = firstDate.slice(0, 10);
    includeFileSaver();
    let blob = new Blob([iCalendarFormatter.ClassesCalendar(getClasses(semesterId))], { type: "text/plain;charset=utf-8" });
    saveAs(blob, semesters[semesterId] + ".ics");
}

function generateClassSchedule() {
    const semesterId = document.chooseForm.semesterOption.value;
    PublicClassTime = getPublicClassTime(semesterId);
    chooseFirstDate(semesterId);
}

class ClassTimeAndPlace {

    //教室、上课时间、上课周
    constructor(classroom, time, weeks) {
        this.classroom = classroom;
        this.parseTime(time);
        this.parseWeeks(weeks);
    }

    //把上课时间转化为Datetime格式
    parseTime(time) {
        const timeReg = /星期(.) 第(\d*)-(\d*)节/;
        if (timeReg.exec(time)) {
            this.weekday = ClassTimeAndPlace.WEEKDAYS()[RegExp.$1];
            this.starttime = PublicClassTime[parseInt(RegExp.$2) - 1][0];
            this.endtime = PublicClassTime[parseInt(RegExp.$3) - 1][1];
        }
    }

    //把上课周转化为数组
    parseWeeks(weeks) {
        const weeksReg = /(\d+-\d+|\d)/g,
            weekReg = /(\d+)/,
            durationReg = /(\d+)-(\d+)/;
        this.weeks = [];
        let weekDurations = [];
        while (weeksReg.test(weeks)) {
            weekDurations.push(RegExp.$1);
        }
        for (const duration of weekDurations) {
            if (durationReg.test(duration)) {
                this.weeks.push(
                    [firstDatetimeOfWeek(parseInt(RegExp.$1)),
                    lastDatetimeOfWeek(parseInt(RegExp.$2))]);
            }
            else if (weekReg.test(duration)) {
                this.weeks.push(
                    [firstDatetimeOfWeek(parseInt(RegExp.$1)),
                    lastDatetimeOfWeek(parseInt(RegExp.$1))])
            }
        }
    }

    //获取第一次课的开始时间
    getFirstStartDatetime() {
        const firstWeek = this.weeks[0][0],
            firstDate = dateToDatetime(new Date(datetimeToDate(firstWeek).getTime() + ClassTimeAndPlace.WEEKDAYS_TO_NUM()[this.weekday] * 24 * 60 * 60 * 1000));
        return firstDate.slice(0, -6) + this.starttime;
    }

    //获取第一次课的结束时间
    getFirstEndDatetime() {
        const firstWeek = this.weeks[0][0],
            firstDate = dateToDatetime(new Date(datetimeToDate(firstWeek).getTime() + ClassTimeAndPlace.WEEKDAYS_TO_NUM()[this.weekday] * 24 * 60 * 60 * 1000));
        return firstDate.slice(0, -6) + this.endtime;
    }
    static WEEKDAYS() {
        return {
            "日": "SU",
            "一": "MO",
            "二": "TU",
            "三": "WE",
            "四": "TH",
            "五": "FR",
            "六": "SA"
        }
    }
    static WEEKDAYS_TO_NUM() {
        return {
            "SU": 0,
            "MO": 1,
            "TU": 2,
            "WE": 3,
            "TH": 4,
            "FR": 5,
            "SA": 6
        }
    }
}

class Class {
    constructor(id, name) {
        this.id = id;
        this.name = name;
        this.timeAndPlace = [];
    }
    AddTimeAndPlace(classroom, time, weeks) {
        this.timeAndPlace.push(new ClassTimeAndPlace(classroom, time, weeks));
    }
}

function getClasses(semesterId) {
    let listId = "list-" + semesterId,
        list = document.getElementById(listId),
        table = list
            .getElementsByTagName("table")[0]
            .getElementsByTagName("tbody")[0],
        rowSpan = 0,
        currentClass,
        Classes = [];
    for (const record of table.children) {
        if (rowSpan <= 0) {
            rowSpan = record.children[0].rowSpan;
            currentClass = new Class(
                record.children[0].innerHTML,
                record.children[1].innerHTML);
            currentClass.AddTimeAndPlace(
                record.children[2].innerHTML,
                record.children[3].innerHTML,
                record.children[4].innerHTML);
        }
        else {
            currentClass.AddTimeAndPlace(
                record.children[0].innerHTML,
                record.children[1].innerHTML,
                record.children[2].innerHTML);
        }
        rowSpan--;
        if (rowSpan <= 0) {
            Classes.push(currentClass);
        }
    }
    return Classes;
}

function getPublicClassTime(semesterId) {
    const tableId = "table-" + semesterId + "-1",
        table = document.getElementById(tableId).getElementsByTagName("tbody")[0],
        timeReg = /(\d+):(\d+)-(\d+):(\d+)/;
    let times = [];
    for (const row of table.children) {
        const record = row.firstElementChild.rowSpan == 1 ? row.firstElementChild : row.firstElementChild.nextElementSibling;
        if (timeReg.exec(record.innerHTML)) {
            let begintime = (RegExp.$1.length == 1 ? "0" : "") + RegExp.$1 + RegExp.$2 + "00",
                endtime = (RegExp.$3.length == 1 ? "0" : "") + RegExp.$3 + RegExp.$4 + "00";
            times.push([begintime, endtime]);
        }
    }
    return times;
}

function firstDatetimeOfWeek(week) {
    const FirstDateOfSemester = new Date(FirstdateOfSemester),
        FirstDateOfWeek = new Date(FirstDateOfSemester.getTime() + week * 7 * 24 * 60 * 60 * 1000);
    return dateToDatetime(FirstDateOfWeek);
}

function lastDatetimeOfWeek(week) {
    const FirstDatetimeOfSemester = new Date(FirstdateOfSemester),
        LastDatetimeOfWeek = new Date(FirstDatetimeOfSemester.getTime() + (week + 1) * 7 * 24 * 60 * 60 * 1000 - 1);
    return dateToDatetime(LastDatetimeOfWeek)
}

function dateToDatetime(date) {
    return ""
        + date.getFullYear()
        + ("0" + (date.getMonth() + 1)).slice(-2)
        + ("0" + date.getDate()).slice(-2)
        + "T"
        + ("0" + date.getHours()).slice(-2)
        + ("0" + date.getMinutes()).slice(-2)
        + ("0" + date.getSeconds()).slice(-2);
}

function datetimeToDate(datetime) {
    let date = datetime.slice(0, 4) + "/";
    date += datetime.slice(4, 6) + "/";
    date += datetime.slice(6, 8) + " ";
    date += datetime.slice(9, 11) + ":";
    date += datetime.slice(11, 13) + ":";
    date += datetime.slice(13, 15);
    return new Date(date);
}

class iCalendarFormatter {
    static ClassEvent(ClassRecord) {
        let event = "";
        for (const Class of ClassRecord.timeAndPlace) {
            event += "BEGIN:VEVENT\n";
            event += "SUMMARY:" + ClassRecord.name + "\n";
            event += "LOCATION:" + Class.classroom + "\n";
            event += "DTSTART:" + Class.getFirstStartDatetime() + "\n";
            event += "DTEND:" + Class.getFirstEndDatetime() + "\n";
            event += "RRULE:FREQ=WEEKLY;"
                + "BYDAY=" + Class.weekday + "\n";
            event += "RDATE;VALUE=PERIOD:"
            for (const week of Class.weeks) {
                event += week[0] + "/" + week[1] + ",";
            } event = event.slice(0, -1) + "\n";
            event += "END:VEVENT\n";
        }
        return event;
    }
    static ClassesCalendar(Classes) {
        let calendar = "BEGIN:VCALENDAR\nVERSION:2.0\n";
        for (const Class of Classes) {
            calendar += iCalendarFormatter.ClassEvent(Class);
        }
        calendar += "END:VCALENDAR\n";
        return calendar;
    }
}

function includeFileSaver() {
    (function (global, factory) {
        if (typeof define === "function" && define.amd) {
            define([], factory);
        } else if (typeof exports !== "undefined") {
            factory();
        } else {
            var mod = {
                exports: {}
            };
            factory();
            global.FileSaver = mod.exports;
        }
    })(this, function () {
        "use strict";

        /*
        * FileSaver.js
        * A saveAs() FileSaver implementation.
        *
        * By Eli Grey, http://eligrey.com
        *
        * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
        * source  : http://purl.eligrey.com/github/FileSaver.js
        */
        // The one and only way of getting global scope in all environments
        // https://stackoverflow.com/q/3277182/1008999
        var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : void 0;

        function bom(blob, opts) {
            if (typeof opts === 'undefined') opts = {
                autoBom: false
            }; else if (typeof opts !== 'object') {
                console.warn('Deprecated: Expected third argument to be a object');
                opts = {
                    autoBom: !opts
                };
            } // prepend BOM for UTF-8 XML and text/* types (including HTML)
            // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF

            if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
                return new Blob([String.fromCharCode(0xFEFF), blob], {
                    type: blob.type
                });
            }

            return blob;
        }

        function download(url, name, opts) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', url);
            xhr.responseType = 'blob';

            xhr.onload = function () {
                saveAs(xhr.response, name, opts);
            };

            xhr.onerror = function () {
                console.error('could not download file');
            };

            xhr.send();
        }

        function corsEnabled(url) {
            var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker

            xhr.open('HEAD', url, false);

            try {
                xhr.send();
            } catch (e) { }

            return xhr.status >= 200 && xhr.status <= 299;
        } // `a.click()` doesn't work for all browsers (#465)


        function click(node) {
            try {
                node.dispatchEvent(new MouseEvent('click'));
            } catch (e) {
                var evt = document.createEvent('MouseEvents');
                evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
                node.dispatchEvent(evt);
            }
        }

        var saveAs = _global.saveAs || ( // probably in some web worker
            typeof window !== 'object' || window !== _global ? function saveAs() { }
                /* noop */
                // Use download attribute first if possible (#193 Lumia mobile)
                : 'download' in HTMLAnchorElement.prototype ? function saveAs(blob, name, opts) {
                    var URL = _global.URL || _global.webkitURL;
                    var a = document.createElement('a');
                    name = name || blob.name || 'download';
                    a.download = name;
                    a.rel = 'noopener'; // tabnabbing
                    // TODO: detect chrome extensions & packaged apps
                    // a.target = '_blank'

                    if (typeof blob === 'string') {
                        // Support regular links
                        a.href = blob;

                        if (a.origin !== location.origin) {
                            corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank');
                        } else {
                            click(a);
                        }
                    } else {
                        // Support blobs
                        a.href = URL.createObjectURL(blob);
                        setTimeout(function () {
                            URL.revokeObjectURL(a.href);
                        }, 4E4); // 40s

                        setTimeout(function () {
                            click(a);
                        }, 0);
                    }
                } // Use msSaveOrOpenBlob as a second approach
                    : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) {
                        name = name || blob.name || 'download';

                        if (typeof blob === 'string') {
                            if (corsEnabled(blob)) {
                                download(blob, name, opts);
                            } else {
                                var a = document.createElement('a');
                                a.href = blob;
                                a.target = '_blank';
                                setTimeout(function () {
                                    click(a);
                                });
                            }
                        } else {
                            navigator.msSaveOrOpenBlob(bom(blob, opts), name);
                        }
                    } // Fallback to using FileReader and a popup
                        : function saveAs(blob, name, opts, popup) {
                            // Open a popup immediately do go around popup blocker
                            // Mostly only available on user interaction and the fileReader is async so...
                            popup = popup || open('', '_blank');

                            if (popup) {
                                popup.document.title = popup.document.body.innerText = 'downloading...';
                            }

                            if (typeof blob === 'string') return download(blob, name, opts);
                            var force = blob.type === 'application/octet-stream';

                            var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari;

                            var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);

                            if ((isChromeIOS || force && isSafari) && typeof FileReader === 'object') {
                                // Safari doesn't allow downloading of blob URLs
                                var reader = new FileReader();

                                reader.onloadend = function () {
                                    var url = reader.result;
                                    url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;');
                                    if (popup) popup.location.href = url; else location = url;
                                    popup = null; // reverse-tabnabbing #460
                                };

                                reader.readAsDataURL(blob);
                            } else {
                                var URL = _global.URL || _global.webkitURL;
                                var url = URL.createObjectURL(blob);
                                if (popup) popup.location = url; else location.href = url;
                                popup = null; // reverse-tabnabbing #460

                                setTimeout(function () {
                                    URL.revokeObjectURL(url);
                                }, 4E4); // 40s
                            }
                        });
        _global.saveAs = saveAs.saveAs = saveAs;

        if (typeof module !== 'undefined') {
            module.exports = saveAs;
        }
    });
}

chooseSemester()