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.
This commit is contained in:
2026-08-20 06:45:21 +08:00
parent 89fd587c21
commit 53d5ca6d04
19 changed files with 5231 additions and 76 deletions
+5 -1
View File
@@ -25,10 +25,11 @@ $('#masterToggle').addEventListener('change', async (e) => {
// ---- PDF settings: truncate long course descriptions (default ON) ----
async function loadPdfSettings() {
const s = await browser.storage.local.get(['truncateDescription', 'showYearLevel', 'customYearLevel']);
const s = await browser.storage.local.get(['truncateDescription', 'showYearLevel', 'customYearLevel', 'yearMode']);
$('#truncateToggle').checked = s.truncateDescription !== false;
$('#showYearToggle').checked = s.showYearLevel !== false;
$('#customYear').value = s.customYearLevel || '';
$('#yearMode').value = s.yearMode || 'auto';
}
$('#truncateToggle').addEventListener('change', async (e) => {
await browser.storage.local.set({ truncateDescription: e.target.checked });
@@ -36,6 +37,9 @@ $('#truncateToggle').addEventListener('change', async (e) => {
$('#showYearToggle').addEventListener('change', async (e) => {
await browser.storage.local.set({ showYearLevel: e.target.checked });
});
$('#yearMode').addEventListener('change', async (e) => {
await browser.storage.local.set({ yearMode: e.target.value });
});
$('#customYear').addEventListener('change', async (e) => {
await browser.storage.local.set({ customYearLevel: e.target.value.trim() });
});