您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add closing time to daily grid
// ==UserScript== // @name Daily Grid Closing Times // @namespace https://aaiscloud.com/ // @version 2.0.1 // @description Add closing time to daily grid // @author Nischal Tonthanahal // @match https://www.aaiscloud.com/*/calendars/dailygridcalendar.aspx // @match https://www.aaiscloud.com/*/Calendars/DailyGridCalendar.aspx // @icon https://www.aais.com/hubfs/favicon.png // @grant none // @license GPLv3 // ==/UserScript== window.closing_times = {}; window.rooms_closing_at = {}; window.all_rooms_done = false; window.room_count = 0; window.viewSwitchObserver = new MutationObserver(removeExistingVerticalLinesIfAny); window.pageSwitchObserver = new MutationObserver(reload); window.pageScrollObserver = new MutationObserver(updateClosingTimes); /** * A set of room names that should be ignored and marked as "Not ITS". */ window.ignored_room_names = new Set(["GFS LBY", "SGM LBY", "SOS B40", "KAP 160", "KAP 267", "OHE 122", "PED 205", "PED 206"]) /** * Updates the closing times for each room on the daily grid. * IMPORTANT: This function is called every time the content on the page changes eg: Due to a scroll event * * This is necessary since the website always fetches the schedules dynamically from the server as you scroll down the page. * The website also destroys every schedule that is not currently in view and has to re-fetch it again every time you scroll up or down. * * As a consequence, we cannot extract all the closing times at one go, and this function has to be called every time * the calendar view changes and then extract only the currently visible room schedules. * We trigger functions on the changed elements using the MutationObserver() API. * * We accumulate the changes into window.closing_times object and then keep track of whether or not we have completed * extraction by using the boolean flag window.all_rooms_done, which is set when the length of window.closing_times equals * the total number of rooms available on the page as shown by the webpage on the bottom right. * * Once we have extracted closing times of all the rooms we need, we display the download CSV button to export it. */ function updateClosingTimes() { try { // Get the time header elements and extract the time information // Get all elements having the class sch-simple-timeheader i.e. the ones that say 06:00 AM, 07:00 AM ... 10:00 PM let time_header_elements = document.querySelectorAll('.sch-simple-timeheader'); // Create an array of all these elements so that we can use the map() function to process them let time_headers = Array.from(time_header_elements); // Process each time header element using the map() function let times = time_headers.map(header => { // Get the bounding rectangle of the time header element eg: 10:00 AM let bound = header.getBoundingClientRect(); // Find out the end coordinate eg: in the block 10:00 AM this would represent the horizontal line of 11:00 AM let end_coordinate = bound.right; // Find out the middle coordinate eg: in the block 10:00 AM this would represent the horizontal line of 10:30 AM let mid_coordinate = bound.left + (bound.right - bound.left) / 2; // Get the text of the element eg: "10:00 AM" let time_string = header.innerText; // Parse the text using a RegEx to find the Hour, Minute, and whether it is AM or PM let time_parsed = time_string.match(/(\d+):(\d+)\s+(AM|PM)/); let hour = time_parsed[1]; // eg: "10" let minute = time_parsed[2]; // eg: "00" let am_pm = time_parsed[3]; // eg: "AM" let time = { hour, minute, am_pm } // eg: {hour:"10", minute:"00", am_pm:"AM"} // Store the structured `time` from above in `times` array // along with the middle and end coordinates that // represent the half-hour and end-of-hour respectively return { time, mid_coordinate, end_coordinate } }) // If the day view is selected, draw vertical lines at the closing times // This is purely visual to help distinguish the event boundaries and has no other purpose if (document.querySelector("#button-1032").classList.contains('x-btn-pressed')) { drawVerticalLines(times); } else { removeExistingVerticalLinesIfAny(); } // Get the room names and update the closing time for each room let room_name_elements = document.querySelectorAll('[data-columnid="RoomDayGridId_BuildingRoomNumberRoomName"]') let room_names = Array.from(room_name_elements); room_names.forEach(room => { let index = room.parentElement.parentElement.parentElement.getAttribute('data-recordindex') if (room.querySelector(`[data-recordindex="${index}"].closing-tag`) != null) { return false; } let room_name = room.innerText; room_name = room_name.replace(/^\s+/, ''); room_name = room_name.replace(/\s+$/, ''); let room_schedule_row = document.querySelector(`table[data-boundview="RoomDayGridId-timelineview"][data-recordindex="${index}"]`) let all_events = room_schedule_row.querySelectorAll('.sch-event'); let last_event; if (all_events.length) { last_event = all_events[all_events.length - 1]; } else { last_event = null; } let closing_time_element = document.createElement("span"); if (window.ignored_room_names.has(room_name)) { closing_time_element.innerHTML = `<div data-recordindex="${index}" class="closing-tag" style="display:inline-block; color: lightgray; background-color:white; font-weight:bold; margin-right: 2px; font-family: 'sans serif'; width:60px; text-align:center">Not ITS</span></div>`; room.querySelector('div').insertBefore(closing_time_element, room.querySelector('span')); if (window.closing_times[room_name] != null) { return; } window.closing_times[room_name] = 'Ignored'; return; } if (last_event == null) { closing_time_element.innerHTML = `<div data-recordindex="${index}" class="closing-tag" style="display:inline-block; color: white; background-color:red; font-weight:bold; margin-right: 2px; font-family: 'sans serif'; width:60px; text-align:center">No Class</span></div>`; room.querySelector('div').insertBefore(closing_time_element, room.querySelector('span')); if (window.closing_times[room_name] != null) { return; } window.closing_times[room_name] = 'No Class'; return; } let bound = last_event.getBoundingClientRect(); let end_coordinate = bound.right; let end_time; for (let i = 0; i < times.length; i++) { const cur_hour = times[i]; const next_hour = (i < times.length - 1) ? times[i + 1] : { time: { hour: '11', minute: '00', am_pm: 'PM' } } if (cur_hour.end_coordinate < end_coordinate) { continue; } if (cur_hour.mid_coordinate >= end_coordinate) { end_time = { ...cur_hour.time }; end_time.minute = '30'; break; } end_time = next_hour.time; break; } let end_time_string = `${end_time.hour}:${end_time.minute} ${end_time.am_pm}` closing_time_element.innerHTML = `<div data-recordindex="${index}" class="closing-tag" style="display:inline-block; color: black; background-color:white; font-weight:bold; margin-right: 2px; font-family: 'sans serif'; width:60px; text-align:center">${end_time_string}</span></div>` room.querySelector('div').insertBefore(closing_time_element, room.querySelector('span')); if (window.closing_times[room_name] != null) { return; } window.closing_times[room_name] = end_time_string; if (window.rooms_closing_at[end_time_string] == null) { window.rooms_closing_at[end_time_string] = []; } window.rooms_closing_at[end_time_string].push(room_name); }); } catch (e) { return; } finally { try { let totalRooms = document.querySelector("#tbtext-1062"); window.room_count = parseInt(totalRooms.innerText.match(/Displaying \d+ - \d+ of (\d+)/)[1]); } catch (e) { } if (window.room_count > 0 && !window.all_rooms_done && Object.keys(window.closing_times).length == window.room_count) { window.all_rooms_done = true; printClosingTimes(); createDownloadButton(); } } } /** * Prints the closing times for all rooms to the console and creates a table. */ function printClosingTimes() { console.clear(); let closing_times_array = Object.entries(window.rooms_closing_at); closing_times_array.sort((a, b) => compareTimeStrings(a[0], b[0])); let output = []; for (let pair of closing_times_array) { let time = pair[0]; let rooms = pair[1]; output.push(`${time}: ${rooms.join(', ')}`); } console.log(output.join('\n')); console.table(window.closing_times); } /** * Compares two time strings in the format "HH:MM AM/PM" and returns a negative value if the first time is earlier, a positive value if the first time is later, and 0 if the times are the same. * @param {string} time1 - The first time string. * @param {string} time2 - The second time string. * @returns {number} - The result of the comparison. */ function compareTimeStrings(time1, time2) { let [hour1, minute1, am_pm1] = time1.split(/[:\s]/); let [hour2, minute2, am_pm2] = time2.split(/[:\s]/); hour1 = Number(hour1); hour2 = Number(hour2); minute1 = Number(minute1); minute2 = Number(minute2); if (am_pm1 != am_pm2) { return (am_pm1 == 'AM') ? -1 : 1; } if (hour1 != hour2) { return hour1 - hour2; } return minute1 - minute2; } /** * Converts an object to a CSV string. * @param {object} obj - The object to be converted to CSV. * @returns {string} - The CSV string. */ function convertToCSV(obj) { var pairs = Object.entries(obj); var csv = ''; for (var i = 0; i < pairs.length; i++) { csv += pairs[i].join(',') + (i < pairs.length - 1 ? '\n' : ''); } return csv; } /** * Creates a download button for the closing times CSV file. */ function createDownloadButton() { var csv = convertToCSV(window.closing_times); var blob = new Blob([csv], { type: 'text/csv' }); var url = URL.createObjectURL(blob); var button = document.createElement('a'); button.style = "position: absolute; left: 50%;margin: 8px; padding: 5px; color: black; border-radius: 3px; background: gold; display:inline-block; font-weight:bold; font-family: 'sans serif'; text-align:center"; button.textContent = 'Download CSV'; button.href = url; button.download = 'closing_times.csv'; button.id = "download_csv"; document.body.append(button); } /** * Removes the download button from the page. */ function removeDownloadButton() { try { document.querySelector("#download_csv").remove() } catch { } } /** * Reloads the page and resets the necessary variables. */ function reload() { window.closing_times = {}; window.rooms_closing_at = {}; window.all_rooms_done = false; window.room_count = 0; removeDownloadButton(); window.pageScrollObserver.disconnect(); console.log("Reload"); setTimeout(() => { console.log("Timer done"); window.pageScrollObserver.observe(document.querySelector('.x-grid-scroll-body.x-scroller'), { childList: true, subtree: true }); updateClosingTimes(); }, 4000); } /** * Removes any existing vertical lines on the page. */ function removeExistingVerticalLinesIfAny() { const existingLines = document.querySelectorAll('.vertical-line'); existingLines.forEach(line => line.remove()); } /** * Draws vertical lines on the page to indicate the closing times for each room. * @param {object[]} times - An array of objects containing the time information. */ function drawVerticalLines(times) { removeExistingVerticalLinesIfAny(); const referenceElement = document.querySelector('#RoomDayGridId-normal'); if (!referenceElement) { console.error('Element with ID "RoomDayGridId-normal" not found'); return; } const referenceRect = referenceElement.getBoundingClientRect(); const colors = [ 'lightpink', 'lightyellow', 'lightcoral', 'lightsalmon', 'lightseagreen', 'lightgoldenrodyellow', ]; times.forEach(({ end_coordinate }, index) => { const line = document.createElement('div'); line.classList.add('vertical-line'); line.style.position = 'absolute'; line.style.left = `${end_coordinate}px`; line.style.top = `${referenceRect.top}px`; line.style.height = `${referenceRect.height}px`; line.style.width = '1px'; line.style.backgroundColor = colors[index % colors.length]; line.style.zIndex = '1'; document.body.appendChild(line); }); } // Initialize the necessary variables and set up the event observers window.addEventListener("load", () => { setTimeout(() => { reload() console.log("Timer done"); window.viewSwitchObserver.observe(document.querySelector("#button-1032"), { childList: true, subtree: true }); window.pageSwitchObserver.observe(document.querySelector("#tbtext-1062"), { childList: true, subtree: true }); window.pageScrollObserver.observe(document.querySelector('.x-grid-scroll-body.x-scroller'), { childList: true, subtree: true }); updateClosingTimes(); }, 4000); });