import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import type { Plugin } from '../../core/types'; import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { waitForElm } from '@/seqta/utils/waitForElm'; const timetablePlugin: Plugin<{}, {}> = { id: 'timetable', name: 'Timetable Enhancer', description: 'Adds extra features to the timetable view', version: '1.0.0', settings: {}, disableToggle: true, run: async (api) => { const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable) return () => { // Call the unregister function to remove the mount listener unregister(); const timetablePage = document.querySelector('.timetablepage') if (timetablePage) { const zoomControls = document.querySelector('.timetable-zoom-controls') if (zoomControls) zoomControls.remove() const hideControls = document.querySelector('.timetable-hide-controls') if (hideControls) hideControls.remove() resetTimetableStyles() } } } }; // Store event handlers globally for cleanup const zoomHandlers = new WeakMap void; zoomOut: () => void }>() function resetTimetableStyles(): void { const firstDayColumn = document.querySelector(".dailycal .content .days td") as HTMLElement if (!firstDayColumn) return const baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight const dayColumns = document.querySelectorAll(".dailycal .content .days td") dayColumns.forEach((td: Element) => { (td as HTMLElement).style.height = `${baseContainerHeight}px` }) const timeColumn = document.querySelector(".times") if (timeColumn) { const times = timeColumn.querySelectorAll(".time") const timeHeight = baseContainerHeight / times.length times.forEach((time: Element) => { (time as HTMLElement).style.height = `${timeHeight}px` }) } const lessons = document.querySelectorAll(".dailycal .lesson") lessons.forEach((lesson: Element) => { const lessonEl = lesson as HTMLElement const originalHeight = lessonEl.getAttribute('data-original-height') if (originalHeight) { lessonEl.style.height = `${originalHeight}px` } }) const entries = document.querySelectorAll(".entry") entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement entryEl.style.opacity = '1' }) const zoomControls = document.querySelector('.timetable-zoom-controls') if (zoomControls) { const handlers = zoomHandlers.get(zoomControls) if (handlers) { const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)') const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)') if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn) if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut) zoomHandlers.delete(zoomControls) } } } async function handleTimetable(): Promise { await waitForElm(".time", true, 10) // Store original heights when timetable loads const lessons = document.querySelectorAll(".dailycal .lesson") lessons.forEach((lesson: Element) => { const lessonEl = lesson as HTMLElement lessonEl.setAttribute( "data-original-height", lessonEl.offsetHeight.toString(), ) }) // Existing time format code if (settingsState.timeFormat == "12") { const times = document.querySelectorAll(".timetablepage .times .time") for (const time of times) { if (!time.textContent) continue time.textContent = convertTo12HourFormat(time.textContent, true) } } handleTimetableZoom() handleTimetableAssessmentHide() } function handleTimetableZoom(): void { console.log("Initializing timetable zoom controls") // Lazy initialize state variables only when function is first called let timetableZoomLevel = 1 let baseContainerHeight: number | null = null const originalEntryPositions = new Map< Element, { topRatio: number; heightRatio: number } >() // Create zoom controls const zoomControls = document.createElement("div") zoomControls.className = "timetable-zoom-controls" const zoomIn = document.createElement("button") zoomIn.className = "uiButton timetable-zoom iconFamily" zoomIn.innerHTML = "" // Unicode for zoom in icon (custom iconfamily) const zoomOut = document.createElement("button") zoomOut.className = "uiButton timetable-zoom iconFamily" zoomOut.innerHTML = "" // Unicode for zoom out icon (custom iconfamily) zoomControls.appendChild(zoomOut) zoomControls.appendChild(zoomIn) const toolbar = document.getElementById("toolbar") toolbar?.appendChild(zoomControls) // Store event listener references const zoomInHandler = () => { if (timetableZoomLevel < 2) { timetableZoomLevel += 0.2 updateZoom() } } const zoomOutHandler = () => { if (timetableZoomLevel > 0.6) { timetableZoomLevel -= 0.2 updateZoom() } } zoomIn.addEventListener("click", zoomInHandler) zoomOut.addEventListener("click", zoomOutHandler) // Store references for cleanup zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler }) const initializePositions = () => { // Get the base container height from the first TD const firstDayColumn = document.querySelector( ".dailycal .content .days td", ) as HTMLElement if (!firstDayColumn) return false baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight // Store original ratios const entries = document.querySelectorAll(".entriesWrapper .entry") entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement // Calculate ratios relative to detected base height if (baseContainerHeight === null) return const topRatio = parseInt(entryEl.style.top) / baseContainerHeight const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight originalEntryPositions.set(entry, { topRatio, heightRatio }) }) return true } const updateZoom = () => { // Initialize positions if not already done if (baseContainerHeight === null && !initializePositions()) { console.error("Failed to initialize positions") return } console.debug(`Updating zoom level to: ${timetableZoomLevel}`) // Calculate new container height if (baseContainerHeight === null) return const newContainerHeight = baseContainerHeight * timetableZoomLevel // Update all day columns (TDs) const dayColumns = document.querySelectorAll(".dailycal .content .days td") dayColumns.forEach((td: Element) => { (td as HTMLElement).style.height = `${newContainerHeight}px` }) // Update all entries using stored ratios const entries = document.querySelectorAll(".entriesWrapper .entry") entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement const originalRatios = originalEntryPositions.get(entry) if (originalRatios) { // Calculate new positions from original ratios const newTop = originalRatios.topRatio * newContainerHeight const newHeight = originalRatios.heightRatio * newContainerHeight // Apply new values entryEl.style.top = `${Math.round(newTop)}px` entryEl.style.height = `${Math.round(newHeight)}px` } }) // Update time column to match const timeColumn = document.querySelector(".times") if (timeColumn) { const times = timeColumn.querySelectorAll(".time") const timeHeight = newContainerHeight / times.length times.forEach((time: Element) => { (time as HTMLElement).style.height = `${timeHeight}px` }) } entries[Math.round((entries.length - 1) / 2)].scrollIntoView({ behavior: "instant", block: "center", }) } } function handleTimetableAssessmentHide(): void { const hideControls = document.createElement("div") hideControls.className = "timetable-hide-controls" const hideOn = document.createElement("button") hideOn.className = "uiButton timetable-hide iconFamily" hideOn.innerHTML = "👁" hideControls.appendChild(hideOn) const toolbar = document.getElementById("toolbar") toolbar?.appendChild(hideControls) function hideElements(): void { const entries = document.querySelectorAll(".entry") entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement if (!entryEl.classList.contains("assessment")) { entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3" } }) } hideOn.addEventListener("click", hideElements) } export default timetablePlugin;