(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() { const table = document.querySelector('table.table-forum'); if (!table) throw new Error('No study load table found on this page.'); 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 = {}; for (const code of uniqueCodes) { try { descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token); } catch (e) { descMap[code] = ''; } } courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; }); let header = {}; 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); 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) { 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). const [prospectusHtml, lackingHtml] = await Promise.all([ fetchText('/prospectus/StudentProspectus'), fetchText('/Extras/ViewLackingSubjectsStudent1') ]); 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. 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); try { btn.disabled = true; btn.innerHTML = ' Downloading...'; const { header, courses } = await scrapeStudyLoad(); 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; 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'); showToast('PDF downloaded.'); } catch (err) { showToast('Error: ' + ((err && err.message) || err), true); } finally { btn.disabled = false; btn.innerHTML = ' Download PDF'; } } // 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) { try { const { header, courses, period, year } = await scrapeStudyLoad(); const included = ISMIStools.filterStudyLoadCourses(courses); if (!included.length) throw new Error('No enrolled courses found.'); const totalUnits = ISMIStools.sumUnits(included); 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'); showToast('Exported ' + format.toUpperCase() + '.'); } catch (err) { showToast('Error: ' + ((err && err.message) || err), true); } } 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; } 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(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(); })(); })();