FDU_Class_Schedule_Generator

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

当前为 2020-07-15 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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()