feat(v1.4.0): exports + smart year level + progress + encrypted passwords
ci / test (push) Successful in 43s
build-extension / build (push) Successful in 6m7s

v1.4.0: CSV/Excel/JSON/NextUp exports, units-first smart year standing, live export progress bar, AES-GCM at-rest password encryption.
This commit was merged in pull request #4.
This commit is contained in:
2026-08-20 04:39:08 +00:00
parent 89fd587c21
commit 0cb70ecee0
19 changed files with 5442 additions and 91 deletions
+41 -12
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() });
});
@@ -96,27 +100,52 @@ function openProfileForm(p) {
editingProfileId = p ? p.id : null;
$('#pName').value = p ? p.name : '';
$('#pUsername').value = p ? p.username : '';
$('#pPassword').value = p ? p.password : '';
// The stored password is encrypted and not revealed; leave the field blank.
// On save, a blank password preserves the existing one.
$('#pPassword').value = '';
$('#pEnabled').checked = p ? !!p.enabled : true;
$('#profileForm').hidden = false;
if (p) $('#pPassword').placeholder = 'Leave blank to keep existing password';
else $('#pPassword').placeholder = 'Password';
}
$('#addProfile').addEventListener('click', () => openProfileForm(null));
$('#cancelProfile').addEventListener('click', () => { $('#profileForm').hidden = true; });
$('#saveProfile').addEventListener('click', async () => {
const profiles = await getProfiles();
const data = {
id: editingProfileId || Date.now().toString(),
name: $('#pName').value.trim(),
username: $('#pUsername').value.trim(),
password: $('#pPassword').value,
enabled: $('#pEnabled').checked
};
if (!data.name || !data.username) return;
const name = $('#pName').value.trim();
const username = $('#pUsername').value.trim();
const typedPassword = $('#pPassword').value;
const enabled = $('#pEnabled').checked;
if (!name || !username) return;
// Encrypt a newly-typed password via the background.
let passwordEnc = null;
if (typedPassword) {
const res = await browser.runtime.sendMessage({ type: 'encrypt-password', password: typedPassword });
if (!res || !res.ok) { alert('Could not encrypt password: ' + ((res && res.error) || 'unknown error')); return; }
passwordEnc = res.cipher;
}
if (editingProfileId) {
const i = profiles.findIndex((x) => x.id === editingProfileId);
if (i >= 0) profiles[i] = data;
if (i >= 0) {
profiles[i].name = name;
profiles[i].username = username;
profiles[i].enabled = enabled;
// Only replace the password if a new one was typed; otherwise keep the
// existing (already encrypted) one.
if (passwordEnc) profiles[i].passwordEnc = passwordEnc;
delete profiles[i].password; // legacy plaintext no longer needed
}
} else {
profiles.push(data);
const profile = {
id: Date.now().toString(),
name,
username,
enabled,
passwordEnc
};
profiles.push(profile);
}
await saveProfiles(profiles);
$('#profileForm').hidden = true;