// ==UserScript==
// @name SJTU Grad student classtable to iCalendar
// @name:zh-CN SJTU研究生课表导出到iCalendar
// @namespace http://tampermonkey.net/
// @version 0.2.0
// @description Export the SJTU graduate school schedule to an iCalendar file. Initial support for iOS calendar locations is currently available.
// @description:zh-CN 将研究生课表导出到iCalendar文件。目前初步支持iOS日历位置。
// @author Victrid
// @match http://yjs.sjtu.edu.cn/gsapp/sys/wdkbapp*
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @run-at document-end
// @license GPL-3.0
// @grant GM_xmlhttpRequest
// @connect plus.sjtu.edu.cn
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// ==/UserScript==
/*jshint esversion: 8 */
(function () {
'use strict';
// This part is modified from https://github.com/nwcell/ics.js, licensed under MIT.
// MIT License
// Copyright (c) 2018 Travis Krause
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// -------------------------------ICS.js start-----------------------------------
const ics = function (prodId, uidDomain) {
var SEPARATOR = '\n';
var calendarEvents = [];
var calendarStart = [
'BEGIN:VCALENDAR',
'PRODID:' + prodId,
'VERSION:2.0'
].join(SEPARATOR);
var calendarEnd = SEPARATOR + 'END:VCALENDAR';
function GetTimeDate(date_object) {
// Return the UTC ICS compatible TIME-DATE of a Date object. UTC part here is critical
const yy = ("0000" + (date_object.getUTCFullYear().toString())).slice(-4);
const MM = ("00" + ((date_object.getUTCMonth() + 1).toString())).slice(-2);
const dd = ("00" + ((date_object.getUTCDate()).toString())).slice(-2);
const hh = ("00" + (date_object.getUTCHours().toString())).slice(-2);
const mm = ("00" + (date_object.getUTCMinutes().toString())).slice(-2);
const ss = ("00" + (date_object.getUTCSeconds().toString())).slice(-2);
return `${yy}${MM}${dd}T${hh}${mm}${ss}Z`;
}
// DTSTAMP
const now = GetTimeDate(new Date());
const geolocation = {
// ios map related private annotations
"东上院": {
handle: `CAES8AIIl8QDEK/e9djnpP4BGhIJnrRwWYUFP0ARTaCIRQxc
XkAiogEKBuS4reWbvRICQ04yCeS4iua1t+W4gkIJ6Ze16KGM5Yy6Uj7kuJzlt53ot684MDDl
j7fkuIrmtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLrlhoUo6L+R5Lic5LiL6ZmiKWI+5Lic
5bed6LevODAw5Y+35LiK5rW35Lqk6YCa5aSn5a2m6Ze16KGM5qCh5Yy65YaFKOi/keS4nOS4
i+mZoikqJ+S4iua1t+S6pOmAmuWkp+WtpumXteihjOagoeWMuuS4nOS4iumZojJW5Lit5Zu9
5LiK5rW35biC6Ze16KGM5Yy65Lic5bed6LevODAw5Y+35LiK5rW35Lqk6YCa5aSn5a2m6Ze1
6KGM5qCh5Yy65YaFKOi/keS4nOS4i+mZoik4L1ABWiMKIQiv3vXY56T+ARISCZ60cFmFBT9A
EU2giEUMXF5AGJfEAw==`.replace(/\s/g, ""),
radius: "95.46661055183932",
geo: "31.021566,121.438249"
},
"东中院": {
handle: `CAESvQIIl8QDEKuT1MbnpP4BGhIJpkboZ+oFP0ARO/4LBAFcXkAigAE
KBuS4reWbvRICQ04yCeS4iua1t+W4gkIJ6Ze16KGM5Yy6Ui3kuJzlt53ot684MDDlj7fkuIr
mtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLpiLeS4nOW3nei3rzgwMOWPt+S4iua1t+S6pOm
AmuWkp+WtpumXteihjOagoeWMuion5LiK5rW35Lqk6YCa5aSn5a2m6Ze16KGM5qCh5Yy65Li
c5Lit6ZmiMkXkuK3lm73kuIrmtbfluILpl7XooYzljLrkuJzlt53ot684MDDlj7fkuIrmtbf
kuqTpgJrlpKflrabpl7XooYzmoKHljLo4L1ABWiMKIQirk9TG56T+ARISCaZG6GfqBT9AETv
+CwQBXF5AGJfEAw==`.replace(/\s/g, ""),
radius: "116.1443983019585",
geo: "31.023108,121.437562"
},
"东下院": {
handle: `CAESzAIIl8QDEOvX+8HnpP4BGhIJjx1U4joGP0ARFokJavhbXkAi
igEKBuS4reWbvRICQ04yCeS4iua1t+W4gkIJ6Ze16KGM5Yy6UjLkuJzlt53ot684MDDlj7fk
uqTpgJrlpKflrablhoUo6L+R5Lqk5aSn5Zu+5Lmm6aaGKWIy5Lic5bed6LevODAw5Y+35Lqk
6YCa5aSn5a2m5YaFKOi/keS6pOWkp+WbvuS5pummhikqJ+S4iua1t+S6pOmAmuWkp+WtpumX
teihjOagoeWMuuS4nOS4i+mZojJK5Lit5Zu95LiK5rW35biC6Ze16KGM5Yy65Lic5bed6Lev
ODAw5Y+35Lqk6YCa5aSn5a2m5YaFKOi/keS6pOWkp+WbvuS5pummhik4L1ABWiMKIQjr1/vB
56T+ARISCY8dVOI6Bj9AERaJCWr4W15AGJfEAw==`.replace(/\s/g, ""),
radius: "86.83563332157695",
geo: "31.024336,121.437"
},
"上院": {
handle: `CAESwwIIl8QDEJfTiujYgf4BGhIJokW28/0EP0ARDHVY4ZZbXkAihg
EKBuS4reWbvRICQ04yCeS4iua1t+W4gkIJ6Ze16KGM5Yy6UjDkuJzlt53ot684MDDlj7fkuI
rmtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLrlhoViMOS4nOW3nei3rzgwMOWPt+S4iua1t+
S6pOmAmuWkp+WtpumXteihjOagoeWMuuWGhSok5LiK5rW35Lqk6YCa5aSn5a2m6Ze16KGM5q
Ch5Yy65LiK6ZmiMkjkuK3lm73kuIrmtbfluILpl7XooYzljLrkuJzlt53ot684MDDlj7fkuI
rmtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLrlhoU4L1ABWiMKIQiX04ro2IH+ARISCaJFtv
P9BD9AEQx1WOGWW15AGJfEAw==`.replace(/\s/g, ""),
radius: "79.62744974845103",
geo: "31.019500,121.431084"
},
"中院": {
handle: `CAESsAIIl8QDELDRiujYgf4BGhIJ8pTVdD0FP0ARgSOBBptbXkAiegoG
5Lit5Zu9EgJDTjIJ5LiK5rW35biCQgnpl7XooYzljLpSKuS4nOW3nei3rzgwMOS4iua1t+S6
pOmAmuWkp+WtpumXteihjOagoeWMumIq5Lic5bed6LevODAw5LiK5rW35Lqk6YCa5aSn5a2m
6Ze16KGM5qCh5Yy6KiTkuIrmtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLrkuK3pmaIyQuS4
reWbveS4iua1t+W4gumXteihjOWMuuS4nOW3nei3rzgwMOS4iua1t+S6pOmAmuWkp+WtpumX
teihjOagoeWMujgvUAFaIwohCLDRiujYgf4BEhIJ8pTVdD0FP0ARgSOBBptbXkAYl8QD`.replace(/\s/g, ""),
radius: "389.6329066623545",
geo: "31.020469,121.431337"
},
"下院": {
handle: `CAESsAIIl8QDEJrTiujYgf4BGhIJ7rPKTGkFP0ARdxGmKJdbXkAiegoG
5Lit5Zu9EgJDTjIJ5LiK5rW35biCQgnpl7XooYzljLpSKuS4nOW3nei3rzgwMOS4iua1t+S6
pOmAmuWkp+WtpumXteihjOagoeWMumIq5Lic5bed6LevODAw5LiK5rW35Lqk6YCa5aSn5a2m
6Ze16KGM5qCh5Yy6KiTkuIrmtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLrkuIvpmaIyQuS4
reWbveS4iua1t+W4gumXteihjOWMuuS4nOW3nei3rzgwMOS4iua1t+S6pOmAmuWkp+WtpumX
teihjOagoeWMujgvUAFaIwohCJrTiujYgf4BEhIJ7rPKTGkFP0ARdxGmKJdbXkAYl8QD`.replace(/\s/g, ""),
radius: "371.4202884635783",
geo: "31.021138,121.431101"
},
"陈瑞球楼": {
handle: `CAEStgIIl8QDEOCelMTnpP4BGhIJ6e3PRUMGP0ARjnVxGw1cXkAiegoG5Lit5Zu9E
gJDTjIJ5LiK5rW35biCQgnpl7XooYzljLpSKuS4iua1t+S6pOmAmuWkp+WtpumXteihjOago
eWMuumZiOeRnueQg+alvGIq5LiK5rW35Lqk6YCa5aSn5a2m6Ze16KGM5qCh5Yy66ZmI55Ge5
5CD5qW8KirkuIrmtbfkuqTpgJrlpKflrabpl7XooYzmoKHljLrpmYjnkZ7nkIPmpbwyQuS4r
eWbveS4iua1t+W4gumXteihjOWMuuS4iua1t+S6pOmAmuWkp+WtpumXteihjOagoeWMuumZi
OeRnueQg+alvDgvUAFaIwohCOCelMTnpP4BEhIJ6e3PRUMGP0ARjnVxGw1cXkAYl8QD`.replace(/\s/g, ""),
radius: "371.4202884635783",
geo: "31.021138,121.431101"
},
};
function FormatXAppleStructuredLocation(building, room) {
function fxa(str, len) {
const size = Math.ceil(str.length / len);
const r = Array(size);
let offset = 0;
for (let i = 0; i < size; i++) {
r[i] = str.substr(offset, len);
offset += len;
}
return r;
}
var t;
if (building in geolocation) {
t = `-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="闵行校区${building}";
X-APPLE-MAPKIT-HANDLE=${geolocation[building].handle};X-APPLE-RADIUS=${geolocation[building].radius};
X-APPLE-REFERENCEFRAME=2;X-TITLE="${room}":geo:${geolocation[building].geo}`.replace(/\s/g, "");
} else {
// default to 陈瑞球楼
t = `-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS="闵行校区${building}";
X-APPLE-MAPKIT-HANDLE=${geolocation["陈瑞球楼"].handle};X-APPLE-RADIUS=${geolocation["陈瑞球楼"].radius};
X-APPLE-REFERENCEFRAME=2;X-TITLE="${room}":geo:${geolocation["陈瑞球楼"].geo}`.replace(/\s/g, "");
}
// icloud exported calendar have them.
var chunk = fxa(t, 72);
chunk[0] = "X" + chunk[0];
for (var i = 1; i < chunk.length; i++) {
chunk[i] = " " + chunk[i];
}
return chunk.join(SEPARATOR);
}
function FormatGEO(building, room) {
var geo;
if (building in geolocation) {
geo = geolocation[building].geo.split(",")
} else {
geo = geolocation["陈瑞球楼"].geo.split(",")
}
return `GEO:${geo[0]};${geo[1]}`
}
return {
'addPeriods': function (subject, description, building, room, location, start_date, period_list, ios) {
// Utilize RDATE periods. Best option if your calendar supports this.
// Note: Apple calendar cannot understand RDATE.
if (ios) {
console.error("iOS calendar does not support RDATE");
return;
}
const start = GetTimeDate(new Date(start_date.getTime() + period_list[0].start));
const end = GetTimeDate(new Date(start_date.getTime() + period_list[0].end));
var period_strings = [];
for (const period of period_list) {
const start_time = GetTimeDate(new Date(start_date.getTime() + period.start));
const end_time = GetTimeDate(new Date(start_date.getTime() + period.end));
period_strings.push(`${start_time}/${end_time}`);
}
const RDATE_PERIOD = period_strings.join(",");
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,
FormatGEO(building, room),
"RDATE;VALUE=PERIOD:" + RDATE_PERIOD,
'SUMMARY;LANGUAGE=zh-CN:' + subject,
'TRANSP:OPAQUE',
'END:VEVENT'
];
calendarEvent = calendarEvent.join(SEPARATOR);
calendarEvents.push(calendarEvent);
return;
},
'addSingle': function (subject, description, building, room, location, start_date, period, ios) {
// Compatible option
const start = GetTimeDate(new Date(start_date.getTime() + period.start));
const end = GetTimeDate(new Date(start_date.getTime() + period.end));
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 + (ios ? SEPARATOR + FormatXAppleStructuredLocation(building, room) : SEPARATOR + FormatGEO(building, room)),
'SUMMARY;LANGUAGE=zh-CN:' + subject,
'TRANSP:OPAQUE',
'END:VEVENT'
];
calendarEvent = calendarEvent.join(SEPARATOR);
calendarEvents.push(calendarEvent);
return calendarEvent;
},
'download': function (filename) {
if (calendarEvents.length < 1) {
return;
}
var calendar = calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;
var blob = new Blob([calendar]);
saveAs(blob, filename + ".ics");
// clear after saving
calendarEvents = [];
},
};
}(`SJTUGraduateiCalendar ${GM_info.script.version}`, `SGics${GM_info.script.version}`);
// -------------------------------ICS.js end-----------------------------------
function ParseWeek(week_notation) {
var weeks = [];
for (var i = 0; i < week_notation.length; i++) {
weeks.push(week_notation.charAt(i) == "1");
}
return weeks;
}
async function GetStartDay(semester) {
// get the start date from sjtu plus
const sjtu_plus_url = "https://plus.sjtu.edu.cn/course-plus-data/lessonData_index.json";
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: sjtu_plus_url,
headers: { "Content-Type": "application/application/json" },
onload: function (req) {
var result = new Date(0);
if (req.status != 200) {
console.log(req.status);
return new Date(0);
}
const data = JSON.parse(req.responseText);
const semester_start_month = parseInt(semester) % 100;
var semester_year = ((parseInt(semester) - semester_start_month) / 100) | 0;
var year_json, semester_json;
switch (semester_start_month) {
case 8:
case 9:
case 10:
year_json = semester_year.toString() + "-" + (semester_year + 1).toString();
semester_json = 1;
break;
case 1:
case 2:
case 3:
year_json = (semester_year - 1).toString() + "-" + semester_year.toString();
semester_json = 2;
break;
case 5:
case 6:
case 7:
year_json = (semester_year - 1).toString() + "-" + semester_year.toString();
semester_json = 3;
break;
default:
console.log(`Error finding semester: how can a semester starts in ${semester_start_month}?`);
result = new Date(0);
}
for (const item of data) {
if (item.year == year_json && item.semester == semester_json) {
if ("first_day" in item) {
result = new Date(Date.parse(item.first_day + " 00:00:00 GMT+0800"));
resolve(result);
return;
} else {
console.log("Error finding semester: first_day not defined by SJTU-plus, maybe too old or too new?");
result = new Date(0);
resolve(result);
return;
}
}
}
console.log("Error finding semester: semester not defined by SJTU-plus, maybe too old or too new?");
result = new Date(0);
resolve(result);
}
});
});
}
function ExpandSchedInfoAndCombine(schedinfo, start_date) {
var full_timetable = [];
for (const sched of schedinfo) {
for (var week = 0; week < sched.weeks.length; week++) {
if (sched.weeks[week]) {
const si = {
week: week,
day: sched.weekday,
start: sched.timeslot,
end: sched.timeslot + 1,
building: sched.building,
room: sched.room,
// Content shown at iCalendar
locatable_loc: `${sched.room}\\n闵行校区${sched.building}`,
// Unique location stored in course scheduling system
_location_cmp: sched._location_cmp,
// Days after Day 0 (1st week's monday)
_daydelta: week * 7 + sched.weekday,
// Comparable value to check whether two time slot are continuous
_tsstart: (week * 7 + sched.weekday) * 24 + sched.timeslot,
_tsend: (week * 7 + sched.weekday) * 24 + sched.timeslot + 1,
};
full_timetable.push(si);
}
}
}
// Sort the table, make sure continuous slot finds themselves
function cont(si1, si2) {
return si1._tsstart - si2._tsstart;
}
full_timetable.sort(cont);
//condensed timetable groups continuous slot together
var condensed_timetable = [];
var current;
for (const si of full_timetable) {
if (current != undefined && current._tsend == si._tsstart && current._location_cmp == si._location_cmp) {
current._tsend = si._tsend;
current.end = si.end;
} else {
if (current != undefined) {
condensed_timetable.push(current);
}
current = si;
}
}
if (current != undefined) {
condensed_timetable.push(current);
}
// vevent dict groups them by location
var vevent_dict = {};
for (const si of condensed_timetable) {
if (!(si._location_cmp in vevent_dict)) {
vevent_dict[si._location_cmp] = {
locatable_loc: si.locatable_loc,
building: si.building,
room: si.room,
timeslots: []
};
}
const absolute_days = si.week * 7 + si.day;
const timeslot = GetHM(si.start, si.end);
vevent_dict[si._location_cmp].timeslots.push({
start: absolute_days * 24 * 60 * 60 * 1000 + timeslot.start * 60 * 1000,
end: absolute_days * 24 * 60 * 60 * 1000 + timeslot.end * 60 * 1000
});
}
var vevent_list = [];
for (const key in vevent_dict) {
vevent_list.push(vevent_dict[key]);
}
return vevent_list;
}
const timeStart = [
'08:00',
'08:55',
'10:00',
'10:55',
'12:00',
'12:55',
'14:00',
'14:55',
'16:00',
'16:55',
'18:00',
'18:55',
'19:41',
'20:25',
"21:15"
];
const timeEnd = [
'08:45',
"09:40",
"10:45",
"11:40",
"12:45",
"13:40",
"14:45",
"15:40",
"16:45",
"17:40",
"18:45",
"19:40",
"20:20",
"21:10",
"22:00"
];
function GetHM(start, end) {
var start_info = timeStart[start - 1].split(":");
var start_m = parseInt(start_info[0]) * 60 + parseInt(start_info[1]);
var end_info = timeEnd[end - 2].split(":");
var end_m = parseInt(end_info[0]) * 60 + parseInt(end_info[1]);
return {
start: start_m,
end: end_m,
};
}
async function GetCourseInfo() {
// Dirty thingy, use their JS to do
var course_info = {};
const bld_regex = /[-0-9]*$/i;
const semester = $('#myXnxqSelect').val();
return new Promise((resolve, reject) => {
requirejs(["/gsapp/sys/wdkbapp/*default/modules/xskcb/xskcbBS.js"], function (bs) {
bs.getXspkjgList(semester, "").done(function (pkjgList) {
//console.log(pkjgList);
for (const course_raw of pkjgList) {
//console.log(course_raw);
if (course_raw["XNXQDM"] != semester) {
console.info("Received erranous value", course_raw["XNXQDM"], semester);
continue;
}
const schedinfo = {
weeks: ParseWeek(course_raw["ZCBH"]),
weekday: course_raw["XQ"] - 1,
timeslot: course_raw["JSJCDM"],
building: course_raw["JASMC"].replace(bld_regex, ""),
room: course_raw["JASMC"],
_location_cmp: course_raw["JASDM"],
};
if (!(course_raw["BJDM"] in course_info)) {
course_info[course_raw["BJDM"]] = {
name: course_raw["KCMC"],
course_code: course_raw["KCDM"],
class_code: course_raw["BJMC"],
teacher: course_raw["JSXM"],
scheds: []
};
}
course_info[course_raw["BJDM"]]["scheds"].push(schedinfo);
}
for (let idx in course_info) {
let ci = course_info[idx];
let newscheds = ExpandSchedInfoAndCombine(ci.scheds);
ci.scheds = newscheds;
}
resolve(course_info);
});
});
});
}
function RDATE_method(info, start_date, ios) {
for (const key in info) {
const course = info[key];
for (const sched of course.scheds) {
ics.addPeriods(course.name, `${course.course_code} ${course.name}\\n教师:${course.teacher}\\n班级号:${course.class_code}`, sched.building, sched.room, sched.locatable_loc, start_date, sched.timeslots, ios);
}
}
}
function SINGLE_method(info, start_date, ios) {
for (const key in info) {
const course = info[key];
for (const sched of course.scheds) {
for (const time of sched.timeslots) {
ics.addSingle(course.name, `${course.course_code} ${course.name}\\n教师:${course.teacher}\\n班级号:${course.class_code}`, sched.building, sched.room, sched.locatable_loc, start_date, time, ios);
}
}
}
}
async function GenerateIOS() {
// This function generates calendar
const info = await GetCourseInfo();
const semester = $('#myXnxqSelect').val();
const start_date = await GetStartDay(semester);
SINGLE_method(info, start_date, true);
ics.download("iOS");
}
async function GenerateNormal() {
// This function generates calendar
const info = await GetCourseInfo();
const semester = $('#myXnxqSelect').val();
const start_date = await GetStartDay(semester);
SINGLE_method(info, start_date, false);
ics.download("Normal");
}
async function GenerateRDATE() {
// This function generates calendar
const info = await GetCourseInfo();
const semester = $('#myXnxqSelect').val();
const start_date = await GetStartDay(semester);
RDATE_method(info, start_date, false);
ics.download("RDATE");
}
function delayedRegistration() {
$(`<a href="javascript:void(0);" class="bh-btn bh-btn-default" id="genios" title="iOS日历,含苹果特定地图信息。每次课均作为单独事件。">导出含iOS位置日历
</a>
<a href="javascript:void(0);" class="bh-btn bh-btn-default" id="gennormal" title="常规日历,含GPS信息。每次课均作为单独事件。">导出常规日历
</a>
<a href="javascript:void(0);" class="bh-btn bh-btn-default" id="genrdate" title="日历条目设置为重复项,支持关联批量编辑,含GPS信息。大多数日历程序无法正确解析RDATE,请测试后使用。">导出RDATE日历
</a>`).insertAfter($("#xsXx").children().find("a"));
$("#genios").click(GenerateIOS);
$("#gennormal").click(GenerateNormal);
$("#genrdate").click(GenerateRDATE);
}
let retries = 50;
const intervalID = setInterval(_ => {
// TODO: Try to check if the label is loaded, not very effective
const match = ($("#myXnxqSelect").length != 0);
if (match != 0) {
delayedRegistration();
}
retries--;
if (retries == 0 || (match != 0)) clearInterval(intervalID);
}, 100);
})();