fix(release): URL-encode release asset names #5
@@ -92,9 +92,12 @@ built `ismis-helper-chrome.zip` directly:
|
|||||||
- Year Level mode: `Auto` (recommended), `Use custom value`, or `Profile value
|
- Year Level mode: `Auto` (recommended), `Use custom value`, or `Profile value
|
||||||
only`.
|
only`.
|
||||||
- **Smart Year Level**: for the semester being printed, `Auto` derives the
|
- **Smart Year Level**: for the semester being printed, `Auto` derives the
|
||||||
standing from the View Grades history (exact) or, failing that, from remaining
|
standing from remaining units (Student Prospectus total − Lacking Subjects,
|
||||||
units (Student Prospectus total − Lacking Subjects). Custom value overrides;
|
reflecting units actually passed) or, failing that, from the View Grades
|
||||||
the profile-scraped value is the fallback.
|
history. Custom value overrides; the profile-scraped value is the fallback.
|
||||||
|
- **Live progress indicator**: during any export a status bar below the buttons
|
||||||
|
shows the current step (reading table, fetching course descriptions `i/N`,
|
||||||
|
profile, year level, building file) with a progress percentage.
|
||||||
- **Next Up import**: the Next Up export writes CSV in the exact column contract
|
- **Next Up import**: the Next Up export writes CSV in the exact column contract
|
||||||
the Raycast [Next Up](https://github.com/) extension accepts
|
the Raycast [Next Up](https://github.com/) extension accepts
|
||||||
(`title, courseCode, days, startTime, endTime, room, units, professorName`),
|
(`title, courseCode, days, startTime, endTime, room, units, professorName`),
|
||||||
|
|||||||
+103
-6
@@ -94,9 +94,10 @@
|
|||||||
return m ? m[1] : '';
|
return m ? m[1] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrapeStudyLoad() {
|
async function scrapeStudyLoad(onProgress) {
|
||||||
const table = document.querySelector('table.table-forum');
|
const table = document.querySelector('table.table-forum');
|
||||||
if (!table) throw new Error('No study load table found on this page.');
|
if (!table) throw new Error('No study load table found on this page.');
|
||||||
|
if (onProgress) onProgress('Reading study load table', 5);
|
||||||
const courses = ISMIStools.parseStudyLoadTable(table.outerHTML);
|
const courses = ISMIStools.parseStudyLoadTable(table.outerHTML);
|
||||||
if (!courses.length) throw new Error('No courses found in study load table.');
|
if (!courses.length) throw new Error('No courses found in study load table.');
|
||||||
|
|
||||||
@@ -117,16 +118,21 @@
|
|||||||
|
|
||||||
const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))];
|
const uniqueCodes = [...new Set(courses.map((c) => c.courseCode))];
|
||||||
const descMap = {};
|
const descMap = {};
|
||||||
for (const code of uniqueCodes) {
|
if (onProgress && uniqueCodes.length) onProgress('Fetching course descriptions (0/' + uniqueCodes.length + ')', 10);
|
||||||
|
for (let i = 0; i < uniqueCodes.length; i++) {
|
||||||
|
const code = uniqueCodes[i];
|
||||||
try {
|
try {
|
||||||
descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token);
|
descMap[code] = await fetchCourseDescription(code, academicPeriod, academicYear, token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
descMap[code] = '';
|
descMap[code] = '';
|
||||||
}
|
}
|
||||||
|
if (onProgress) onProgress('Fetching course descriptions (' + (i + 1) + '/' + uniqueCodes.length + ')',
|
||||||
|
10 + 60 * ((i + 1) / uniqueCodes.length));
|
||||||
}
|
}
|
||||||
courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; });
|
courses.forEach((c) => { c.description = descMap[c.courseCode] || ''; });
|
||||||
|
|
||||||
let header = {};
|
let header = {};
|
||||||
|
if (onProgress) onProgress('Reading profile', 75);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/Manage/Student', { credentials: 'same-origin' });
|
const resp = await fetch('/Manage/Student', { credentials: 'same-origin' });
|
||||||
if (!resp.ok) throw new Error('Profile request failed: ' + resp.status);
|
if (!resp.ok) throw new Error('Profile request failed: ' + resp.status);
|
||||||
@@ -144,7 +150,7 @@
|
|||||||
// Resolve the Year Level for the PRINTED semester (hybrid, see design
|
// Resolve the Year Level for the PRINTED semester (hybrid, see design
|
||||||
// spec): custom setting wins; else View Grades history matching; else the
|
// spec): custom setting wins; else View Grades history matching; else the
|
||||||
// units math; else the profile-scraped value.
|
// units math; else the profile-scraped value.
|
||||||
header.yearLevel = await resolveYearLevel(header, selectedPeriod, selectedYear);
|
header.yearLevel = await resolveYearLevel(header, selectedPeriod, selectedYear, onProgress);
|
||||||
|
|
||||||
return { header, courses, period: selectedPeriod, year: selectedYear };
|
return { header, courses, period: selectedPeriod, year: selectedYear };
|
||||||
}
|
}
|
||||||
@@ -163,7 +169,7 @@
|
|||||||
// Hybrid year-level resolution. Returns null only if every source failed.
|
// Hybrid year-level resolution. Returns null only if every source failed.
|
||||||
// mode: 'auto' (default) runs the full hybrid; 'custom' uses only the custom
|
// mode: 'auto' (default) runs the full hybrid; 'custom' uses only the custom
|
||||||
// value; 'off' uses only the profile-scraped value.
|
// value; 'off' uses only the profile-scraped value.
|
||||||
async function resolveYearLevel(header, period, year) {
|
async function resolveYearLevel(header, period, year, onProgress) {
|
||||||
const settings = await browser.storage.local.get(['yearMode', 'customYearLevel']);
|
const settings = await browser.storage.local.get(['yearMode', 'customYearLevel']);
|
||||||
const mode = settings.yearMode || 'auto';
|
const mode = settings.yearMode || 'auto';
|
||||||
const custom = String(settings.customYearLevel || '').trim();
|
const custom = String(settings.customYearLevel || '').trim();
|
||||||
@@ -183,10 +189,12 @@
|
|||||||
// total prospectus units minus still-lacking units. This reflects real
|
// total prospectus units minus still-lacking units. This reflects real
|
||||||
// earned units and is correct even for students who repeated or failed
|
// earned units and is correct even for students who repeated or failed
|
||||||
// a year (the naive grade-history count would over-advance them).
|
// a year (the naive grade-history count would over-advance them).
|
||||||
|
if (onProgress) onProgress('Determining year level', 80);
|
||||||
const [prospectusHtml, lackingHtml] = await Promise.all([
|
const [prospectusHtml, lackingHtml] = await Promise.all([
|
||||||
fetchText('/prospectus/StudentProspectus'),
|
fetchText('/prospectus/StudentProspectus'),
|
||||||
fetchText('/Extras/ViewLackingSubjectsStudent1')
|
fetchText('/Extras/ViewLackingSubjectsStudent1')
|
||||||
]);
|
]);
|
||||||
|
if (onProgress) onProgress('Checking remaining units', 88);
|
||||||
if (prospectusHtml) {
|
if (prospectusHtml) {
|
||||||
const yearUnits = ISMIStools.parseProspectusYearUnits(prospectusHtml);
|
const yearUnits = ISMIStools.parseProspectusYearUnits(prospectusHtml);
|
||||||
const lacking = lackingHtml ? ISMIStools.parseLackingSubjectsUnits(lackingHtml) : 0;
|
const lacking = lackingHtml ? ISMIStools.parseLackingSubjectsUnits(lackingHtml) : 0;
|
||||||
@@ -197,6 +205,7 @@
|
|||||||
// 2. GRADE HISTORY — fallback only when the units path is unavailable.
|
// 2. GRADE HISTORY — fallback only when the units path is unavailable.
|
||||||
// It counts distinct academic years, so it is only an approximation for
|
// It counts distinct academic years, so it is only an approximation for
|
||||||
// the printed semester and is not used when units data is present.
|
// the printed semester and is not used when units data is present.
|
||||||
|
if (onProgress) onProgress('Checking grade history', 93);
|
||||||
const gradesHtml = await fetchText('/ViewGrades');
|
const gradesHtml = await fetchText('/ViewGrades');
|
||||||
if (gradesHtml) {
|
if (gradesHtml) {
|
||||||
const history = ISMIStools.parseGradeHistory(gradesHtml);
|
const history = ISMIStools.parseGradeHistory(gradesHtml);
|
||||||
@@ -231,26 +240,34 @@
|
|||||||
|
|
||||||
async function onDownloadPdf() {
|
async function onDownloadPdf() {
|
||||||
const btn = document.getElementById(BTN_ID);
|
const btn = document.getElementById(BTN_ID);
|
||||||
|
setExportButtonsDisabled(true);
|
||||||
try {
|
try {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Downloading...';
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Downloading...';
|
||||||
const { header, courses } = await scrapeStudyLoad();
|
setProgress('Reading study load', 2);
|
||||||
|
const { header, courses } = await scrapeStudyLoad((l, p) => setProgress(l, p));
|
||||||
const included = ISMIStools.filterStudyLoadCourses(courses);
|
const included = ISMIStools.filterStudyLoadCourses(courses);
|
||||||
if (!included.length) throw new Error('No enrolled courses found.');
|
if (!included.length) throw new Error('No enrolled courses found.');
|
||||||
const totalUnits = ISMIStools.sumUnits(included);
|
const totalUnits = ISMIStools.sumUnits(included);
|
||||||
const settings = await browser.storage.local.get(['truncateDescription', 'showYearLevel']);
|
const settings = await browser.storage.local.get(['truncateDescription', 'showYearLevel']);
|
||||||
header.showYearLevel = settings.showYearLevel !== false;
|
header.showYearLevel = settings.showYearLevel !== false;
|
||||||
|
setProgress('Building PDF', 95);
|
||||||
const res = await browser.runtime.sendMessage({
|
const res = await browser.runtime.sendMessage({
|
||||||
type: 'generate-pdf',
|
type: 'generate-pdf',
|
||||||
data: { header, courses: included, totalUnits, truncateDescription: settings.truncateDescription !== false }
|
data: { header, courses: included, totalUnits, truncateDescription: settings.truncateDescription !== false }
|
||||||
});
|
});
|
||||||
if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed');
|
if (!res || !res.ok) throw new Error((res && res.error) || 'PDF generation failed');
|
||||||
|
setProgress('Done ✓', 100);
|
||||||
showToast('PDF downloaded.');
|
showToast('PDF downloaded.');
|
||||||
|
setTimeout(clearProgress, 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setProgress('Error: ' + ((err && err.message) || err), 100);
|
||||||
showToast('Error: ' + ((err && err.message) || err), true);
|
showToast('Error: ' + ((err && err.message) || err), true);
|
||||||
} finally {
|
} finally {
|
||||||
|
setExportButtonsDisabled(false);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fa fa-download"></i> Download PDF';
|
btn.innerHTML = '<i class="fa fa-download"></i> Download PDF';
|
||||||
|
setTimeout(clearProgress, 4000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,20 +275,29 @@
|
|||||||
// every format gets the same header/courses/totalUnits payload and the same
|
// every format gets the same header/courses/totalUnits payload and the same
|
||||||
// hybrid year-level resolution.
|
// hybrid year-level resolution.
|
||||||
async function onExport(format) {
|
async function onExport(format) {
|
||||||
|
setExportButtonsDisabled(true);
|
||||||
try {
|
try {
|
||||||
const { header, courses, period, year } = await scrapeStudyLoad();
|
setProgress('Reading ' + format.toUpperCase() + ' data', 2);
|
||||||
|
const { header, courses, period, year } = await scrapeStudyLoad((l, p) => setProgress(l, p));
|
||||||
const included = ISMIStools.filterStudyLoadCourses(courses);
|
const included = ISMIStools.filterStudyLoadCourses(courses);
|
||||||
if (!included.length) throw new Error('No enrolled courses found.');
|
if (!included.length) throw new Error('No enrolled courses found.');
|
||||||
const totalUnits = ISMIStools.sumUnits(included);
|
const totalUnits = ISMIStools.sumUnits(included);
|
||||||
|
setProgress('Building ' + format.toUpperCase() + ' file', 95);
|
||||||
const res = await browser.runtime.sendMessage({
|
const res = await browser.runtime.sendMessage({
|
||||||
type: 'export',
|
type: 'export',
|
||||||
format,
|
format,
|
||||||
data: { header, courses: included, totalUnits, period, year }
|
data: { header, courses: included, totalUnits, period, year }
|
||||||
});
|
});
|
||||||
if (!res || !res.ok) throw new Error((res && res.error) || 'Export failed');
|
if (!res || !res.ok) throw new Error((res && res.error) || 'Export failed');
|
||||||
|
setProgress('Done ✓', 100);
|
||||||
showToast('Exported ' + format.toUpperCase() + '.');
|
showToast('Exported ' + format.toUpperCase() + '.');
|
||||||
|
setTimeout(clearProgress, 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setProgress('Error: ' + ((err && err.message) || err), 100);
|
||||||
showToast('Error: ' + ((err && err.message) || err), true);
|
showToast('Error: ' + ((err && err.message) || err), true);
|
||||||
|
setTimeout(clearProgress, 4000);
|
||||||
|
} finally {
|
||||||
|
setExportButtonsDisabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +312,76 @@
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable/enable all export buttons (incl. PDF) to prevent double-triggers.
|
||||||
|
function setExportButtonsDisabled(disabled) {
|
||||||
|
const ids = [BTN_ID, 'ismisHelperExportCsvBtn', 'ismisHelperExportXlsxBtn',
|
||||||
|
'ismisHelperExportJsonBtn', 'ismisHelperExportNextUpBtn'];
|
||||||
|
for (const id of ids) {
|
||||||
|
const b = document.getElementById(id);
|
||||||
|
if (b) b.disabled = disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Export progress indicator ----
|
||||||
|
const PROGRESS_ID = 'ismisHelperProgress';
|
||||||
|
const PROGRESS_FILL_ID = 'ismisHelperProgressFill';
|
||||||
|
const PROGRESS_LABEL_ID = 'ismisHelperProgressLabel';
|
||||||
|
let progressEl = null;
|
||||||
|
|
||||||
|
// Set the status bar label + width (0-100). With no percent, shows just text.
|
||||||
|
function setProgress(label, percent) {
|
||||||
|
if (!progressEl) progressEl = document.getElementById(PROGRESS_ID);
|
||||||
|
if (!progressEl) return;
|
||||||
|
progressEl.style.display = 'flex';
|
||||||
|
const fill = document.getElementById(PROGRESS_FILL_ID);
|
||||||
|
if (fill) fill.style.width = Math.max(0, Math.min(100, percent)) + '%';
|
||||||
|
const lab = document.getElementById(PROGRESS_LABEL_ID);
|
||||||
|
if (lab) lab.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearProgress() {
|
||||||
|
if (!progressEl) progressEl = document.getElementById(PROGRESS_ID);
|
||||||
|
if (!progressEl) return;
|
||||||
|
progressEl.style.display = 'none';
|
||||||
|
const fill = document.getElementById(PROGRESS_FILL_ID);
|
||||||
|
if (fill) fill.style.width = '0%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeProgressEl() {
|
||||||
|
if (document.getElementById(PROGRESS_ID)) return;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.id = PROGRESS_ID;
|
||||||
|
el.style.display = 'none';
|
||||||
|
el.style.alignItems = 'center';
|
||||||
|
el.style.gap = '10px';
|
||||||
|
el.style.margin = '8px 0';
|
||||||
|
el.style.fontSize = '12px';
|
||||||
|
el.style.color = '#555';
|
||||||
|
el.style.width = '100%';
|
||||||
|
|
||||||
|
const track = document.createElement('div');
|
||||||
|
track.style.flex = '1';
|
||||||
|
track.style.height = '8px';
|
||||||
|
track.style.background = '#e2e2e2';
|
||||||
|
track.style.borderRadius = '4px';
|
||||||
|
track.style.overflow = 'hidden';
|
||||||
|
const fill = document.createElement('div');
|
||||||
|
fill.id = PROGRESS_FILL_ID;
|
||||||
|
fill.style.width = '0%';
|
||||||
|
fill.style.height = '100%';
|
||||||
|
fill.style.background = '#28a745';
|
||||||
|
fill.style.transition = 'width .2s ease';
|
||||||
|
track.appendChild(fill);
|
||||||
|
|
||||||
|
const lab = document.createElement('span');
|
||||||
|
lab.id = PROGRESS_LABEL_ID;
|
||||||
|
lab.style.whiteSpace = 'nowrap';
|
||||||
|
|
||||||
|
el.appendChild(track);
|
||||||
|
el.appendChild(lab);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function injectDownloadButton() {
|
function injectDownloadButton() {
|
||||||
if (document.getElementById(BTN_ID)) return;
|
if (document.getElementById(BTN_ID)) return;
|
||||||
const unitsP = findTotalUnitsP();
|
const unitsP = findTotalUnitsP();
|
||||||
@@ -320,6 +416,7 @@
|
|||||||
|
|
||||||
unitsP.parentNode.insertBefore(wrapper, unitsP);
|
unitsP.parentNode.insertBefore(wrapper, unitsP);
|
||||||
wrapper.appendChild(btnGroup);
|
wrapper.appendChild(btnGroup);
|
||||||
|
wrapper.appendChild(makeProgressEl());
|
||||||
wrapper.appendChild(unitsP);
|
wrapper.appendChild(unitsP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user