(function () { 'use strict'; // Cross-browser API namespace (see background.js). No-op in Firefox. if (typeof browser === 'undefined' || browser == null) { var browser = (typeof chrome !== 'undefined' ? chrome : {}); } const MASTER_KEY = 'enabled'; async function isEnabled() { const { enabled } = await browser.storage.local.get(MASTER_KEY); return enabled !== false; } function autoAcceptPrivacy() { const modal = document.getElementById('myModal'); if (!modal) return; const checkbox = document.getElementById('Confirmation'); const submit = document.getElementById('Submit'); if (checkbox && !checkbox.checked) checkbox.click(); if (submit) submit.click(); } function watchPrivacyModal() { autoAcceptPrivacy(); const observer = new MutationObserver(() => autoAcceptPrivacy()); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), 30000); } async function autoLogin() { const { profiles } = await browser.storage.local.get('profiles'); const active = (profiles || []).find((p) => p.enabled); if (!active) return; const userField = document.querySelector( 'input[name="Username"], input[id="Username"], input[name="UserName"], input[name="username"], input[id="UserName"], input[id="username"]' ); const passField = document.querySelector('input[type="password"]'); if (!userField || !passField) return; userField.value = active.username; passField.value = active.password; userField.dispatchEvent(new Event('input', { bubbles: true })); passField.dispatchEvent(new Event('input', { bubbles: true })); const form = userField.closest('form') || passField.closest('form'); if (!form) return; setTimeout(() => { const submitBtn = form.querySelector('input[type="submit"], button[type="submit"]'); if (submitBtn) submitBtn.click(); else form.submit(); }, 300); } async function fetchCourseDescription(courseCode, academicPeriod, academicYear, token) { const body = new URLSearchParams(); body.set('Courses', courseCode); body.set('AcademicPeriod', academicPeriod); body.set('AcademicYear', academicYear); body.set('AjaxFormVM.Controller', 'CourseSchedule'); body.set('AjaxFormVM.Action', 'CourseScheduleOffered'); body.set('AjaxFormVM.ResultTargetId', 'CourseScheduleOfferedList'); body.set('AjaxFormVM.LinkToReturn', ''); body.set('AjaxFormVM.LinkToReturnTargetId', ''); body.set('AjaxFormVM.FormHeader', 'Choose term to view courses offered'); body.set('AjaxFormVM.FormHeaderIcon', ''); body.set('AjaxFormVM.FormId', 'CourseScheduleCourseScheduleOffered'); body.set('AjaxFormVM.ShowCloseButton', 'True'); body.set('AjaxFormVM.PreventAutoUpdateTarget', 'True'); body.set('AjaxFormVM.CloseModalAfterSubmit', 'False'); body.set('AjaxFormVM.AjaxFormType', 'Search'); body.set('AjaxFormVM.FormNoteLocation', ''); body.set('AjaxFormVM.SubmitButton.FaIcon', ''); body.set('AjaxFormVM.SubmitButton.ButtonText', ''); body.set('AjaxFormVM.SubmitButton.ButtonSize', ''); body.set('__RequestVerificationToken', token); const resp = await fetch('/CourseSchedule/CourseScheduleOffered?Length=14', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, body: body.toString(), credentials: 'same-origin' }); if (!resp.ok) throw new Error('Offered courses request failed: ' + resp.status); const html = await resp.text(); const courses = ISMIStools.parseOfferedCourses(html); const match = courses.find((c) => c.courseCode === courseCode); return match ? match.description : ''; } function extractTotalUnits(text) { const m = /Total Units:\s*([\d.]+)/i.exec(text); return m ? m[1] : ''; } async function scrapeStudyLoad(onProgress) { const table = document.querySelector('table.table-forum'); if (!table) throw new Error('No study load table found on this page.'); if (onProgress) onProgress('Reading study load table', 5); const courses = ISMIStools.parseStudyLoadTable(table.outerHTML); if (!courses.length) throw new Error('No courses found in study load table.'); const periodEl = document.getElementById('AcademicPeriod'); const yearEl = document.getElementById('AcademicYear'); const tokenEl = document.querySelector('input[name="__RequestVerificationToken"]'); const academicPeriod = periodEl ? periodEl.value : ''; const academicYear = yearEl ? yearEl.value : ''; const token = tokenEl ? tokenEl.value : ''; // Semester/year come from the search dropdowns the user picked. // AcademicPeriod's selected option label (e.g. "1ST Semester") + year. const periodLabel = (periodEl && periodEl.selectedOptions && periodEl.selectedOptions[0]) ? periodEl.selectedOptions[0].textContent.trim() : ''; const selectedPeriod = periodLabel || academicPeriod; const selectedYear = academicYear; const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))]; const descMap = {}; if (onProgress && uniqueCodes.length) onProgress('Fetching course descriptions (0/' + uniqueCodes.length + ')', 10); for (let i = 0; i < uniqueCodes.length; i++) { const code = uniqueCodes[i]; try { descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token); } catch (e) { descMap[code] = ''; } if (onProgress) onProgress('Fetching course descriptions (' + (i + 1) + '/' + uniqueCodes.length + ')', 10 + 60 * ((i + 1) / uniqueCodes.length)); } courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; }); let header = {}; if (onProgress) onProgress('Reading profile', 75); try { const resp = await fetch('/Manage/Student', { credentials: 'same-origin' }); if (!resp.ok) throw new Error('Profile request failed: ' + resp.status); const html = await resp.text(); header = ISMIStools.parseProfilePage(html); } catch (e) { header = {}; } // Semester goes into the header so the PDF shows the dropdown's term/year. header.semester = (selectedPeriod && selectedYear) ? selectedPeriod + ' ' + selectedYear : (header.semester || ''); // Resolve the Year Level for the PRINTED semester (hybrid, see design // spec): custom setting wins; else View Grades history matching; else the // units math; else the profile-scraped value. header.yearLevel = await resolveYearLevel(header, selectedPeriod, selectedYear, onProgress); return { header, courses, period: selectedPeriod, year: selectedYear }; } // Fetch a same-origin page and return its text, or null on failure. async function fetchText(path) { try { const resp = await fetch(path, { credentials: 'same-origin' }); if (!resp.ok) return null; return await resp.text(); } catch (e) { return null; } } // Hybrid year-level resolution. Returns null only if every source failed. // mode: 'auto' (default) runs the full hybrid; 'custom' uses only the custom // value; 'off' uses only the profile-scraped value. async function resolveYearLevel(header, period, year, onProgress) { const settings = await browser.storage.local.get(['yearMode', 'customYearLevel']); const mode = settings.yearMode || 'auto'; const custom = String(settings.customYearLevel || '').trim(); if (mode === 'off') { return header.yearLevel ? String(header.yearLevel) : null; } if (mode === 'custom') { return custom ? custom : null; } // auto: custom value wins, then the hybrid sources. if (custom) return custom; const yearNum = parseInt(year, 10); // 1. UNITS MATH — primary. Standing from units actually passed: // total prospectus units minus still-lacking units. This reflects real // earned units and is correct even for students who repeated or failed // a year (the naive grade-history count would over-advance them). if (onProgress) onProgress('Determining year level', 80); const [prospectusHtml, lackingHtml] = await Promise.all([ fetchText('/prospectus/StudentProspectus'), fetchText('/Extras/ViewLackingSubjectsStudent1') ]); if (onProgress) onProgress('Checking remaining units', 88); if (prospectusHtml) { const yearUnits = ISMIStools.parseProspectusYearUnits(prospectusHtml); const lacking = lackingHtml ? ISMIStools.parseLackingSubjectsUnits(lackingHtml) : 0; const level = ISMIStools.computeYearStandingFromUnits(yearUnits, lacking); if (level != null) return String(level); } // 2. GRADE HISTORY — fallback only when the units path is unavailable. // It counts distinct academic years, so it is only an approximation for // the printed semester and is not used when units data is present. if (onProgress) onProgress('Checking grade history', 93); const gradesHtml = await fetchText('/ViewGrades'); if (gradesHtml) { const history = ISMIStools.parseGradeHistory(gradesHtml); const level = ISMIStools.yearLevelForSemester(history, yearNum); if (level != null) return String(level); } // 3. Profile-scraped value as a last resort. if (header.yearLevel) return String(header.yearLevel); return null; } // ---- On-page Download PDF button ---- const BTN_ID = 'ismisHelperDownloadBtn'; function showToast(msg, isError) { if (window.toastr) { if (isError) window.toastr.error(msg); else window.toastr.success(msg); } else if (isError) { alert(msg); } } function findTotalUnitsP() { const ps = document.querySelectorAll('#StudyLoad p'); for (const p of ps) { if (/Total Units/i.test(p.textContent || '')) return p; } return null; } async function onDownloadPdf() { const btn = document.getElementById(BTN_ID); setExportButtonsDisabled(true); try { btn.disabled = true; btn.innerHTML = ' Downloading...'; setProgress('Reading study load', 2); const { header, courses } = await scrapeStudyLoad((l, p) => setProgress(l, p)); const included = ISMIStools.filterStudyLoadCourses(courses); if (!included.length) throw new Error('No enrolled courses found.'); const totalUnits = ISMIStools.sumUnits(included); const settings = await browser.storage.local.get(['truncateDescription', 'showYearLevel']); header.showYearLevel = settings.showYearLevel !== false; setProgress('Building PDF', 95); const res = await browser.runtime.sendMessage({ type: 'generate-pdf', data: { header, courses: included, totalUnits, truncateDescription: settings.truncateDescription !== false } }); if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed'); setProgress('Done ✓', 100); showToast('PDF downloaded.'); setTimeout(clearProgress, 3000); } catch (err) { setProgress('Error: ' + ((err && err.message) || err), 100); showToast('Error: ' + ((err && err.message) || err), true); } finally { setExportButtonsDisabled(false); btn.disabled = false; btn.innerHTML = ' Download PDF'; setTimeout(clearProgress, 4000); } } // Export another format (csv/json/xlsx/nextup). Reuses scrapeStudyLoad so // every format gets the same header/courses/totalUnits payload and the same // hybrid year-level resolution. async function onExport(format) { setExportButtonsDisabled(true); try { setProgress('Reading ' + format.toUpperCase() + ' data', 2); const { header, courses, period, year } = await scrapeStudyLoad((l, p) => setProgress(l, p)); const included = ISMIStools.filterStudyLoadCourses(courses); if (!included.length) throw new Error('No enrolled courses found.'); const totalUnits = ISMIStools.sumUnits(included); setProgress('Building ' + format.toUpperCase() + ' file', 95); const res = await browser.runtime.sendMessage({ type: 'export', format, data: { header, courses: included, totalUnits, period, year } }); if (!res || !res.ok) throw new Error((res && res.error) || 'Export failed'); setProgress('Done ✓', 100); showToast('Exported ' + format.toUpperCase() + '.'); setTimeout(clearProgress, 3000); } catch (err) { setProgress('Error: ' + ((err && err.message) || err), 100); showToast('Error: ' + ((err && err.message) || err), true); setTimeout(clearProgress, 4000); } finally { setExportButtonsDisabled(false); } } function makeExportButton(id, label, icon, format) { const b = document.createElement('button'); b.id = id; b.type = 'button'; b.className = 'btn green'; b.style.marginLeft = '6px'; b.innerHTML = ' ' + label; b.addEventListener('click', () => onExport(format)); return b; } // Disable/enable all export buttons (incl. PDF) to prevent double-triggers. function setExportButtonsDisabled(disabled) { const ids = [BTN_ID, 'ismisHelperExportCsvBtn', 'ismisHelperExportXlsxBtn', 'ismisHelperExportJsonBtn', 'ismisHelperExportNextUpBtn']; for (const id of ids) { const b = document.getElementById(id); if (b) b.disabled = disabled; } } // ---- Export progress indicator ---- const PROGRESS_ID = 'ismisHelperProgress'; const PROGRESS_FILL_ID = 'ismisHelperProgressFill'; const PROGRESS_LABEL_ID = 'ismisHelperProgressLabel'; let progressEl = null; // Set the status bar label + width (0-100). With no percent, shows just text. function setProgress(label, percent) { if (!progressEl) progressEl = document.getElementById(PROGRESS_ID); if (!progressEl) return; progressEl.style.display = 'flex'; const fill = document.getElementById(PROGRESS_FILL_ID); if (fill) fill.style.width = Math.max(0, Math.min(100, percent)) + '%'; const lab = document.getElementById(PROGRESS_LABEL_ID); if (lab) lab.textContent = label; } function clearProgress() { if (!progressEl) progressEl = document.getElementById(PROGRESS_ID); if (!progressEl) return; progressEl.style.display = 'none'; const fill = document.getElementById(PROGRESS_FILL_ID); if (fill) fill.style.width = '0%'; } function makeProgressEl() { if (document.getElementById(PROGRESS_ID)) return; const el = document.createElement('div'); el.id = PROGRESS_ID; el.style.display = 'none'; el.style.alignItems = 'center'; el.style.gap = '10px'; el.style.margin = '8px 0'; el.style.fontSize = '12px'; el.style.color = '#555'; el.style.width = '100%'; const track = document.createElement('div'); track.style.flex = '1'; track.style.height = '8px'; track.style.background = '#e2e2e2'; track.style.borderRadius = '4px'; track.style.overflow = 'hidden'; const fill = document.createElement('div'); fill.id = PROGRESS_FILL_ID; fill.style.width = '0%'; fill.style.height = '100%'; fill.style.background = '#28a745'; fill.style.transition = 'width .2s ease'; track.appendChild(fill); const lab = document.createElement('span'); lab.id = PROGRESS_LABEL_ID; lab.style.whiteSpace = 'nowrap'; el.appendChild(track); el.appendChild(lab); return el; } function injectDownloadButton() { if (document.getElementById(BTN_ID)) return; const unitsP = findTotalUnitsP(); if (!unitsP) return; const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.alignItems = 'center'; wrapper.style.justifyContent = 'space-between'; wrapper.style.margin = '10px 0'; wrapper.style.flexWrap = 'wrap'; const btn = document.createElement('button'); btn.id = BTN_ID; btn.type = 'button'; btn.className = 'btn green'; btn.innerHTML = ' Download PDF'; btn.addEventListener('click', onDownloadPdf); const btnCsv = makeExportButton('ismisHelperExportCsvBtn', 'CSV', 'fa fa-file-text-o', 'csv'); const btnXlsx = makeExportButton('ismisHelperExportXlsxBtn', 'Excel', 'fa fa-file-o', 'xlsx'); const btnJson = makeExportButton('ismisHelperExportJsonBtn', 'JSON', 'fa fa-file-code-o', 'json'); const btnNextUp = makeExportButton('ismisHelperExportNextUpBtn', 'Next Up', 'fa fa-calendar', 'nextup'); const btnGroup = document.createElement('div'); btnGroup.style.display = 'flex'; btnGroup.appendChild(btn); btnGroup.appendChild(btnCsv); btnGroup.appendChild(btnXlsx); btnGroup.appendChild(btnJson); btnGroup.appendChild(btnNextUp); unitsP.parentNode.insertBefore(wrapper, unitsP); wrapper.appendChild(btnGroup); wrapper.appendChild(makeProgressEl()); wrapper.appendChild(unitsP); } function tryInjectButton() { if (document.getElementById(BTN_ID)) return; if (document.querySelector('#StudyLoad table.table-forum')) injectDownloadButton(); } function watchStudyLoad() { tryInjectButton(); const observer = new MutationObserver((mutations) => { const inStudyLoad = mutations.some( (m) => m.target && m.target.closest && m.target.closest('#StudyLoad') ); if (inStudyLoad) tryInjectButton(); }); observer.observe(document.body, { childList: true, subtree: true }); } (async function init() { if (!(await isEnabled())) return; watchPrivacyModal(); if (/\/Account\/Login/i.test(location.href)) autoLogin(); if (document.getElementById('StudyLoad')) watchStudyLoad(); })(); })();