mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
+1
-1
@@ -47,7 +47,7 @@
|
|||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"publish-browser-extension": "^3.0.1",
|
"publish-browser-extension": "^4.0.0",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.85.1",
|
||||||
"sass-loader": "^16.0.5",
|
"sass-loader": "^16.0.5",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
|
|||||||
+6
-4
@@ -50,10 +50,12 @@ async function init() {
|
|||||||
documentLoadStyle.textContent = documentLoadCSS;
|
documentLoadStyle.textContent = documentLoadCSS;
|
||||||
document.head.appendChild(documentLoadStyle);
|
document.head.appendChild(documentLoadStyle);
|
||||||
|
|
||||||
const icon = document.querySelector(
|
const icons =
|
||||||
'link[rel*="icon"]',
|
document.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]');
|
||||||
)! as HTMLLinkElement;
|
|
||||||
icon.href = icon48; // Change the icon
|
icons.forEach((link) => {
|
||||||
|
link.href = icon48;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initializeSettingsState();
|
await initializeSettingsState();
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
api.storage.weightings = {};
|
api.storage.weightings = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear any stuck "processing" states so they can retry
|
||||||
|
let hasStuckProcessing = false;
|
||||||
|
for (const key in api.storage.weightings) {
|
||||||
|
if (api.storage.weightings[key] === "processing") {
|
||||||
|
delete api.storage.weightings[key];
|
||||||
|
hasStuckProcessing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStuckProcessing) {
|
||||||
|
api.storage.weightings = { ...api.storage.weightings };
|
||||||
|
}
|
||||||
|
|
||||||
api.seqta.onMount(".assessmentsWrapper", async () => {
|
api.seqta.onMount(".assessmentsWrapper", async () => {
|
||||||
await waitForElm(
|
await waitForElm(
|
||||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
||||||
@@ -125,10 +137,12 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
);
|
);
|
||||||
if (!assessmentsList) return;
|
if (!assessmentsList) return;
|
||||||
|
|
||||||
const gradeElements = document.querySelectorAll(
|
// Get marks from React state to match with DOM elements
|
||||||
"[class*='Thermoscore__text___']",
|
const state = await ReactFiber.find(
|
||||||
);
|
"[class*='AssessmentList__items___']",
|
||||||
if (!gradeElements.length) return;
|
).getState();
|
||||||
|
const marks = state["marks"];
|
||||||
|
if (!marks || !marks.length) return;
|
||||||
|
|
||||||
// Parse and average grades
|
// Parse and average grades
|
||||||
const letterToNumber: Record<string, number> = {
|
const letterToNumber: Record<string, number> = {
|
||||||
@@ -162,19 +176,59 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
return letterToNumber[str] ?? 0;
|
return letterToNumber[str] ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let total = 0;
|
// Get all assessment items (excluding the average we might have added)
|
||||||
|
const assessmentItems = Array.from(
|
||||||
|
assessmentsList.querySelectorAll(`[class*='AssessmentItem__AssessmentItem___']`),
|
||||||
|
).filter(
|
||||||
|
(item) =>
|
||||||
|
!item.querySelector(`[class*='AssessmentItem__title___']`)?.textContent?.includes("Subject Average"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Match marks to assessment items and calculate weighted average
|
||||||
|
let weightedTotal = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
let hasInaccurateWeighting = false;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
gradeElements.forEach((el) => {
|
|
||||||
const grade = parseGrade(el.textContent || "");
|
for (let i = 0; i < marks.length && i < assessmentItems.length; i++) {
|
||||||
if (grade > 0) {
|
const mark = marks[i];
|
||||||
total += grade;
|
const assessmentItem = assessmentItems[i];
|
||||||
count++;
|
const gradeElement = assessmentItem.querySelector(
|
||||||
|
`[class*='Thermoscore__text___']`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gradeElement) continue;
|
||||||
|
|
||||||
|
const grade = parseGrade(gradeElement.textContent || "");
|
||||||
|
if (grade <= 0) continue;
|
||||||
|
|
||||||
|
const assessmentID = String(mark.id);
|
||||||
|
const weighting = api.storage.weightings[assessmentID];
|
||||||
|
|
||||||
|
// Check if weighting is unavailable or still processing
|
||||||
|
if (!weighting || weighting === "N/A" || weighting === "processing") {
|
||||||
|
hasInaccurateWeighting = true;
|
||||||
|
// Fall back to equal weighting if unavailable
|
||||||
|
weightedTotal += grade;
|
||||||
|
totalWeight += 1;
|
||||||
|
} else {
|
||||||
|
const weight = parseFloat(weighting);
|
||||||
|
if (!isNaN(weight) && weight > 0) {
|
||||||
|
weightedTotal += grade * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
} else {
|
||||||
|
// Invalid weight, use equal weighting
|
||||||
|
weightedTotal += grade;
|
||||||
|
totalWeight += 1;
|
||||||
|
hasInaccurateWeighting = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
if (!count) return;
|
if (!count || totalWeight === 0) return;
|
||||||
|
|
||||||
const avg = total / count;
|
const avg = weightedTotal / totalWeight;
|
||||||
const rounded = Math.ceil(avg / 5) * 5;
|
const rounded = Math.ceil(avg / 5) * 5;
|
||||||
const numberToLetter = Object.entries(letterToNumber).reduce(
|
const numberToLetter = Object.entries(letterToNumber).reduce(
|
||||||
(acc, [k, v]) => {
|
(acc, [k, v]) => {
|
||||||
@@ -195,6 +249,16 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
);
|
);
|
||||||
if (existing?.textContent === "Subject Average") return;
|
if (existing?.textContent === "Subject Average") return;
|
||||||
|
|
||||||
|
// Build warning message if needed
|
||||||
|
let warningHTML = "";
|
||||||
|
if (hasInaccurateWeighting) {
|
||||||
|
warningHTML = /* html */ `
|
||||||
|
<div style="margin-top: 4px; font-size: 11px; color: rgba(255, 255, 255, 0.6); opacity: 0.8; line-height: 1.3;">
|
||||||
|
⚠ Some weightings unavailable
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Use the dynamic class names in the HTML template
|
// Use the dynamic class names in the HTML template
|
||||||
const averageElement = stringToHTML(/* html */ `
|
const averageElement = stringToHTML(/* html */ `
|
||||||
<div class="${assessmentItemClass}">
|
<div class="${assessmentItemClass}">
|
||||||
@@ -202,12 +266,13 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
<div class="${metaClass}">
|
<div class="${metaClass}">
|
||||||
<div class="${simpleResultClass}">
|
<div class="${simpleResultClass}">
|
||||||
<div class="${titleClass}">Subject Average</div>
|
<div class="${titleClass}">Subject Average</div>
|
||||||
|
${warningHTML}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="${thermoscoreClass}">
|
<div class="${thermoscoreClass}">
|
||||||
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
|
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
|
||||||
<div class="${textClass}" title="${display}">${display}</div>
|
<div class="${textClass}" title="${hasInaccurateWeighting ? display + ' (some weightings unavailable)' : display}">${display}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,19 +283,308 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function extractPDFText(url: string): Promise<string> {
|
// Detect Firefox (has stricter CSP for blob URLs)
|
||||||
const loadingTask = pdfjs.getDocument(url);
|
// Use userAgent instead of deprecated InstallTrigger
|
||||||
const pdf = await loadingTask.promise;
|
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 &&
|
||||||
|
!navigator.userAgent.toLowerCase().includes('seamonkey') &&
|
||||||
|
!navigator.userAgent.toLowerCase().includes('waterfox');
|
||||||
|
|
||||||
let text = "";
|
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
|
||||||
|
// Detect if URL is a blob URL
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
const isBlobUrl = url.startsWith('blob:');
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const content = await page.getTextContent();
|
// For Firefox, ALWAYS use page context to avoid any CSP issues
|
||||||
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
// For blob URLs in any browser, use page context
|
||||||
|
if (isBlobUrl || isFirefox) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Inject script into page context to fetch (bypasses Firefox CSP restrictions)
|
||||||
|
const script = document.createElement('script');
|
||||||
|
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
// Escape URL for use in script
|
||||||
|
const escapedUrl = url.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
fetch('${escapedUrl}')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||||
|
}
|
||||||
|
return response.arrayBuffer();
|
||||||
|
})
|
||||||
|
.then(arrayBuffer => {
|
||||||
|
window.postMessage({
|
||||||
|
type: '${requestId}',
|
||||||
|
success: true,
|
||||||
|
data: Array.from(new Uint8Array(arrayBuffer))
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.postMessage({
|
||||||
|
type: '${requestId}',
|
||||||
|
success: false,
|
||||||
|
error: error.message || String(error)
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === requestId) {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.success) {
|
||||||
|
// Convert back to ArrayBuffer
|
||||||
|
const uint8Array = new Uint8Array(event.data.data);
|
||||||
|
resolve(uint8Array.buffer);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error || 'Failed to fetch PDF'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
// Timeout after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
reject(new Error('Timeout fetching PDF'));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Regular URL - fetch normally, but check if response URL becomes blob
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
credentials: 'include',
|
||||||
|
redirect: 'follow',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if response URL is a blob URL (server might redirect to blob)
|
||||||
|
if (response.url && response.url.startsWith('blob:')) {
|
||||||
|
// Re-fetch using page context
|
||||||
|
return await fetchPDFAsArrayBuffer(response.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.arrayBuffer();
|
||||||
|
} catch (error: any) {
|
||||||
|
// If error mentions blob or security, try using page context
|
||||||
|
if (error?.message?.includes('blob') || error?.message?.includes('Security') || error?.message?.includes('CSP')) {
|
||||||
|
// Force use page context
|
||||||
|
return await fetchPDFAsArrayBuffer(url);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return text;
|
async function extractPDFText(url: string): Promise<string> {
|
||||||
|
// For Firefox, do everything in page context to avoid blob URL CSP issues
|
||||||
|
if (isFirefox) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
// Escape URL for use in script (handle both single and double quotes)
|
||||||
|
const escapedUrl = url.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
const requestId = '${requestId}';
|
||||||
|
const url = '${escapedUrl}';
|
||||||
|
|
||||||
|
// Check if pdfjs is already loaded
|
||||||
|
if (window.pdfjsLib) {
|
||||||
|
extractPDF();
|
||||||
|
} else {
|
||||||
|
// Load pdfjs in page context
|
||||||
|
const pdfjsScript = document.createElement('script');
|
||||||
|
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.mjs';
|
||||||
|
pdfjsScript.type = 'module';
|
||||||
|
|
||||||
|
pdfjsScript.onload = function() {
|
||||||
|
extractPDF();
|
||||||
|
};
|
||||||
|
pdfjsScript.onerror = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load pdfjs library'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(pdfjsScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPDF() {
|
||||||
|
try {
|
||||||
|
// Disable worker for Firefox to avoid blob URL CSP issues
|
||||||
|
// Set to empty string to disable worker completely
|
||||||
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
|
||||||
|
|
||||||
|
// Use XMLHttpRequest instead of fetch for better blob URL handling
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.responseType = 'arraybuffer';
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = xhr.response;
|
||||||
|
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
|
||||||
|
throw new Error('PDF response is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pdfjsLib.getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
useSystemFonts: true,
|
||||||
|
verbosity: 0,
|
||||||
|
// Explicitly disable worker
|
||||||
|
useWorkerFetch: false,
|
||||||
|
isEvalSupported: false
|
||||||
|
}).promise
|
||||||
|
.then(pdf => {
|
||||||
|
const pagePromises = [];
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
pagePromises.push(
|
||||||
|
pdf.getPage(i).then(page => {
|
||||||
|
return page.getTextContent().then(content => {
|
||||||
|
return content.items.map(item => item.str).join(' ');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(pagePromises);
|
||||||
|
})
|
||||||
|
.then(pages => {
|
||||||
|
const text = pages.join('\\n');
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: true,
|
||||||
|
text: text
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'PDF parsing error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'ArrayBuffer error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Network error fetching PDF'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.ontimeout = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Timeout fetching PDF'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.timeout = 30000;
|
||||||
|
xhr.send();
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Setup error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === requestId) {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.success) {
|
||||||
|
resolve(event.data.text);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error || 'Failed to extract PDF text'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
// Timeout after 60 seconds (PDF parsing can take time)
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
reject(new Error('Timeout extracting PDF text'));
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Chrome - use extension context
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
|
||||||
|
|
||||||
|
if (arrayBuffer.byteLength === 0) {
|
||||||
|
throw new Error('PDF response is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingTask = pdfjs.getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
useSystemFonts: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const content = await page.getTextContent();
|
||||||
|
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWeightings(mark: any, api: any) {
|
async function handleWeightings(mark: any, api: any) {
|
||||||
@@ -238,45 +592,79 @@ async function handleWeightings(mark: any, api: any) {
|
|||||||
const metaclassID = mark.metaclassID;
|
const metaclassID = mark.metaclassID;
|
||||||
const userInfo = await getUserInfo();
|
const userInfo = await getUserInfo();
|
||||||
const userID = userInfo.id;
|
const userID = userInfo.id;
|
||||||
if (api.storage.weightings[assessmentID] != undefined) {
|
|
||||||
|
// Skip if already processed (not "processing")
|
||||||
|
if (api.storage.weightings[assessmentID] != undefined && api.storage.weightings[assessmentID] !== "processing") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set to processing
|
||||||
api.storage.weightings = {
|
api.storage.weightings = {
|
||||||
...api.storage.weightings,
|
...api.storage.weightings,
|
||||||
[assessmentID]: "processing",
|
[assessmentID]: "processing",
|
||||||
};
|
};
|
||||||
|
|
||||||
const filename =
|
try {
|
||||||
"BetterSEQTA-" + String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
const filename =
|
||||||
|
"BetterSEQTA-" + String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
||||||
|
|
||||||
await fetch(`${location.origin}/seqta/student/print/assessment`, {
|
const printResponse = await fetch(`${location.origin}/seqta/student/print/assessment`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||||
body: JSON.stringify({
|
credentials: 'include',
|
||||||
fileName: filename,
|
body: JSON.stringify({
|
||||||
id: assessmentID,
|
fileName: filename,
|
||||||
metaclass: metaclassID,
|
id: assessmentID,
|
||||||
student: userID,
|
metaclass: metaclassID,
|
||||||
}),
|
student: userID,
|
||||||
});
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
if (!printResponse.ok) {
|
||||||
const text = await extractPDFText(pdfUrl);
|
throw new Error(`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Use regex to find the line "Assessment weight: X"
|
// Wait a bit for the PDF to be generated
|
||||||
const match = text.match(/Assessment weight:\s*(\d+\.?\d*)/i);
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
const weight = match ? match[1] : "N/A";
|
|
||||||
|
|
||||||
// Save it to storage
|
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
||||||
api.storage.weightings = {
|
|
||||||
...api.storage.weightings,
|
// Check if URL is a blob URL (which extensions can't access)
|
||||||
[assessmentID]: weight,
|
if (pdfUrl.startsWith('blob:')) {
|
||||||
};
|
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: string;
|
||||||
|
try {
|
||||||
|
// For Firefox, extractPDFText already handles everything in page context
|
||||||
|
// For Chrome, it uses extension context
|
||||||
|
text = await extractPDFText(pdfUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (isFirefox && (error?.message?.includes('blob') || error?.message?.includes('Security') || error?.message?.includes('CSP'))) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
try {
|
||||||
|
text = await extractPDFText(pdfUrl);
|
||||||
|
} catch (retryError: any) {
|
||||||
|
throw new Error(`PDF extraction failed after retry: ${retryError.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`PDF extraction failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Assessment ID ${assessmentID} weight:`, weight);
|
const match = text.match(/Assessment weight:\s*(\d+\.?\d*)/i);
|
||||||
|
const weight = match ? match[1] : "N/A";
|
||||||
|
|
||||||
console.log(text);
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: weight,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: "N/A",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseAssessments(api: any) {
|
async function parseAssessments(api: any) {
|
||||||
|
|||||||
@@ -104,17 +104,7 @@ export async function loadHomePage() {
|
|||||||
const date = new Date();
|
const date = new Date();
|
||||||
const TodayFormatted = formatDate(date);
|
const TodayFormatted = formatDate(date);
|
||||||
|
|
||||||
const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [
|
const [assessmentsPromise, classesPromise, prefsPromise] = [
|
||||||
fetch(`${location.origin}/seqta/student/load/timetable?`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
from: TodayFormatted,
|
|
||||||
until: TodayFormatted,
|
|
||||||
student: 69,
|
|
||||||
}),
|
|
||||||
}).then((res) => res.json()),
|
|
||||||
|
|
||||||
GetUpcomingAssessments(),
|
GetUpcomingAssessments(),
|
||||||
|
|
||||||
GetActiveClasses(),
|
GetActiveClasses(),
|
||||||
@@ -126,65 +116,13 @@ export async function loadHomePage() {
|
|||||||
}).then((res) => res.json()),
|
}).then((res) => res.json()),
|
||||||
];
|
];
|
||||||
|
|
||||||
const [timetableData, assessments, classes, prefs] = await Promise.all([
|
const [assessments, classes, prefs] = await Promise.all([
|
||||||
timetablePromise,
|
|
||||||
assessmentsPromise,
|
assessmentsPromise,
|
||||||
classesPromise,
|
classesPromise,
|
||||||
prefsPromise,
|
prefsPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const dayContainer = document.getElementById("day-container");
|
callHomeTimetable(TodayFormatted, true);
|
||||||
if (dayContainer && timetableData.payload.items.length > 0) {
|
|
||||||
const lessonArray = timetableData.payload.items.sort((a: any, b: any) =>
|
|
||||||
a.from.localeCompare(b.from),
|
|
||||||
);
|
|
||||||
const colours = await GetLessonColours();
|
|
||||||
|
|
||||||
dayContainer.innerHTML = "";
|
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
|
||||||
const lesson = lessonArray[i];
|
|
||||||
const subjectname = `timetable.subject.colour.${lesson.code}`;
|
|
||||||
const subject = colours.find(
|
|
||||||
(element: any) => element.name === subjectname,
|
|
||||||
);
|
|
||||||
|
|
||||||
lesson.colour = subject
|
|
||||||
? `--item-colour: ${subject.value};`
|
|
||||||
: "--item-colour: #8e8e8e;";
|
|
||||||
lesson.from = lesson.from.substring(0, 5);
|
|
||||||
lesson.until = lesson.until.substring(0, 5);
|
|
||||||
|
|
||||||
if (settingsState.timeFormat === "12") {
|
|
||||||
lesson.from = convertTo12HourFormat(lesson.from);
|
|
||||||
lesson.until = convertTo12HourFormat(lesson.until);
|
|
||||||
}
|
|
||||||
|
|
||||||
lesson.attendanceTitle = CheckUnmarkedAttendance(lesson.attendance);
|
|
||||||
|
|
||||||
const div = makeLessonDiv(lesson, i + 1);
|
|
||||||
if (GetThresholdOfColor(subject?.value) > 300) {
|
|
||||||
const firstChild = div.firstChild as HTMLElement;
|
|
||||||
if (firstChild) {
|
|
||||||
firstChild.classList.add("day-inverted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dayContainer.appendChild(div.firstChild!);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSelectedDate.getDate() === date.getDate()) {
|
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
|
||||||
CheckCurrentLesson(lessonArray[i], i + 1);
|
|
||||||
}
|
|
||||||
CheckCurrentLessonAll(lessonArray);
|
|
||||||
}
|
|
||||||
} else if (dayContainer) {
|
|
||||||
dayContainer.innerHTML = `
|
|
||||||
<div class="day-empty">
|
|
||||||
<img src="${browser.runtime.getURL(LogoLight)}" />
|
|
||||||
<p>No lessons available.</p>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
dayContainer?.classList.remove("loading");
|
|
||||||
|
|
||||||
const activeClass = classes.find((c: any) => c.hasOwnProperty("active"));
|
const activeClass = classes.find((c: any) => c.hasOwnProperty("active"));
|
||||||
const activeSubjects = activeClass?.subjects || [];
|
const activeSubjects = activeClass?.subjects || [];
|
||||||
@@ -742,7 +680,8 @@ function callHomeTimetable(date: string, change?: any) {
|
|||||||
GetLessonColours().then((colours) => {
|
GetLessonColours().then((colours) => {
|
||||||
let subjects = colours;
|
let subjects = colours;
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
for (let i = 0; i < lessonArray.length; i++) {
|
||||||
let subjectname = `timetable.subject.colour.${lessonArray[i].code}`;
|
|
||||||
|
let subjectname = ((lessonArray[i].type == "tutorial") ? `timetable.tutor.${lessonArray[i].tutorID}` : `timetable.subject.colour.${lessonArray[i].code}`);
|
||||||
|
|
||||||
let subject = subjects.find(
|
let subject = subjects.find(
|
||||||
(element: any) => element.name === subjectname,
|
(element: any) => element.name === subjectname,
|
||||||
@@ -930,33 +869,35 @@ function makeLessonDiv(lesson: any, num: number) {
|
|||||||
programmeID,
|
programmeID,
|
||||||
metaID,
|
metaID,
|
||||||
assessments,
|
assessments,
|
||||||
|
type
|
||||||
} = lesson;
|
} = lesson;
|
||||||
|
|
||||||
let lessonString = `
|
let lessonString = `
|
||||||
<div class="day" id="${code + num}" style="${colour}">
|
<div class="day" id="${code + num}" style="${colour}">
|
||||||
<h2>${description || "Unknown"}</h2>
|
<h2>${(type == "class") ? description : (type == "tutorial") ? "Tutorial" : "Unknown"}</h2>
|
||||||
<h3>${staff || "Unknown"}</h3>
|
<h3>${staff || "Unknown"}</h3>
|
||||||
<h3>${room || "Unknown"}</h3>
|
<h3>${(type == "class") ? room : (type == "tutorial") ? "N/A" : "Unknown"}</h3>
|
||||||
<h4>${from || "Unknown"} - ${until || "Unknown"}</h4>
|
<h4>${from || "Unknown"} - ${until || "Unknown"}</h4>
|
||||||
<h5>${attendanceTitle || "Unknown"}</h5>
|
<h5>${attendanceTitle || "Unknown"}</h5>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (programmeID !== 0) {
|
if (type == "class") {
|
||||||
lessonString += `
|
if (programmeID !== 0) {
|
||||||
|
lessonString += `
|
||||||
<div class="day-button clickable" style="right: 5px;" onclick="location.href='${buildAssessmentURL(programmeID, metaID)}'">${assessmentsicon}</div>
|
<div class="day-button clickable" style="right: 5px;" onclick="location.href='${buildAssessmentURL(programmeID, metaID)}'">${assessmentsicon}</div>
|
||||||
<div class="day-button clickable" style="right: 35px;" onclick="location.href='../#?page=/courses/${programmeID}:${metaID}'">${coursesicon}</div>
|
<div class="day-button clickable" style="right: 35px;" onclick="location.href='../#?page=/courses/${programmeID}:${metaID}'">${coursesicon}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assessments && assessments.length > 0) {
|
if (assessments && assessments.length > 0) {
|
||||||
const assessmentString = assessments
|
const assessmentString = assessments
|
||||||
.map(
|
.map(
|
||||||
(element: any) =>
|
(element: any) =>
|
||||||
`<p onclick="location.href = '${buildAssessmentURL(programmeID, metaID, element.id)}';">${element.title}</p>`,
|
`<p onclick="location.href = '${buildAssessmentURL(programmeID, metaID, element.id)}';">${element.title}</p>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
lessonString += `
|
lessonString += `
|
||||||
<div class="fixed-tooltip assessmenttooltip">
|
<div class="fixed-tooltip assessmenttooltip">
|
||||||
<svg style="width:28px;height:28px;border-radius:0;" viewBox="0 0 24 24">
|
<svg style="width:28px;height:28px;border-radius:0;" viewBox="0 0 24 24">
|
||||||
<path fill="#ed3939" d="M16 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H16C17.11 22 18 21.11 18 20V4C18 2.9 17.11 2 16 2M16 20H4V4H6V12L8.5 9.75L11 12V4H16V20M20 15H22V17H20V15M22 7V13H20V7H22Z" />
|
<path fill="#ed3939" d="M16 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H16C17.11 22 18 21.11 18 20V4C18 2.9 17.11 2 16 2M16 20H4V4H6V12L8.5 9.75L11 12V4H16V20M20 15H22V17H20V15M22 7V13H20V7H22Z" />
|
||||||
@@ -964,6 +905,7 @@ function makeLessonDiv(lesson: any, num: number) {
|
|||||||
<div class="tooltiptext">${assessmentString}</div>
|
<div class="tooltiptext">${assessmentString}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lessonString += "</div>";
|
lessonString += "</div>";
|
||||||
|
|||||||
Reference in New Issue
Block a user