'use strict'; // Cross-browser API namespace. Firefox provides Promise-based `browser.*`; // Chrome MV3 exposes `browser.*` too, but some builds only populate the // `chrome.*` global. Alias to whichever is present. if (typeof browser === 'undefined' || browser == null) { var browser = (typeof chrome !== 'undefined' ? chrome : {}); } function errMsg(err) { return String((err && err.message) || err); } // Build a download URL from PDF bytes. Firefox's background is a real page // (URL.createObjectURL available) and supports blob: URLs in downloads. // Chrome MV3 runs the background as a service worker where URL.createObjectURL // is NOT available, but its downloads API accepts base64 data: URLs. // Firefox rejects data: URLs in browser.downloads.download with // "Access denied for URL data:...", so pick per-browser. function buildDownloadUrl(bytes) { if (typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function') { const blob = new Blob([bytes], { type: 'application/pdf' }); return URL.createObjectURL(blob); } // Fallback: Chrome MV3 service worker. PDFs here are small (~150KB), well // within data-URL length limits. let binary = ''; const chunk = 0x8000; for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); } return 'data:application/pdf;base64,' + btoa(binary); } browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg && msg.type === 'generate-pdf') { generatePdf(msg.data) .then(() => sendResponse({ ok: true })) .catch((err) => sendResponse({ ok: false, error: errMsg(err) })); return true; } }); function formatPeriod(period) { if (!period) return ''; return String(period) .replace(/\b1ST\b/g, '1st') .replace(/\b2ND\b/g, '2nd') .replace(/\b3RD\b/g, '3rd') .replace(/\b(\d+)TH\b/g, '$1th') .toLowerCase() .replace(/^./, (c) => c.toUpperCase()); } async function generatePdf(data) { const bytes = await ISMIStools.buildStudyLoadPdf(data); // Firefox accepts blob: object URLs; Chrome MV3 service worker has no // URL.createObjectURL, so it falls back to a base64 data: URL there. const url = buildDownloadUrl(bytes); const header = data.header || {}; const year = parseInt(data.year, 10); const period = formatPeriod(data.period || header.semester || ''); const compactProgram = ISMIStools.compactProgram(header.program || ''); const schoolYear = Number.isFinite(year) ? `${year}-${year + 1}` : ''; const parts = [ header.studentId || (header.fullName ? header.fullName.replace(/\s+/g, ' ') : ''), compactProgram, schoolYear ? 'S.Y. ' + schoolYear : '', period, 'Study Load' ].filter(Boolean); const filename = parts.join(' - ') + '.pdf'; const fallback = 'Study Load - ' + (header.fullName || 'Student') + '.pdf'; const safeFilename = filename || fallback; try { const id = await browser.downloads.download({ url, filename: safeFilename, saveAs: true }); const onChanged = (delta) => { if (delta.id === id && delta.state && (delta.state.current === 'complete' || delta.state.current === 'interrupted')) { browser.downloads.onChanged.removeListener(onChanged); } }; browser.downloads.onChanged.addListener(onChanged); } catch (err) { throw err; } }