From d65bfa8c467ec8823082137e0ed018e359e1b20d Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Tue, 11 Feb 2025 21:40:57 +1100 Subject: [PATCH] feat: add zoom scaling to timetable page #202 --- src/SEQTA.ts | 129 ++++++++++++++++++++++++++++++++++++++++-- src/css/injected.scss | 10 ++++ 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 5a41151e..027e6d87 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -753,6 +753,117 @@ async function LoadPageElements(): Promise { await handleSublink(sublink); } +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(); + + // 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 = ''; // Using unicode for zoom in icon + + const zoomOut = document.createElement('button'); + zoomOut.className = 'uiButton timetable-zoom iconFamily'; + zoomOut.innerHTML = ''; // Using unicode for zoom out icon + + + zoomControls.appendChild(zoomIn); + zoomControls.appendChild(zoomOut); + + const toolbar = document.getElementById('toolbar'); + toolbar?.appendChild(zoomControls); + + 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`; + }); + } + }; + + zoomIn.addEventListener('click', () => { + if (timetableZoomLevel < 2) { + timetableZoomLevel += 0.2; + updateZoom(); + } + }); + + zoomOut.addEventListener('click', () => { + if (timetableZoomLevel > 0.6) { + timetableZoomLevel -= 0.2; + updateZoom(); + } + }); +} async function handleNotices(node: Element): Promise { if (!(node instanceof HTMLElement)) return; @@ -804,15 +915,25 @@ async function handleSublink(sublink: string | undefined): Promise { } async function handleTimetable(): Promise { - await waitForElm('.time', true, 10) + 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') + const times = document.querySelectorAll('.timetablepage .times .time'); for (const time of times) { - if (!time.textContent) continue - time.textContent = convertTo12HourFormat(time.textContent, true) + if (!time.textContent) continue; + time.textContent = convertTo12HourFormat(time.textContent, true); } } + + handleTimetableZoom(); } async function handleNewsPage(): Promise { diff --git a/src/css/injected.scss b/src/css/injected.scss index 8e4377d4..27f6e8df 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -11,10 +11,16 @@ --auto-background: var(--better-pale, var(--background-secondary)) !important; font-family: Rubik, sans-serif !important; } + .hidden { display: none; } +button.uiButton.timetable-zoom.iconFamily, +.iconFamily { + font-family: "IconFamily" !important; +} + body, .legacy-root input, .legacy-root textarea, @@ -229,6 +235,10 @@ html { } } +.timetable-zoom { + font-size: 14px !important; +} + #main > .dashboard { grid-template-columns: repeat(autofit, minmax(200px, 400px)) !important; background: unset;