// ==UserScript==
// @name WHU Schedule Export as iCS Calendar
// @name:zh 武大课程表导出为 iCS
// @name:zh-CN 武大课程表导出为 iCS
// @name:zh-TW 武大課程表匯出為 iCS
// @namespace https://github.com/Ostrichbeta/WHU-class-schedule-export-ics/raw/main/schedule_export.js
// @version 0.91
// @description Export your timetable as ics format.
// @description:zh-CN 导出课表为 ics 格式
// @description:zh-TW 匯出課表為 ics 格式
// @author Ostrichbeta Chan
// @license GPL-3.0
// @match https://jwgl.whu.edu.cn/kbcx/xskbcx_cxXskbcxIndex.html*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @require https://code.jquery.com/jquery-3.6.1.min.js
// @require https://cdn.jsdelivr.net/npm/opencc-js@1.0.4/data.min.js
// @require https://cdn.jsdelivr.net/npm/opencc-js@1.0.4/data.cn2t.min.js
// @require https://cdn.jsdelivr.net/npm/opencc-js@1.0.4/bundle-browser.min.js
// @require https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
window.jQuery361 = $.noConflict(true); // Avoid the confliction with the original page
/*
* 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
: this;
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 !== "undefined"
) {
// 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;
}
/* UUID genertor from https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid */
function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
);
}
/* global saveAs, Blob, BlobBuilder, console */
/* exported ics */
/* https://github.com/nwcell/ics.js */
var ics = function (uidDomain, prodId) {
"use strict";
if (
navigator.userAgent.indexOf("MSIE") > -1 &&
navigator.userAgent.indexOf("MSIE 10") == -1
) {
console.log("Unsupported Browser");
return;
}
if (typeof uidDomain === "undefined") {
uidDomain = "default";
}
if (typeof prodId === "undefined") {
prodId = "Calendar";
}
var SEPARATOR = navigator.appVersion.indexOf("Win") !== -1 ? "\r\n" : "\n";
var calendarEvents = [];
var calendarStart = [
"BEGIN:VCALENDAR",
"PRODID:" + prodId,
"VERSION:2.0",
].join(SEPARATOR);
var calendarEnd = SEPARATOR + "END:VCALENDAR";
var BYDAY_VALUES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
return {
/**
* Returns events array
* @return {array} Events
*/
events: function () {
return calendarEvents;
},
/**
* Returns calendar
* @return {string} Calendar in iCalendar format
*/
calendar: function () {
return (
calendarStart +
SEPARATOR +
calendarEvents.join(SEPARATOR) +
calendarEnd
);
},
/**
* Add event to the calendar
* @param {string} subject Subject/Title of event
* @param {string} description Description of event
* @param {string} location Location of event
* @param {string} begin Beginning date of event
* @param {string} stop Ending date of event
*/
addEvent: function (
subject,
description,
location,
begin,
stop,
rrule,
valarm
) {
// I'm not in the mood to make these optional... So they are all required
if (
typeof subject === "undefined" ||
typeof description === "undefined" ||
typeof location === "undefined" ||
typeof begin === "undefined" ||
typeof stop === "undefined"
) {
return false;
}
// validate rrule
if (rrule) {
if (!rrule.rrule) {
if (
rrule.freq !== "YEARLY" &&
rrule.freq !== "MONTHLY" &&
rrule.freq !== "WEEKLY" &&
rrule.freq !== "DAILY"
) {
throw "Recurrence rrule frequency must be provided and be one of the following: 'YEARLY', 'MONTHLY', 'WEEKLY', or 'DAILY'";
}
if (rrule.until) {
if (
isNaN(Date.parse(rrule.until)) &&
isNaN(Date.parse(rrule.until.toISOString()))
) {
throw "Recurrence rrule 'until' must be a valid date string";
}
}
if (rrule.interval) {
if (isNaN(parseInt(rrule.interval))) {
throw "Recurrence rrule 'interval' must be an integer";
}
}
if (rrule.count) {
if (isNaN(parseInt(rrule.count))) {
throw "Recurrence rrule 'count' must be an integer";
}
}
if (typeof rrule.byday !== "undefined") {
if (
Object.prototype.toString.call(rrule.byday) !== "[object Array]"
) {
throw "Recurrence rrule 'byday' must be an array";
}
if (rrule.byday.length > 7) {
throw "Recurrence rrule 'byday' array must not be longer than the 7 days in a week";
}
// Filter any possible repeats
rrule.byday = rrule.byday.filter(function (elem, pos) {
return rrule.byday.indexOf(elem) == pos;
});
for (var d in rrule.byday) {
if (BYDAY_VALUES.indexOf(rrule.byday[d]) < 0) {
throw "Recurrence rrule 'byday' values must include only the following: 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'";
}
}
}
}
}
// validate valarm
if (valarm && Object.keys(valarm).length != 0) {
if (valarm.trigger) {
if (
isNaN(valarm.trigger) ||
!(typeof valarm.trigger === "number")
) {
throw "Trigger time must be an integer!";
}
}
if (valarm.description) {
if (!(typeof valarm.description === "string")) {
throw "The description of valarm must be a string!";
}
}
}
//TODO add time and time zone? use moment to format?
var start_date = new Date(begin);
var end_date = new Date(stop);
var now_date = new Date();
var start_year = ("0000" + start_date.getFullYear().toString()).slice(
-4
);
var start_month = ("00" + (start_date.getMonth() + 1).toString()).slice(
-2
);
var start_day = ("00" + start_date.getDate().toString()).slice(-2);
var start_hours = ("00" + start_date.getHours().toString()).slice(-2);
var start_minutes = ("00" + start_date.getMinutes().toString()).slice(
-2
);
var start_seconds = ("00" + start_date.getSeconds().toString()).slice(
-2
);
var end_year = ("0000" + end_date.getFullYear().toString()).slice(-4);
var end_month = ("00" + (end_date.getMonth() + 1).toString()).slice(-2);
var end_day = ("00" + end_date.getDate().toString()).slice(-2);
var end_hours = ("00" + end_date.getHours().toString()).slice(-2);
var end_minutes = ("00" + end_date.getMinutes().toString()).slice(-2);
var end_seconds = ("00" + end_date.getSeconds().toString()).slice(-2);
var now_year = ("0000" + now_date.getFullYear().toString()).slice(-4);
var now_month = ("00" + (now_date.getMonth() + 1).toString()).slice(-2);
var now_day = ("00" + now_date.getDate().toString()).slice(-2);
var now_hours = ("00" + now_date.getHours().toString()).slice(-2);
var now_minutes = ("00" + now_date.getMinutes().toString()).slice(-2);
var now_seconds = ("00" + now_date.getSeconds().toString()).slice(-2);
// Since some calendars don't add 0 second events, we need to remove time if there is none...
var start_time = "";
var end_time = "";
if (
start_hours +
start_minutes +
start_seconds +
end_hours +
end_minutes +
end_seconds !=
0
) {
start_time = "T" + start_hours + start_minutes + start_seconds;
end_time = "T" + end_hours + end_minutes + end_seconds;
}
var now_time = "T" + now_hours + now_minutes + now_seconds;
var start = start_year + start_month + start_day + start_time;
var end = end_year + end_month + end_day + end_time;
var now = now_year + now_month + now_day + now_time;
// recurrence rrule vars
var rruleString;
if (rrule) {
if (rrule.rrule) {
rruleString = rrule.rrule;
} else {
rruleString = "RRULE:FREQ=" + rrule.freq;
if (rrule.until) {
var uDate = new Date(
Date.parse(rrule.until.toISOString())
).toISOString();
rruleString +=
";UNTIL=" +
uDate.substring(0, uDate.length - 13).replace(/[-]/g, "") +
"000000Z";
}
if (rrule.interval) {
rruleString += ";INTERVAL=" + rrule.interval;
}
if (rrule.count) {
rruleString += ";COUNT=" + rrule.count;
}
if (rrule.byday && rrule.byday.length > 0) {
rruleString += ";BYDAY=" + rrule.byday.join(",");
}
}
}
var valarmArray = [];
var valarmString;
if (valarm && Object.keys(valarm).length != 0) {
let uuid = uuidv4();
valarmArray.push("BEGIN:VALARM");
valarmArray.push("X-WR-ALARMUID:" + uuid);
valarmArray.push("UID:" + uuid);
valarmArray.push(
"TRIGGER:-PT" + Math.floor(valarm.trigger).toString() + "M"
);
valarmArray.push("ACTION:DISPLAY");
if (valarm.description) {
valarmArray.push("DESCRIPTION:" + valarm.description);
}
valarmArray.push("END:VALARM");
valarmString = valarmArray.join(SEPARATOR);
}
var stamp = new Date().toISOString();
var calendarEvent = [
"BEGIN:VEVENT",
"UID:" + calendarEvents.length + "@" + uidDomain,
"CLASS:PUBLIC",
"DESCRIPTION:" + description,
"DTSTAMP;VALUE=DATE-TIME:" + now,
"DTSTART;VALUE=DATE-TIME:" + start,
"DTEND;VALUE=DATE-TIME:" + end,
"LOCATION:" + location,
"SUMMARY:" + subject,
"TRANSP:TRANSPARENT",
"END:VEVENT",
];
if (rruleString) {
calendarEvent.splice(4, 0, rruleString);
}
if (valarm && Object.keys(valarm).length != 0) {
calendarEvent.splice(calendarEvent.length - 1, 0, valarmString);
}
calendarEvent = calendarEvent.join(SEPARATOR);
calendarEvents.push(calendarEvent);
return calendarEvent;
},
/**
* Download calendar using the saveAs function from filesave.js
* @param {string} filename Filename
* @param {string} ext Extention
*/
download: function (filename, ext) {
if (calendarEvents.length < 1) {
return false;
}
ext = typeof ext !== "undefined" ? ext : ".ics";
filename = typeof filename !== "undefined" ? filename : "calendar";
var calendar =
calendarStart +
SEPARATOR +
calendarEvents.join(SEPARATOR) +
calendarEnd;
var blob;
if (navigator.userAgent.indexOf("MSIE 10") === -1) {
// chrome or firefox
blob = new Blob([calendar]);
} else {
// ie
var bb = new BlobBuilder();
bb.append(calendar);
blob = bb.getBlob(
"text/x-vCalendar;charset=" + document.characterSet
);
}
saveAs(blob, filename + ext);
return calendar;
},
/**
* Build and return the ical contents
*/
build: function () {
if (calendarEvents.length < 1) {
return false;
}
var calendar =
calendarStart +
SEPARATOR +
calendarEvents.join(SEPARATOR) +
calendarEnd;
return calendar;
},
};
};
let _dayschedule = [
{ start: "00:00", end: "00:00", "🤔": ":thinking:" },
{ start: "08:00", end: "08:45" },
{ start: "08:50", end: "09:35" },
{ start: "09:50", end: "10:35" },
{ start: "10:40", end: "11:25" },
{ start: "11:30", end: "12:15" },
{ start: "14:05", end: "14:50" },
{ start: "14:55", end: "15:40" },
{ start: "15:45", end: "16:30" },
{ start: "16:40", end: "17:25" },
{ start: "17:30", end: "18:15" },
{ start: "18:30", end: "19:15" },
{ start: "19:20", end: "20:05" },
{ start: "20:10", end: "20:55" },
];
let _termschedule_start; // 学期开始的时间
function get_start_time(week, day, no) {
// Set the initial day to Sunday no matter whay day the start day is
let date = new Date(); // Get current timezone offset
let start_time = _termschedule_start + date.getTimezoneOffset() * 60 * 1000;
start_time -= new Date(_termschedule_start).getDay() * 86400 * 1000;
start_time += (week - 1) * 7 * 86400 * 1000;
start_time += day * 86400 * 1000;
let start_time_hhmm = _dayschedule[no]["start"].split(":");
start_time +=
parseInt(start_time_hhmm[0]) * 3600 * 1000 +
parseInt(start_time_hhmm[1]) * 60 * 1000;
return new Date(
start_time - 8 * 3600 * 1000 - date.getTimezoneOffset() * 60 * 1000
);
}
function get_end_time(week, day, no) {
let date = new Date();
let end_time = _termschedule_start + date.getTimezoneOffset() * 60 * 1000;
end_time -= new Date(_termschedule_start).getDay() * 86400 * 1000;
end_time += (week - 1) * 7 * 86400 * 1000;
end_time += day * 86400 * 1000;
let end_time_hhmm = _dayschedule[no]["end"].split(":");
end_time +=
parseInt(end_time_hhmm[0]) * 3600 * 1000 +
parseInt(end_time_hhmm[1]) * 60 * 1000;
return new Date(
end_time - 8 * 3600 * 1000 - date.getTimezoneOffset() * 60 * 1000
);
}
function get_end_of_week(week) {
let date = new Date();
return new Date(
_termschedule_start +
date.getTimezoneOffset() * 60 * 1000 -
new Date(_termschedule_start).getDay() * 86400 * 1000 +
week * 7 * 86400 * 1000 -
1 -
8 * 3600 * 1000 -
date.getTimezoneOffset() * 60 * 1000
);
}
// Language Check
let language_list = navigator.languages;
let tcindex = -1;
let scindex = -1;
for (let i = 0; i < language_list.length; i++) {
if (
language_list[i] == "zh" ||
language_list[i] == "zh-CN" ||
language_list[i] == "zh-SG" ||
language_list[i] == "zh-Hans"
)
scindex = i;
if (
language_list[i] == "zh-TW" ||
language_list[i] == "zh-HK" ||
language_list[i] == "zh-Hant"
)
tcindex = i;
}
if (tcindex < 0) tcindex = language_list.length;
if (scindex < 0) scindex = language_list.length;
function export_ics() {
var cal = ics();
let is_convert_to_tc = false;
var conf_form = $(
'<div class="form-content" style="display:none;">' +
' <form class="form" role="form" lang="' +
(scindex <= tcindex ? "zh-CN" : "zh-TW") +
'">' +
' <div class="form-group">' +
' <label for="start_date">' +
(scindex <= tcindex ? "开学日期" : "開學日期") +
"</label>" +
' <input type="date" class="bootbox-input-date form-control" id="start_date" name="start_date" value="' +
new Date().toISOString().slice(0, 10) +
'"></input>' +
" </div>" +
' <div class="form-group">' +
' <label for="lang_sel">' +
(scindex <= tcindex ? "课表语言" : "課表語言") +
"</label>" +
' <select class="bootbox-input bootbox-input-select form-control" id="lang_sel" name="lang_sel">' +
' <option value="zh-sc"' +
(scindex <= tcindex ? " selected" : "") +
">" +
(scindex <= tcindex ? "简体中文" : "簡體中文") +
"</option>" +
' <option value="zh-tc"' +
(scindex <= tcindex ? "" : " selected") +
">" +
(scindex <= tcindex ? "繁体中文" : "繁體中文") +
"</option>" +
" </select>" +
" </div>" +
' <div class="form-group">' +
' <div class="checkbox" id="hasAlarmSwitchDiv">' +
" <label>" +
' <input class="bootbox-input bootbox-input-checkbox" type="checkbox" id="hasAlarmSwitch">' +
" " +
(scindex <= tcindex ? "设定上课前提醒" : "設定上課前提醒") +
" </label>" +
" </div>" +
' <div id="alarm-panel" hidden>' +
' <div class="checkbox" id="hasAlarmSwitchDiv" hidden>' +
" <label>" +
' <input class="bootbox-input bootbox-input-checkbox" type="checkbox" id="hasOtherIntervalForFirstClass">' +
" " +
(scindex <= tcindex
? "第一节课另设提醒时间间隔"
: "第一節課另設提醒時間間隔") +
" </label>" +
" </div>" +
' <div class="row">' +
' <div class="col-sm-3">' +
' <div class="form-group">' +
' <label for="normal_trigger">' +
(scindex <= tcindex ? "课前提醒" : "課前提醒") +
"</label>" +
' <input type="number" class="bootbox-input-number form-control" id="normal_trigger" name="normal_trigger" value="15" min="0" max="1440"></input>' +
" </div>" +
" </div>" +
' <div class="col-sm-3">' +
' <div class="form-group">' +
' <label for="morning_first_class_trigger">' +
(scindex <= tcindex ? "早上首节前提醒" : "早上首節前提醒") +
"</label>" +
' <input type="number" disabled="true" class="bootbox-input-number form-control" id="morning_first_class_trigger" name="morning_first_class_trigger" value="15" min="0" max="1440"></input>' +
" </div>" +
" </div>" +
' <div class="col-sm-3">' +
' <div class="form-group">' +
' <label for="afternoon_first_class_trigger">' +
(scindex <= tcindex ? "下午首节前提醒" : "下午首節前提醒") +
"</label>" +
' <input type="number" disabled="true" class="bootbox-input-number form-control" id="afternoon_first_class_trigger" name="afternoon_first_class_trigger" value="15" min="0" max="1440"></input>' +
" </div>" +
" </div>" +
' <div class="col-sm-3">' +
' <div class="form-group">' +
' <label for="evening_first_class_trigger">' +
(scindex <= tcindex ? "晚上首节前提醒" : "晚上首節前提醒") +
"</label>" +
' <input type="number" disabled="true" class="bootbox-input-number form-control" id="evening_first_class_trigger" name="evening_first_class_trigger" value="15" min="0" max="1440"></input>' +
" </div>" +
" </div>" +
" </div>" +
' <div class="form-group" style="padding-top: 1px;">' +
' <p for="none">' +
(scindex <= tcindex
? "提醒时间单位为分钟,范围 0~1440 ,如果设定为 0 则不提醒。"
: "提醒時間單位為分鐘,範圍 0~1440 ,如果設定為 0 則不提醒。") +
" </p>" +
" </div>" +
" </div>" +
" </div>" +
' <div class="form-group" style="padding-top: 1px;">' +
' <p for="none">' +
(scindex <= tcindex
? '运行说明:在上面选择开学第一周的任意日期(由周日开始周六结束算一周),然后在下方选择课表导出的语言,再按下「导出」即可将课表存为 .ics 的日历格式。繁体中文的课表由原始表经过 <a href="https://github.com/BYVoid/OpenCC">OpenCC</a> 程序转换得出,可能会有字符错误,请谅解。<br>本程序免费并在 <a href="https://github.com/Ostrichbeta/WHU-class-schedule-export-ics">GitHub</a> 开放源代码。'
: '運行說明:在上面選擇開學第一週的任意日期(由週日開始週六結束為一週),然後在下方選擇課表匯出的語言,再按下「匯出」即可將課表存為 .ics 的日曆格式。繁體中文的課表由原始表經過 <a href="https://github.com/BYVoid/OpenCC">OpenCC</a> 程式轉換得出,可能會有字元錯誤,請諒解。<br>本程式免費並在 <a href="https://github.com/Ostrichbeta/WHU-class-schedule-export-ics">GitHub</a> 開放原始碼。') +
"</p>" +
" </div>" +
" </form>" +
"</div>"
);
// Control the toggle of alarm panel
$("body").on("change", "#hasAlarmSwitch", function () {
if ($("#hasAlarmSwitch") && $("#alarm-panel")) {
if ($("#hasAlarmSwitch").is(":checked")) {
$("#alarm-panel").show();
} else {
$("#alarm-panel").hide();
}
}
});
// Contol the toggle of unique interval for the first class
$("body").on("change", "#hasOtherIntervalForFirstClass", function () {
if ($("#hasOtherIntervalForFirstClass")) {
if ($("#hasOtherIntervalForFirstClass").is(":checked")) {
// Remove all the disability to edit other editboxes
if ($("#morning_first_class_trigger"))
$("#morning_first_class_trigger").prop("disabled", false);
if ($("#afternoon_first_class_trigger"))
$("#afternoon_first_class_trigger").prop("disabled", false);
if ($("#evening_first_class_trigger"))
$("#evening_first_class_trigger").prop("disabled", false);
} else {
if ($("#morning_first_class_trigger"))
$("#morning_first_class_trigger").prop("disabled", true);
if ($("#afternoon_first_class_trigger"))
$("#afternoon_first_class_trigger").prop("disabled", true);
if ($("#evening_first_class_trigger"))
$("#evening_first_class_trigger").prop("disabled", true);
// Reset all the values to the same as the first one
if ($("#morning_first_class_trigger"))
$("#morning_first_class_trigger").val($("#normal_trigger").val());
if ($("#afternoon_first_class_trigger"))
$("#afternoon_first_class_trigger").val($("#normal_trigger").val());
if ($("#evening_first_class_trigger"))
$("#evening_first_class_trigger").val($("#normal_trigger").val());
}
}
});
// Control the sync of the edit box
$("body").on("change", "#normal_trigger", function () {
if ($("#normal_trigger")) {
if (parseInt($("#normal_trigger").val()) > 1440) {
$("#normal_trigger").val("1440");
}
if (
parseInt($("#normal_trigger").val()) < 0 ||
$("#normal_trigger").val() == ""
) {
$("#normal_trigger").val("0");
}
if (!$("#hasOtherIntervalForFirstClass").is(":checked")) {
if ($("#morning_first_class_trigger"))
$("#morning_first_class_trigger").val($("#normal_trigger").val());
if ($("#afternoon_first_class_trigger"))
$("#afternoon_first_class_trigger").val($("#normal_trigger").val());
if ($("#evening_first_class_trigger"))
$("#evening_first_class_trigger").val($("#normal_trigger").val());
}
}
});
// Ensure all the inputs are in the correct range
$("body").on("change", "#morning_first_class_trigger", function () {
if ($("#morning_first_class_trigger")) {
if (parseInt($("#morning_first_class_trigger").val()) > 1440) {
$("#morning_first_class_trigger").val("1440");
}
if (
parseInt($("#morning_first_class_trigger").val()) < 0 ||
$("#morning_first_class_trigger").val() == ""
) {
$("#morning_first_class_trigger").val("0");
}
}
});
$("body").on("change", "#afternoon_first_class_trigger", function () {
if ($("#afternoon_first_class_trigger")) {
if (parseInt($("#afternoon_first_class_trigger").val()) > 1440) {
$("#afternoon_first_class_trigger").val("1440");
}
if (
parseInt($("#afternoon_first_class_trigger").val()) < 0 ||
$("#afternoon_first_class_trigger").val() == ""
) {
$("#afternoon_first_class_trigger").val("0");
}
}
});
$("body").on("change", "#evening_first_class_trigger", function () {
if ($("#evening_first_class_trigger")) {
if (parseInt($("#evening_first_class_trigger").val()) > 1440) {
$("#evening_first_class_trigger").val("1440");
}
if (
parseInt($("#evening_first_class_trigger").val()) < 0 ||
$("#evening_first_class_trigger").val() == ""
) {
$("#evening_first_class_trigger").val("0");
}
}
});
bootbox.confirm({
title: scindex <= tcindex ? "导出设置" : "匯出設定",
message: conf_form.html(),
size: "small",
buttons: {
cancel: {
label: scindex <= tcindex ? "取消" : "取消",
},
confirm: {
label: scindex <= tcindex ? "导出" : "匯出",
},
},
callback: function (result) {
if (!result) return;
else {
_termschedule_start = Date.parse($("#start_date").attr("value"));
is_convert_to_tc = $("#lang_sel").val() == "zh-tc";
// Fetch the vertical list
for (let i of $("#table2").children().eq(0).children()) {
if ($("#table2").children().eq(0).children().eq(0).is(i)) continue; // Skip the first element
if (typeof $(i).attr("id") == "undefined") continue;
let day_in_week =
parseInt($(i).attr("id").split("_")[1]) == 7
? 0
: parseInt($(i).attr("id").split("_")[1]);
let is_first_morning_class_set = false;
let is_first_afternoon_class_set = false;
let is_first_evening_class_set = false;
for (let j of $(i).children()) {
if ($(i).children().eq(0).is(j)) continue; // Skip the first element which is an indicator
let Tsubject = "";
let Tdescription = "";
let Tlocation = "";
let Tbegin = "";
let Tend = "";
let TbeginList = [];
let TendList = [];
let TuntilList = [];
let TrruleList = [];
let Tvalarm = {};
let single_class_obj = $(j)
.children()
.eq(
$(j).children().filter(":nth-child(1)").attr("rowspan") ==
undefined
? 0
: 1
)
.children()
.eq(0);
// Detects title, if a class has been modified, the span tag will be changed to u
if (
$(single_class_obj).children().filter("span.title").size() == 0
) {
Tsubject = $(single_class_obj)
.children()
.filter("u.title")
.text();
} else {
Tsubject = $(single_class_obj)
.children()
.filter("span.title")
.text();
}
let class_duration_obj = $(j).children().filter(":nth-child(1)");
while (class_duration_obj.attr("rowspan") == undefined) {
// If a single class' rowspan is not equal to 1, it means there are modifications for this class
class_duration_obj = class_duration_obj
.parent()
.prev()
.children()
.filter(":nth-child(1)");
}
let class_duration_list = class_duration_obj.text().match(/\d+/g); // e.g.: [1, 2]
for (let k of $(single_class_obj)
.children()
.filter(":nth-child(2)")
.children()) {
let class_information_child_text_raw = $(k).text().trim(); // Remove the edge spaces here.
let class_information_list =
class_information_child_text_raw.split(":");
switch (class_information_list[0]) {
case "周数":
let week_range_list =
class_information_list[1].match(/\d+(-\d+)?周(?:\([单双]\))?/g);
let week_duration_list = [];
week_range_list.forEach((item, index, arr) => {
let week_duration_item = item.match(/\d+/g);
let single_double = item.match(/\([单双]\)/);
let double_interval = false;
if (week_duration_item.length == 0) {
bootbox.alert({
title: scindex <= tcindex ? "错误" : "錯誤",
message:
scindex <= tcindex
? "周数未显示,无法生成!\n脚本只能取得屏幕上所显示的信息,请通过点按左侧齿轮,在菜单中选中「时间」项开启。"
: "週數未顯示,無法匯出!\n腳本只能取得螢幕上所顯示的資訊,請通過點按左側齒輪,在彈出選單中選中「時間」項開啟。",
size: "small",
});
return;
}
if (single_double) {
double_interval = true;
}
let startWeek = week_duration_item[0];
let endWeek = "";
if (week_duration_item.length == 1) {
endWeek = week_duration_item[0];
} else {
endWeek = week_duration_item[1];
}
week_duration_list.push([
parseInt(startWeek),
parseInt(endWeek),
double_interval
]);
});
for (let item of week_duration_list) {
TbeginList.push(
get_start_time(
item[0],
day_in_week,
parseInt(class_duration_list[0])
)
);
TendList.push(
get_end_time(
item[0],
day_in_week,
parseInt(
class_duration_list.length == 1
? class_duration_list[0]
: class_duration_list[1]
)
)
);
TuntilList.push(get_end_of_week(parseInt(item[1])));
let Trrule = {};
Trrule.freq = "WEEKLY";
Trrule.interval = item[2] ? 2 : 1;
TrruleList.push(Trrule);
}
break;
case "上课地点":
Tlocation = "武汉大学" + class_information_list[1];
break;
default:
Tdescription +=
(Tdescription == "" ? "" : "\\n") +
class_information_list[0] +
":" +
class_information_list[1];
}
}
// Check if the alarm switch is on, and check if this is the first class
if ($("#hasAlarmSwitch") && $("#hasAlarmSwitch").is(":checked")) {
if (
parseInt(class_duration_list[0]) >= 1 &&
parseInt(class_duration_list[0]) <= 5 &&
!is_first_morning_class_set
) {
// Class started in the morning
is_first_morning_class_set = true;
if (parseInt($("#morning_first_class_trigger").val()) != 0) {
// Ignore the alarm if the trigger time is zero
Tvalarm = {
trigger: parseInt(
$("#morning_first_class_trigger").val()
),
description: "alarm-morning-first-class",
};
}
} else if (
parseInt(class_duration_list[0]) >= 6 &&
parseInt(class_duration_list[0]) <= 10 &&
!is_first_afternoon_class_set
) {
// Class started in the afternoon
is_first_afternoon_class_set = true;
if (
parseInt($("#afternoon_first_class_trigger").val()) != 0
) {
Tvalarm = {
trigger: parseInt(
$("#afternoon_first_class_trigger").val()
),
description: "alarm-afternoon-first-class",
};
}
} else if (
parseInt(class_duration_list[0]) >= 11 &&
parseInt(class_duration_list[0]) <= 13 &&
!is_first_evening_class_set
) {
// Class started in the evening
is_first_evening_class_set = true;
if (
parseInt($("#afternoon_first_class_trigger").val()) != 0
) {
Tvalarm = {
trigger: parseInt(
$("#evening_first_class_trigger").val()
),
description: "alarm-evening-first-class",
};
}
} else {
// Normal class
if (parseInt($("#normal_trigger").val()) != 0) {
Tvalarm = {
trigger: parseInt($("#normal_trigger").val()),
description: "alarm-normal-class",
};
}
}
}
if (is_convert_to_tc) {
// Chinese Conversion
let converter = OpenCC.Converter({ from: "cn", to: "twp" });
Tsubject = converter(Tsubject);
Tdescription = converter(Tdescription);
Tlocation = converter(Tlocation);
}
for (let index = 0; index < TbeginList.length; index++) {
TrruleList[index].until = TuntilList[index];
console.log({
"tsubject": Tsubject,
"tdescription": Tdescription,
"tlocation": Tlocation,
"tbegin": TbeginList[index],
"tend": TendList[index],
"trrule": TrruleList[index],
"tvalarm": Tvalarm
});
cal.addEvent(
Tsubject,
Tdescription,
Tlocation,
TbeginList[index],
TendList[index],
TrruleList[index],
Tvalarm
);
}
}
}
let converter = OpenCC.Converter({ from: "cn", to: "twp" });
cal.download(
is_convert_to_tc
? converter($(".timetable_title").eq(0).text())
: $(".timetable_title").eq(0).text()
);
}
},
});
}
var export_button = $(
'<button type="button" class="btn btn-default" id="exportICS" data-type="list"><span class="bigger-120 glyphicon glyphicon-calendar"> ' +
(scindex <= tcindex ? "导出iCS" : "匯出iCS") +
"</span></button>"
);
export_button.click(export_ics);
$("#tb").prepend(export_button);
})();