Files
ismis-helper/background.js
T
adrianbonpin 53d5ca6d04 feat(v1.4.0): CSV/Excel/JSON/NextUp exports + hybrid smart year standing
- Export modules (export-csv/json/xlsx) mirroring the PDF layout, plus a
  Next Up import CSV matching the raycast/nextup column contract.
- Hybrid Year Level: View Grades history (exact per-semester standing),
  falling back to units math (prospectus total - lacking), then profile.
  Popup adds a year-mode setting (auto/custom/off).
- On-page button row now offers PDF / CSV / Excel / JSON / Next Up.
- Bump version to 1.4.0.
- Tag-only build workflow with an optional AMO sign step for a signed .xpi;
  separate ci.yml keeps tests on pushes/PRs.
- Tests: year-standing parser tests + export tests (CSV quoting, JSON schema,
  XLSX structure); CI validates XLSX via openpyxl.
2026-08-20 06:45:21 +08:00

122 lines
4.7 KiB
JavaScript

'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);
}
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg && msg.type === 'export') {
runExport(msg.format, msg.data)
.then(() => sendResponse({ ok: true }))
.catch((err) => sendResponse({ ok: false, error: errMsg(err) }));
return true;
}
if (msg && msg.type === 'generate-pdf') {
generatePdf(msg.data)
.then(() => sendResponse({ ok: true }))
.catch((err) => sendResponse({ ok: false, error: errMsg(err) }));
return true;
}
});
// Generic downloader used by every export format. `filename` is used with
// saveAs so the user picks the location, mirroring the PDF flow.
async function downloadBytes(bytes, mime, filename, fallback) {
const safeFilename = filename || fallback;
const blob = new Blob([bytes], { type: mime });
const url = (typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function')
? URL.createObjectURL(blob)
: null;
let downloadUrl = url;
if (!downloadUrl) {
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
downloadUrl = 'data:' + mime + ';base64,' + btoa(binary);
}
try {
const id = await browser.downloads.download({ url: downloadUrl, 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) {
if (url) { try { URL.revokeObjectURL(url); } catch (e) {} }
throw err;
}
}
async function runExport(format, data) {
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 core = [
header.studentId || (header.fullName ? header.fullName.replace(/\s+/g, ' ') : ''),
compactProgram,
schoolYear ? 'S.Y. ' + schoolYear : '',
period
].filter(Boolean);
const base = core.join(' - ') || 'Study Load';
if (format === 'csv') {
const text = ISMIStools.buildStudyLoadCsv(data);
const bytes = new TextEncoder().encode(text);
await downloadBytes(bytes, 'text/csv', base + '.csv', 'Study Load.csv');
} else if (format === 'json') {
const text = JSON.stringify(ISMIStools.buildStudyLoadJson(data), null, 2);
const bytes = new TextEncoder().encode(text);
await downloadBytes(bytes, 'application/json', base + '.json', 'Study Load.json');
} else if (format === 'xlsx') {
const bytes = ISMIStools.buildStudyLoadXlsx(data);
await downloadBytes(bytes, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', base + '.xlsx', 'Study Load.xlsx');
} else if (format === 'nextup') {
const text = ISMIStools.buildNextUpCsv(data);
const bytes = new TextEncoder().encode(text);
await downloadBytes(bytes, 'text/csv', base + '-nextup.csv', 'Study Load - nextup.csv');
} else {
throw new Error('Unknown export format: ' + format);
}
}
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);
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';
await downloadBytes(bytes, 'application/pdf', filename, fallback);
}