From e54a024e769010a01762a78fd7f24860ce108d4b Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:03:40 +0200 Subject: [PATCH 01/22] =?UTF-8?q?pr=C3=BCfunge=20overview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/manifest.firefox.json | 92 +++++++++++------------ src/styles/contentScripts/selma/base.scss | 33 ++++++++ 2 files changed, 77 insertions(+), 48 deletions(-) create mode 100644 src/styles/contentScripts/selma/base.scss diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index 0f706290..286323ab 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -2,10 +2,7 @@ "name": "TUfast TU Dresden", "version": "8.1.0.1", "description": "Das Produktivitäts-Tool für TU Dresden Studierende 🚀", - "permissions": [ - "storage", - "alarms" - ], + "permissions": ["storage", "alarms"], "optional_permissions": [ "tabs", "notifications", @@ -13,9 +10,7 @@ "webRequest", "webRequestBlocking" ], - "host_permissions": [ - "*://*/" - ], + "host_permissions": ["*://*/"], "background": { "scripts": ["background.js"], "type": "module" @@ -83,6 +78,11 @@ "run_at": "document_idle", "matches": ["https://selma.tu-dresden.de/*"] }, + { + "css": ["styles/contentScripts/selma/base.css"], + "run_at": "document_start", + "matches": ["https://selma.tu-dresden.de/*"] + }, { "js": ["contentScripts/login/qis.js"], "run_at": "document_idle", @@ -223,47 +223,43 @@ "page": "freshContent/settings/index.html", "open_in_tab": true }, - "web_accessible_resources": [{ - "resources": [ - "assets/*", - "contentScripts/other/notification.js" - ], - "matches": [""] - }, - { - "resources": ["contentScripts/login/common.js"], - "matches": [ - "https://*.tu-dresden.de/*", - "https://bildungsportal.sachsen.de/*", - "https://videocampus.sachsen.de/*", - "https://git.imld.de/*", - "https://gitlab.hrz.tu-chemnitz.de/*", - "https://*.slub-dresden.de/*" - ] - }, - { - "resources": [ - "contentScripts/forward/searchEngines/common.js", - "contentScripts/forward/searchEngines/sites.json*" - ], - "matches": [ - "https://www.startpage.com/*", - "https://www.qwant.com/*", - "https://www.google.de/*", - "https://www.google.com/*", - "https://duckduckgo.com/*", - "https://www.ecosia.org/*", - "https://www.bing.com/*", - "https://search.brave.com/*" - ] - }, - { - "resources": [ - "contentScripts/other/hisqis/*", - "snowpack/pkg/*" - ], - "matches": ["https://qis.dez.tu-dresden.de/*"] - }], + "web_accessible_resources": [ + { + "resources": ["assets/*", "contentScripts/other/notification.js"], + "matches": [""] + }, + { + "resources": ["contentScripts/login/common.js"], + "matches": [ + "https://*.tu-dresden.de/*", + "https://bildungsportal.sachsen.de/*", + "https://videocampus.sachsen.de/*", + "https://git.imld.de/*", + "https://gitlab.hrz.tu-chemnitz.de/*", + "https://*.slub-dresden.de/*" + ] + }, + { + "resources": [ + "contentScripts/forward/searchEngines/common.js", + "contentScripts/forward/searchEngines/sites.json*" + ], + "matches": [ + "https://www.startpage.com/*", + "https://www.qwant.com/*", + "https://www.google.de/*", + "https://www.google.com/*", + "https://duckduckgo.com/*", + "https://www.ecosia.org/*", + "https://www.bing.com/*", + "https://search.brave.com/*" + ] + }, + { + "resources": ["contentScripts/other/hisqis/*", "snowpack/pkg/*"], + "matches": ["https://qis.dez.tu-dresden.de/*"] + } + ], "manifest_version": 3, "browser_specific_settings": { "gecko": { diff --git a/src/styles/contentScripts/selma/base.scss b/src/styles/contentScripts/selma/base.scss new file mode 100644 index 00000000..d794edb3 --- /dev/null +++ b/src/styles/contentScripts/selma/base.scss @@ -0,0 +1,33 @@ +.pageContent { + min-width: unset; + max-width: unset; + padding: 3rem 10vh 5rem 5rem; +} + +tbody > tr > th { + /* + text-align: left; + vertical-align: top; + padding-top: 1.3rem; + padding-bottom: 1.3rem; + background: #e7eaf0; + color: #666; + font-weight: 400; + + */ + padding-top: 2rem; + padding-bottom: 0.5rem; + background: unset; +} + +tbody > tr > td { + background: unset; + border-bottom: 1px solid #ccc; + padding-top: 0.5rem; + padding-bottom: 2rem; +} + +tbody :nth-child(4n of tr), +tbody :nth-child(4n - 1 of tr) { + background: #e7eaf0; +} From 013e210f7a4b038f862146a5e94eab01c1d1d027 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:33:08 +0200 Subject: [PATCH 02/22] =?UTF-8?q?pr=C3=BCfungsergebnisse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/contentScripts/other/selma/layout.ts | 17 +++++++++++ src/manifest.firefox.json | 15 +++++++++- src/styles/contentScripts/selma/base.scss | 28 ------------------- .../contentScripts/selma/exam_results.scss | 24 ++++++++++++++++ src/styles/contentScripts/selma/my_exams.scss | 17 +++++++++++ 5 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 src/contentScripts/other/selma/layout.ts create mode 100644 src/styles/contentScripts/selma/exam_results.scss create mode 100644 src/styles/contentScripts/selma/my_exams.scss diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts new file mode 100644 index 00000000..487f57fa --- /dev/null +++ b/src/contentScripts/other/selma/layout.ts @@ -0,0 +1,17 @@ +const currentView = document.location.pathname; + +if (currentView.startsWith("/APP/EXAMRESULTS/")) { + // Prüfungen > Ergebnisse + + // Remove the "gut/befriedigend" section + const headRow = document.querySelector("thead>tr")!; + headRow.removeChild(headRow.children.item(3)!); + + const body = document.querySelector("tbody")!; + for (const row of body.children) { + row.removeChild(row.children.item(3)!); + + const firstCol = row.children.item(0)!; + firstCol.querySelector("div")!.style.color = "rgb(102, 102, 102)"; + } +} diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index 286323ab..17aeb560 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -74,7 +74,10 @@ ] }, { - "js": ["contentScripts/login/selma.js"], + "js": [ + "contentScripts/login/selma.js", + "contentScripts/other/selma/layout.js" + ], "run_at": "document_idle", "matches": ["https://selma.tu-dresden.de/*"] }, @@ -83,6 +86,16 @@ "run_at": "document_start", "matches": ["https://selma.tu-dresden.de/*"] }, + { + "css": ["styles/contentScripts/selma/my_exams.css"], + "run_at": "document_start", + "matches": ["https://selma.tu-dresden.de/APP/MYEXAMS/*"] + }, + { + "css": ["styles/contentScripts/selma/exam_results.css"], + "run_at": "document_start", + "matches": ["https://selma.tu-dresden.de/APP/EXAMRESULTS/*"] + }, { "js": ["contentScripts/login/qis.js"], "run_at": "document_idle", diff --git a/src/styles/contentScripts/selma/base.scss b/src/styles/contentScripts/selma/base.scss index d794edb3..0f7b647b 100644 --- a/src/styles/contentScripts/selma/base.scss +++ b/src/styles/contentScripts/selma/base.scss @@ -3,31 +3,3 @@ max-width: unset; padding: 3rem 10vh 5rem 5rem; } - -tbody > tr > th { - /* - text-align: left; - vertical-align: top; - padding-top: 1.3rem; - padding-bottom: 1.3rem; - background: #e7eaf0; - color: #666; - font-weight: 400; - - */ - padding-top: 2rem; - padding-bottom: 0.5rem; - background: unset; -} - -tbody > tr > td { - background: unset; - border-bottom: 1px solid #ccc; - padding-top: 0.5rem; - padding-bottom: 2rem; -} - -tbody :nth-child(4n of tr), -tbody :nth-child(4n - 1 of tr) { - background: #e7eaf0; -} diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss new file mode 100644 index 00000000..2f361e92 --- /dev/null +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -0,0 +1,24 @@ +tbody > tr > th { + padding-top: 2rem; + padding-bottom: 0.5rem; + background: unset; +} + +tbody > tr > td { + background: unset; + border-bottom: 1px solid #ccc; + padding-top: 0.5rem; + padding-bottom: 2rem; +} + +tbody :nth-child(2n of tr) { + background: #e7eaf0; +} + +tbody > tr.tbdata ::first-line { + color: black; +} + +tbody > tr.tbdata > td ::first-line { + color: #666; +} diff --git a/src/styles/contentScripts/selma/my_exams.scss b/src/styles/contentScripts/selma/my_exams.scss new file mode 100644 index 00000000..072d478c --- /dev/null +++ b/src/styles/contentScripts/selma/my_exams.scss @@ -0,0 +1,17 @@ +tbody > tr > th { + padding-top: 2rem; + padding-bottom: 0.5rem; + background: unset; +} + +tbody > tr > td { + background: unset; + border-bottom: 1px solid #ccc; + padding-top: 0.5rem; + padding-bottom: 2rem; +} + +tbody :nth-child(4n of tr), +tbody :nth-child(4n - 1 of tr) { + background: #e7eaf0; +} From fe6a264e4bb396d71e07f7fbc0c0caf3a34fab8c Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:22:59 +0200 Subject: [PATCH 03/22] diagram --- src/contentScripts/other/selma/layout.ts | 141 +++++++++++++++++- .../contentScripts/selma/exam_results.scss | 19 ++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 487f57fa..c5ea4e52 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -1,4 +1,63 @@ const currentView = document.location.pathname; +// Regex for extracting Programm name and arguments from a popup Script +// This is used to get the URL which would be opened in a popup +const popupScriptsRegex = + /Message = dl_popUp\("\/scripts\/mgrqispi\.dll\?APPNAME=CampusNet&PRGNAME=(\w+)&ARGUMENTS=([^"]+)"/; + +function scriptToURL(script: string): string { + const matches = script.match(popupScriptsRegex)!; + + const porgamName = matches.at(1)!; + const prgArguments = matches.at(2)!; + + return `https://selma.tu-dresden.de/APP/${porgamName}/${prgArguments}`; +} + +type GradeStat = { + grade: number; + count: number; +}; + +function maxGradeCount(values: GradeStat[]): number { + let max = 0; + for (const { grade, count } of values) { + if (count > max) max = count; + } + return max; +} + +function totalGradeCount(values: GradeStat[]): number { + return values.map(({ grade, count }) => count).reduce((p, c) => p + c); +} + +function calculateAverage(values: GradeStat[]): number { + return ( + values.map(({ grade, count }) => grade * count).reduce((p, c) => p + c) / + totalGradeCount(values) + ); +} + +// Reduce the grade increments +function pickGradeSubset(values: GradeStat[]): GradeStat[] { + const increments = [1, 1.3, 1.7, 2, 2.3, 2.7, 3, 3.3, 3.7, 4]; + + const newValues = increments.map((inc) => ({ + grade: inc, + count: 0, + })); + + let currentIncIndex = 0; + for (const { grade, count } of values) { + // Skip to next increment if we reached it's lower end + if (currentIncIndex !== increments.length - 1) { + const nextIncrement = increments[currentIncIndex + 1]; + if (grade >= nextIncrement) currentIncIndex++; + } + newValues[currentIncIndex].count += count; + } + + return newValues; +} if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Prüfungen > Ergebnisse @@ -8,10 +67,88 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { headRow.removeChild(headRow.children.item(3)!); const body = document.querySelector("tbody")!; + const promises: Promise<{ doc: Document; elm: Element }>[] = []; for (const row of body.children) { row.removeChild(row.children.item(3)!); - const firstCol = row.children.item(0)!; - firstCol.querySelector("div")!.style.color = "rgb(102, 102, 102)"; + const lastCol = row.children.item(3)!; + const scriptContent = lastCol.children.item(1)!.innerHTML; + + const url = scriptToURL(scriptContent); + + promises.push( + fetch(url).then(async (s) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(await s.text(), "text/html"); + + return { doc, elm: lastCol }; + }), + ); } + + (async () => { + const gradeOverviews = await Promise.all(promises); + + for (let i = 0; i < gradeOverviews.length; i++) { + const { doc, elm } = gradeOverviews[i]; + const tableBody = doc.querySelector("tbody")!; + const values = [...tableBody.children] + .map((tr) => { + const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); + const grade = parseFloat(gradeText); + + const countText = tr.children.item(1)!.textContent!; + let count: number; + if (countText === "---") { + count = 0; + } else { + count = parseInt(countText); + } + + return { + grade, + count, + }; + }) + .slice(0, -2); // Remove the 5.0 from all lists + + // Avg + const avg = calculateAverage(values); + elm.innerHTML = `avg: ${avg.toFixed(2)}`; + + // Drawing the Chart + const coarseValues = pickGradeSubset(values); + + const width = 200; + const spacing = 0.1; + const barWidth = (width * (1 - spacing)) / coarseValues.length; + const height = 100; + + let barsSvg = ""; + const maxCount = maxGradeCount(coarseValues); + for (let x = 0; x < coarseValues.length; x++) { + const { grade, count } = coarseValues[x]; + const barHeight = (count / maxCount) * height; + + barsSvg += ` + + `; + } + + elm.setAttribute("style", "vertical-align: middle;"); + elm.innerHTML = ` + + ${barsSvg} + + `; + + console.log(coarseValues); + } + })(); } diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index 2f361e92..e9cbd1dc 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -1,3 +1,7 @@ +table { + line-height: 1.5; +} + tbody > tr > th { padding-top: 2rem; padding-bottom: 0.5rem; @@ -8,7 +12,6 @@ tbody > tr > td { background: unset; border-bottom: 1px solid #ccc; padding-top: 0.5rem; - padding-bottom: 2rem; } tbody :nth-child(2n of tr) { @@ -22,3 +25,17 @@ tbody > tr.tbdata ::first-line { tbody > tr.tbdata > td ::first-line { color: #666; } + +tbody > tr.tbdata > td > svg { + // width: 10rem; + display: block; + // margin: auto; + height: 100%; + fill: #315584; + display: block; + max-height: 100%; +} + +tbody > tr.tbdata > td :has(svg) { + vertical-align: middle; +} From 3c3243e74abfffe7e278cbde7f209f8e30484028 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:57:32 +0200 Subject: [PATCH 04/22] fix diagram layout --- src/contentScripts/other/selma/layout.ts | 17 +++++++++++++++++ .../contentScripts/selma/exam_results.scss | 17 +++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index c5ea4e52..e49cf31c 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -139,7 +139,10 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { `; } + // Remove the inline vertical alignment elm.setAttribute("style", "vertical-align: middle;"); + + // Present the bar chart elm.innerHTML = ` `; + // Insert placeholder element for proper spacing + // elm.parentElement!.append(document.createElement("td")); + console.log(coarseValues); } + + // Remove the inline style that sets a width on the top right table cell + const tableHeadRow = document.querySelector("thead>tr")!; + + tableHeadRow.children.item(3)!.setAttribute("style", ""); + // Add spacing element + /* + const spacer = document.createElement("th"); + spacer.style.width = "2rem"; + tableHeadRow.append(spacer); + */ })(); } diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index e9cbd1dc..26f8f9cd 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -11,7 +11,7 @@ tbody > tr > th { tbody > tr > td { background: unset; border-bottom: 1px solid #ccc; - padding-top: 0.5rem; + // padding-top: 0.5rem; } tbody :nth-child(2n of tr) { @@ -27,15 +27,20 @@ tbody > tr.tbdata > td ::first-line { } tbody > tr.tbdata > td > svg { - // width: 10rem; + height: 3lh; display: block; - // margin: auto; - height: 100%; + margin-left: auto; + margin-right: auto; fill: #315584; - display: block; - max-height: 100%; } tbody > tr.tbdata > td :has(svg) { vertical-align: middle; } + +/* +thead :nth-child(4) { + width: 100%; + padding: 0; +} +*/ From 5918cac366e378fa877d279aee148a11b945ec64 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:28:58 +0200 Subject: [PATCH 05/22] reorganise code --- src/contentScripts/other/selma/layout.ts | 289 +++++++++++------- .../contentScripts/selma/exam_results.scss | 28 +- 2 files changed, 196 insertions(+), 121 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index e49cf31c..80698a5d 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -13,52 +13,6 @@ function scriptToURL(script: string): string { return `https://selma.tu-dresden.de/APP/${porgamName}/${prgArguments}`; } -type GradeStat = { - grade: number; - count: number; -}; - -function maxGradeCount(values: GradeStat[]): number { - let max = 0; - for (const { grade, count } of values) { - if (count > max) max = count; - } - return max; -} - -function totalGradeCount(values: GradeStat[]): number { - return values.map(({ grade, count }) => count).reduce((p, c) => p + c); -} - -function calculateAverage(values: GradeStat[]): number { - return ( - values.map(({ grade, count }) => grade * count).reduce((p, c) => p + c) / - totalGradeCount(values) - ); -} - -// Reduce the grade increments -function pickGradeSubset(values: GradeStat[]): GradeStat[] { - const increments = [1, 1.3, 1.7, 2, 2.3, 2.7, 3, 3.3, 3.7, 4]; - - const newValues = increments.map((inc) => ({ - grade: inc, - count: 0, - })); - - let currentIncIndex = 0; - for (const { grade, count } of values) { - // Skip to next increment if we reached it's lower end - if (currentIncIndex !== increments.length - 1) { - const nextIncrement = increments[currentIncIndex + 1]; - if (grade >= nextIncrement) currentIncIndex++; - } - newValues[currentIncIndex].count += count; - } - - return newValues; -} - if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Prüfungen > Ergebnisse @@ -69,6 +23,9 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const body = document.querySelector("tbody")!; const promises: Promise<{ doc: Document; elm: Element }>[] = []; for (const row of body.children) { + // Remove useless inline styles which set the vertical alignment + for (const col of row.children) col.removeAttribute("style"); + row.removeChild(row.children.item(3)!); const lastCol = row.children.item(3)!; @@ -90,82 +47,184 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const gradeOverviews = await Promise.all(promises); for (let i = 0; i < gradeOverviews.length; i++) { + // Parse the grade distributions const { doc, elm } = gradeOverviews[i]; const tableBody = doc.querySelector("tbody")!; - const values = [...tableBody.children] - .map((tr) => { - const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); - const grade = parseFloat(gradeText); - - const countText = tr.children.item(1)!.textContent!; - let count: number; - if (countText === "---") { - count = 0; - } else { - count = parseInt(countText); - } - - return { - grade, - count, - }; - }) - .slice(0, -2); // Remove the 5.0 from all lists - - // Avg - const avg = calculateAverage(values); - elm.innerHTML = `avg: ${avg.toFixed(2)}`; - - // Drawing the Chart - const coarseValues = pickGradeSubset(values); - - const width = 200; - const spacing = 0.1; - const barWidth = (width * (1 - spacing)) / coarseValues.length; - const height = 100; - - let barsSvg = ""; - const maxCount = maxGradeCount(coarseValues); - for (let x = 0; x < coarseValues.length; x++) { - const { grade, count } = coarseValues[x]; - const barHeight = (count / maxCount) * height; - - barsSvg += ` - - `; - } - - // Remove the inline vertical alignment - elm.setAttribute("style", "vertical-align: middle;"); + const values = [...tableBody.children].map((tr) => { + const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); + const grade = parseFloat(gradeText); + + const countText = tr.children.item(1)!.textContent!; + let count: number; + if (countText === "---") count = 0; + else count = parseInt(countText); + + return { + grade, + count, + }; + }); + // .slice(0, -2); // Remove the 5.0 from all lists // Present the bar chart - elm.innerHTML = ` - - ${barsSvg} - - `; - - // Insert placeholder element for proper spacing - // elm.parentElement!.append(document.createElement("td")); - - console.log(coarseValues); + const graphSVG = Graphing.createSVGGradeDistributionGraph(values); + elm.innerHTML = graphSVG; } // Remove the inline style that sets a width on the top right table cell const tableHeadRow = document.querySelector("thead>tr")!; - - tableHeadRow.children.item(3)!.setAttribute("style", ""); - // Add spacing element - /* - const spacer = document.createElement("th"); - spacer.style.width = "2rem"; - tableHeadRow.append(spacer); - */ + tableHeadRow.children.item(3)!.removeAttribute("style"); })(); } + +/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Proabably a proper bundler config would be better + +*/ + +namespace Graphing { + type GradeStat = { + grade: number; + count: number; + }; + + function maxGradeCount(values: GradeStat[]): number { + let max = 0; + for (const { count } of values) { + if (count > max) max = count; + } + return max; + } + + function totalGradeCount(values: GradeStat[]): number { + return values.map(({ grade, count }) => count).reduce((p, c) => p + c); + } + + function calculateAverage(values: GradeStat[]): number { + return ( + values.map(({ grade, count }) => grade * count).reduce((p, c) => p + c) / + totalGradeCount(values) + ); + } + + // Reduce the grade increments + function pickGradeSubset(values: GradeStat[]): GradeStat[] { + const increments = [1, 1.3, 1.7, 2, 2.3, 2.7, 3, 3.3, 3.7, 4, 5]; + + const newValues = increments.map((inc) => ({ + grade: inc, + count: 0, + })); + + let currentIncIndex = 0; + for (const { grade, count } of values) { + // Skip to next increment if we reached it's lower end + if (currentIncIndex !== increments.length - 1) { + const nextIncrement = increments[currentIncIndex + 1]; + if (grade >= nextIncrement) currentIncIndex++; + } + newValues[currentIncIndex].count += count; + } + + return newValues; + } + + export function createSVGGradeDistributionGraph( + values: GradeStat[], + width = 200, + height = 100, + ): string { + // Reduce the bar count / pick bigger intervals + const coarseValues = pickGradeSubset(values); + + // Spacing in percent of bar width + const spacing = 0.1; + const barWidth = (width * (1 - spacing)) / coarseValues.length; + + // Drawing the Chart + let barsSvg = ""; + const maxCount = maxGradeCount(coarseValues); + for (let x = 0; x < coarseValues.length - 1; x++) { + const { count } = coarseValues[x]; + const barHeight = (count / maxCount) * height; + + barsSvg += ` + + `; + } + + // Color the last rect for 5.0 / failed differently + { + const x = coarseValues.length - 1; + const { count } = coarseValues[x]; + const barHeight = (count / maxCount) * height; + + barsSvg += ` + + `; + } + + return ` + + ${barsSvg} + + `; + } +} diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index 26f8f9cd..67e09a63 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -1,7 +1,9 @@ +// Better spacing of the table content table { line-height: 1.5; } +// Alternating background and separator lines tbody > tr > th { padding-top: 2rem; padding-bottom: 0.5rem; @@ -22,25 +24,39 @@ tbody > tr.tbdata ::first-line { color: black; } +// Make the bottom text gray in the "Prüfungsleistung section" tbody > tr.tbdata > td ::first-line { color: #666; } +// The diagram tbody > tr.tbdata > td > svg { height: 3lh; display: block; margin-left: auto; margin-right: auto; - fill: #315584; + + .passed { + fill: #315584; + } + + .failed { + fill: #dd272757; + } } tbody > tr.tbdata > td :has(svg) { vertical-align: middle; } -/* -thead :nth-child(4) { - width: 100%; - padding: 0; +// Date styling +tbody > tr.tbdata :nth-child(2 of td) { + vertical-align: middle; +} + +// Grade styling +tbody > tr.tbdata :nth-child(3 of td) { + vertical-align: middle; + text-align: center; + font-size: 1.7rem; } -*/ From 1a54b90792b81998829ce01cd3d145af44757e07 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:02:52 +0200 Subject: [PATCH 06/22] Add features to the courseresults page as well --- src/contentScripts/other/selma/layout.ts | 81 ++++++++++++++++++- src/manifest.firefox.json | 5 +- .../contentScripts/selma/exam_results.scss | 29 +++++-- 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 80698a5d..5e1497ef 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -2,7 +2,7 @@ const currentView = document.location.pathname; // Regex for extracting Programm name and arguments from a popup Script // This is used to get the URL which would be opened in a popup const popupScriptsRegex = - /Message = dl_popUp\("\/scripts\/mgrqispi\.dll\?APPNAME=CampusNet&PRGNAME=(\w+)&ARGUMENTS=([^"]+)"/; + /dl_popUp\("\/scripts\/mgrqispi\.dll\?APPNAME=CampusNet&PRGNAME=(\w+)&ARGUMENTS=([^"]+)"/; function scriptToURL(script: string): string { const matches = script.match(popupScriptsRegex)!; @@ -28,8 +28,85 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { row.removeChild(row.children.item(3)!); + // Extract script content const lastCol = row.children.item(3)!; - const scriptContent = lastCol.children.item(1)!.innerHTML; + const scriptElm = lastCol.children.item(1); + if (scriptElm === null) continue; + + const scriptContent = scriptElm!.innerHTML; + + const url = scriptToURL(scriptContent); + + promises.push( + fetch(url).then(async (s) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(await s.text(), "text/html"); + + return { doc, elm: lastCol }; + }), + ); + } + + (async () => { + const gradeOverviews = await Promise.all(promises); + + for (let i = 0; i < gradeOverviews.length; i++) { + // Parse the grade distributions + const { doc, elm } = gradeOverviews[i]; + const tableBody = doc.querySelector("tbody")!; + const values = [...tableBody.children].map((tr) => { + const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); + const grade = parseFloat(gradeText); + + const countText = tr.children.item(1)!.textContent!; + let count: number; + if (countText === "---") count = 0; + else count = parseInt(countText); + + return { + grade, + count, + }; + }); + // .slice(0, -2); // Remove the 5.0 from all lists + + // Present the bar chart + const graphSVG = Graphing.createSVGGradeDistributionGraph(values); + elm.innerHTML = graphSVG; + } + + // Remove the inline style that sets a width on the top right table cell + const tableHeadRow = document.querySelector("thead>tr")!; + tableHeadRow.children.item(3)!.removeAttribute("style"); + })(); +} else if (currentView.startsWith("/APP/COURSERESULTS/")) { + // Prüfungen > Ergebnisse + + // Remove the "bestanden" section + const headRow = document.querySelector("thead>tr")!; + headRow.removeChild(headRow.children.item(3)!); + + const body = document.querySelector("tbody")!; + const promises: Promise<{ doc: Document; elm: Element }>[] = []; + for (const row of body.children) { + // Remove useless inline styles which set the vertical alignment + for (const col of row.children) col.removeAttribute("style"); + + row.removeChild(row.children.item(3)!); + + // Extract script content + const lastCol = row.children.item(4)!; + const scriptElm = lastCol.children.item(1); + if (scriptElm === null) { + const gradeElm = row.children.item(2)!; + + // Replace text because it is too big + if (gradeElm.textContent!.includes("noch nicht gesetzt")) { + gradeElm.textContent = "/"; + } + continue; + } + const scriptContent = scriptElm!.innerHTML; const url = scriptToURL(scriptContent); diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index 17aeb560..55384cae 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -94,7 +94,10 @@ { "css": ["styles/contentScripts/selma/exam_results.css"], "run_at": "document_start", - "matches": ["https://selma.tu-dresden.de/APP/EXAMRESULTS/*"] + "matches": [ + "https://selma.tu-dresden.de/APP/EXAMRESULTS/*", + "https://selma.tu-dresden.de/APP/COURSERESULTS/*" + ] }, { "js": ["contentScripts/login/qis.js"], diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index 67e09a63..11a91e79 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -13,24 +13,28 @@ tbody > tr > th { tbody > tr > td { background: unset; border-bottom: 1px solid #ccc; - // padding-top: 0.5rem; } tbody :nth-child(2n of tr) { background: #e7eaf0; } -tbody > tr.tbdata ::first-line { - color: black; +tbody > tr ::first-line { + color: #002557; } // Make the bottom text gray in the "Prüfungsleistung section" -tbody > tr.tbdata > td ::first-line { +tbody > tr > td ::first-line { color: #666; } +// Vertically center the text in all courseresult rows +tbody > tr > td.tbdata { + vertical-align: middle; +} + // The diagram -tbody > tr.tbdata > td > svg { +tbody > tr > td > svg { height: 3lh; display: block; margin-left: auto; @@ -45,17 +49,28 @@ tbody > tr.tbdata > td > svg { } } -tbody > tr.tbdata > td :has(svg) { +// Not sure if this helps +tbody > tr > td :has(svg) { vertical-align: middle; } +//Courseresults page +tbody > tr > td.tbdata > svg { + height: 2lh; +} + // Date styling +// tr.tbdata means it only applies to the Examresults page tbody > tr.tbdata :nth-child(2 of td) { vertical-align: middle; } +// td.tbdata means it only applies to the Courseresults page +tbody > tr :nth-child(3 of td.tbdata) { + vertical-align: middle; +} // Grade styling -tbody > tr.tbdata :nth-child(3 of td) { +tbody > tr :nth-child(3 of td) { vertical-align: middle; text-align: center; font-size: 1.7rem; From 3fa00ce3e9a640fbffa7ad5c86134e17e2d4c06a Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:04:55 +0200 Subject: [PATCH 07/22] Use document_idle instead of document_start --- src/manifest.firefox.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index 55384cae..09816f2d 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -83,17 +83,17 @@ }, { "css": ["styles/contentScripts/selma/base.css"], - "run_at": "document_start", + "run_at": "document_idle", "matches": ["https://selma.tu-dresden.de/*"] }, { "css": ["styles/contentScripts/selma/my_exams.css"], - "run_at": "document_start", + "run_at": "document_idle", "matches": ["https://selma.tu-dresden.de/APP/MYEXAMS/*"] }, { "css": ["styles/contentScripts/selma/exam_results.css"], - "run_at": "document_start", + "run_at": "document_idle", "matches": [ "https://selma.tu-dresden.de/APP/EXAMRESULTS/*", "https://selma.tu-dresden.de/APP/COURSERESULTS/*" From c39f024615d99b7bda4d59a0ae3390635ecc5d84 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:29:13 +0200 Subject: [PATCH 08/22] Add href to grade overview --- src/contentScripts/other/selma/layout.ts | 31 +++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 5e1497ef..92401748 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -21,7 +21,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { headRow.removeChild(headRow.children.item(3)!); const body = document.querySelector("tbody")!; - const promises: Promise<{ doc: Document; elm: Element }>[] = []; + const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; for (const row of body.children) { // Remove useless inline styles which set the vertical alignment for (const col of row.children) col.removeAttribute("style"); @@ -42,7 +42,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const parser = new DOMParser(); const doc = parser.parseFromString(await s.text(), "text/html"); - return { doc, elm: lastCol }; + return { doc, elm: lastCol, url }; }), ); } @@ -52,7 +52,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { for (let i = 0; i < gradeOverviews.length; i++) { // Parse the grade distributions - const { doc, elm } = gradeOverviews[i]; + const { doc, elm, url } = gradeOverviews[i]; const tableBody = doc.querySelector("tbody")!; const values = [...tableBody.children].map((tr) => { const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); @@ -71,7 +71,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // .slice(0, -2); // Remove the 5.0 from all lists // Present the bar chart - const graphSVG = Graphing.createSVGGradeDistributionGraph(values); + const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); elm.innerHTML = graphSVG; } @@ -79,6 +79,16 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const tableHeadRow = document.querySelector("thead>tr")!; tableHeadRow.children.item(3)!.removeAttribute("style"); })(); + /* + + + + + + + + +*/ } else if (currentView.startsWith("/APP/COURSERESULTS/")) { // Prüfungen > Ergebnisse @@ -87,7 +97,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { headRow.removeChild(headRow.children.item(3)!); const body = document.querySelector("tbody")!; - const promises: Promise<{ doc: Document; elm: Element }>[] = []; + const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; for (const row of body.children) { // Remove useless inline styles which set the vertical alignment for (const col of row.children) col.removeAttribute("style"); @@ -115,7 +125,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const parser = new DOMParser(); const doc = parser.parseFromString(await s.text(), "text/html"); - return { doc, elm: lastCol }; + return { doc, elm: lastCol, url }; }), ); } @@ -125,7 +135,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { for (let i = 0; i < gradeOverviews.length; i++) { // Parse the grade distributions - const { doc, elm } = gradeOverviews[i]; + const { doc, elm, url } = gradeOverviews[i]; const tableBody = doc.querySelector("tbody")!; const values = [...tableBody.children].map((tr) => { const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); @@ -144,7 +154,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // .slice(0, -2); // Remove the 5.0 from all lists // Present the bar chart - const graphSVG = Graphing.createSVGGradeDistributionGraph(values); + const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); elm.innerHTML = graphSVG; } @@ -253,6 +263,7 @@ namespace Graphing { export function createSVGGradeDistributionGraph( values: GradeStat[], + url: string, width = 200, height = 100, ): string { @@ -300,7 +311,9 @@ namespace Graphing { - ${barsSvg} + + ${barsSvg} + `; } From f69cb8f6fee509ac471c5b310ec3ae0f770291cd Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:35:37 +0200 Subject: [PATCH 09/22] Make everythin asynchronous to prevent lag --- src/contentScripts/other/selma/layout.ts | 35 ++++++++++-------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 92401748..8299abe5 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -47,12 +47,8 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { ); } - (async () => { - const gradeOverviews = await Promise.all(promises); - - for (let i = 0; i < gradeOverviews.length; i++) { - // Parse the grade distributions - const { doc, elm, url } = gradeOverviews[i]; + promises.forEach((p) => + p.then(({ doc, elm, url }) => { const tableBody = doc.querySelector("tbody")!; const values = [...tableBody.children].map((tr) => { const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); @@ -73,12 +69,12 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Present the bar chart const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); elm.innerHTML = graphSVG; - } + }), + ); - // Remove the inline style that sets a width on the top right table cell - const tableHeadRow = document.querySelector("thead>tr")!; - tableHeadRow.children.item(3)!.removeAttribute("style"); - })(); + // Remove the inline style that sets a width on the top right table cell + const tableHeadRow = document.querySelector("thead>tr")!; + tableHeadRow.children.item(3)!.removeAttribute("style"); /* @@ -130,12 +126,9 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { ); } - (async () => { - const gradeOverviews = await Promise.all(promises); - - for (let i = 0; i < gradeOverviews.length; i++) { + promises.forEach((p) => + p.then(({ doc, elm, url }) => { // Parse the grade distributions - const { doc, elm, url } = gradeOverviews[i]; const tableBody = doc.querySelector("tbody")!; const values = [...tableBody.children].map((tr) => { const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); @@ -156,12 +149,12 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Present the bar chart const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); elm.innerHTML = graphSVG; - } + }), + ); - // Remove the inline style that sets a width on the top right table cell - const tableHeadRow = document.querySelector("thead>tr")!; - tableHeadRow.children.item(3)!.removeAttribute("style"); - })(); + // Remove the inline style that sets a width on the top right table cell + const tableHeadRow = document.querySelector("thead>tr")!; + tableHeadRow.children.item(3)!.removeAttribute("style"); } /* From 04b264708d1d34cb63df38212c6ccf19a2edca5d Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:38:59 +0200 Subject: [PATCH 10/22] Reorganise 'my exams' table --- src/contentScripts/other/selma/layout.ts | 62 ++++++++++++++++++- src/styles/contentScripts/selma/base.scss | 2 +- src/styles/contentScripts/selma/my_exams.scss | 27 +++++--- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 8299abe5..2d18ac90 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -155,8 +155,68 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Remove the inline style that sets a width on the top right table cell const tableHeadRow = document.querySelector("thead>tr")!; tableHeadRow.children.item(3)!.removeAttribute("style"); -} + /* + + + + + + + + + + + + + + +*/ +} else if (currentView.startsWith("/APP/MYEXAMS/")) { + // Prüfungen + + const body = document.querySelector("tbody")!; + const rows = [...body.children]; + for (let i = 0; i < rows.length; i += 2) { + const topRow = rows[i]; + const botRow = rows[i + 1]; + + const thElm = topRow.children.item(0)!; + thElm.className += " module-description"; + const [moduleCode, hyperlink, _space, _br, description] = thElm.childNodes; + + { + // Move exam type and examinant to the right side + thElm.setAttribute("colspan", "2"); + const newSpacer = document.createElement("th"); + newSpacer.setAttribute("colspan", "2"); + newSpacer.replaceChildren(...botRow.children.item(1)!.children); + topRow.appendChild(newSpacer); + } + + { + // Move the description under the exam title + // Remove useless first element + botRow.removeChild(botRow.children.item(1)!); + const newDescriptionElm = botRow.children.item(0)!; + newDescriptionElm.setAttribute("colspan", "2"); + newDescriptionElm.className += " module-description"; + + // Some entries do not have a description + if (thElm.childNodes.length === 5) { + newDescriptionElm.appendChild(description); + } + } + + // Table head "Prüfungsleistung" + document.querySelector("thead > tr > th#Name")!.textContent = ""; + // Table head "Termin" + document.querySelector("thead > tr > th#Date")!.textContent = + "Prüfungsleistung/Termin"; + + console.log(thElm); + } +} /* diff --git a/src/styles/contentScripts/selma/base.scss b/src/styles/contentScripts/selma/base.scss index 0f7b647b..3b19e273 100644 --- a/src/styles/contentScripts/selma/base.scss +++ b/src/styles/contentScripts/selma/base.scss @@ -1,5 +1,5 @@ .pageContent { min-width: unset; max-width: unset; - padding: 3rem 10vh 5rem 5rem; + padding: 3rem 5vw 5rem 5rem; } diff --git a/src/styles/contentScripts/selma/my_exams.scss b/src/styles/contentScripts/selma/my_exams.scss index 072d478c..15bd6533 100644 --- a/src/styles/contentScripts/selma/my_exams.scss +++ b/src/styles/contentScripts/selma/my_exams.scss @@ -1,14 +1,21 @@ -tbody > tr > th { - padding-top: 2rem; - padding-bottom: 0.5rem; - background: unset; -} +//Alternating background +tbody > tr { + > th { + padding-top: 2rem; + padding-bottom: 0.5rem; + background: unset; + } + + > td { + background: unset; + border-bottom: 1px solid #ccc; + padding-top: 0.5rem; + padding-bottom: 2rem; -tbody > tr > td { - background: unset; - border-bottom: 1px solid #ccc; - padding-top: 0.5rem; - padding-bottom: 2rem; + &.module-description { + padding-right: 2rem; + } + } } tbody :nth-child(4n of tr), From 96f06cd5ebceecc6dc721c356de8e8cd242f79e8 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:47:07 +0200 Subject: [PATCH 11/22] Remove useless timespans from 'my exams' table --- src/contentScripts/other/selma/layout.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 2d18ac90..b87eed25 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -208,13 +208,17 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { } } + { + // Remove useless timespans + const dateElm = botRow.children.item(1)!; + dateElm.textContent = dateElm.textContent!.replaceAll("00:00-00:00", ""); + } + // Table head "Prüfungsleistung" document.querySelector("thead > tr > th#Name")!.textContent = ""; // Table head "Termin" document.querySelector("thead > tr > th#Date")!.textContent = "Prüfungsleistung/Termin"; - - console.log(thElm); } } /* From 6a97772e21f058d71875101d5720e2aeb79f7758 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:01:55 +0200 Subject: [PATCH 12/22] Add column title 'Notenverteilung' --- src/contentScripts/other/selma/layout.ts | 9 +++++++++ src/styles/contentScripts/selma/exam_results.scss | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index b87eed25..b6ddd83d 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -19,6 +19,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Remove the "gut/befriedigend" section const headRow = document.querySelector("thead>tr")!; headRow.removeChild(headRow.children.item(3)!); + headRow.children.item(3)!.textContent = "Notenverteilung"; const body = document.querySelector("tbody")!; const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; @@ -92,6 +93,14 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const headRow = document.querySelector("thead>tr")!; headRow.removeChild(headRow.children.item(3)!); + // Add "Notenverteilung" header + { + headRow.children.item(3)!.removeAttribute("colspan"); + const newHeader = document.createElement("th"); + newHeader.textContent = "Notenverteilung"; + headRow.appendChild(newHeader); + } + const body = document.querySelector("tbody")!; const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; for (const row of body.children) { diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index 11a91e79..cc2dfa2b 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -75,3 +75,17 @@ tbody > tr :nth-child(3 of td) { text-align: center; font-size: 1.7rem; } + +// Align the Header texts +thead > tr { + // "Note" / "Modulenote" + :nth-child(3) { + text-align: center; + } + + // "Notenverteilung" + :nth-last-child(1), + :nth-last-child(2) { + text-align: center; + } +} From 5c1d7bb3f2e44a42221c3657f7dbcfd6b43525d0 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:39:31 +0200 Subject: [PATCH 13/22] Map grade placeholders to emojis and add tooltips --- src/contentScripts/other/selma/layout.ts | 65 ++++++++++++++---------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index b6ddd83d..398ff178 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -13,6 +13,18 @@ function scriptToURL(script: string): string { return `https://selma.tu-dresden.de/APP/${porgamName}/${prgArguments}`; } +function mapGrade(gradeElm: Element) { + const grade = gradeElm.textContent!; + + if (grade.includes("be")) { + gradeElm.textContent = "✔"; + gradeElm.setAttribute("title", "Bestanden"); + } else if (grade.includes("noch nicht gesetzt")) { + gradeElm.textContent = "🕓"; + gradeElm.setAttribute("title", "Noch nicht gesetzt"); + } +} + if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Prüfungen > Ergebnisse @@ -107,20 +119,21 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { // Remove useless inline styles which set the vertical alignment for (const col of row.children) col.removeAttribute("style"); + // Remove "Status" column row.removeChild(row.children.item(3)!); + { + // Map grade descriptions to emojis + const gradeElm = row.children.item(2)!; + mapGrade(gradeElm); + } + // Extract script content const lastCol = row.children.item(4)!; const scriptElm = lastCol.children.item(1); - if (scriptElm === null) { - const gradeElm = row.children.item(2)!; + // Skip courses wihtout grades + if (scriptElm === null) continue; - // Replace text because it is too big - if (gradeElm.textContent!.includes("noch nicht gesetzt")) { - gradeElm.textContent = "/"; - } - continue; - } const scriptContent = scriptElm!.innerHTML; const url = scriptToURL(scriptContent); @@ -343,41 +356,37 @@ namespace Graphing { // Drawing the Chart let barsSvg = ""; const maxCount = maxGradeCount(coarseValues); - for (let x = 0; x < coarseValues.length - 1; x++) { - const { count } = coarseValues[x]; + for (let x = 0; x < coarseValues.length; x++) { + const { grade, count } = coarseValues[x]; const barHeight = (count / maxCount) * height; - barsSvg += ` - - `; - } - - // Color the last rect for 5.0 / failed differently - { - const x = coarseValues.length - 1; - const { count } = coarseValues[x]; - const barHeight = (count / maxCount) * height; + // Allows styling the failed sections differently + let className = "passed"; + if (grade >= 5.0) className = "failed"; barsSvg += ` - `; + > + ${grade.toFixed(2)} + + `; } return ` + + + ${barsSvg} From e0b9f65c2616b3c731e78575942b32fec6e9d668 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:31:25 +0200 Subject: [PATCH 14/22] Add try counter to course results --- src/contentScripts/other/selma/layout.ts | 121 +++++++++++++++++- .../contentScripts/selma/exam_results.scss | 20 ++- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 398ff178..f1af8feb 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -113,6 +113,7 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { headRow.appendChild(newHeader); } + // Create the grade distribution graph const body = document.querySelector("tbody")!; const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; for (const row of body.children) { @@ -178,6 +179,61 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { const tableHeadRow = document.querySelector("thead>tr")!; tableHeadRow.children.item(3)!.removeAttribute("style"); + // Draw try counter in the jExam style + for (const row of body.children) { + const linkElm = row.children.item(3)!; + const scriptElm = linkElm.children.item(1); + // Skip courses wihtout grades + if (scriptElm === null) continue; + + // Extract script content + const scriptContent = scriptElm!.innerHTML; + const url = scriptToURL(scriptContent); + + // Center the remaining "> Prüfung" links so it looks better after everything loaded + linkElm.setAttribute("style", "text-align: center;"); + + // Fetch data + fetch(url).then(async (s) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(await s.text(), "text/html"); + + // Extracting the grades of individual tries + const tableBody = doc.querySelector("tbody")!; + const tries: Graphing.Try[] = []; + + // Search for tries + for (let i = 0; i < tableBody.children.length; i++) { + const trElm = tableBody.children.item(i)!; + const firstTd = trElm.querySelector("td.level02"); + + // Before a row with a grade there is always a row containing "Modulprüfung" + if (firstTd !== null && firstTd.textContent === "Modulprüfung") { + // Next row will contain a try with a grade + let nextTrElm = tableBody.children.item(i + 1)!; + // Sometimes there is an extra row + if (nextTrElm.children.length === 1) { + nextTrElm = tableBody.children.item(i + 2)!; + } + + // Extract information + const date = nextTrElm.children.item(2)!.textContent!.trim(); + const grade = nextTrElm.children.item(3)!.textContent!.trim(); + tries.push({ date, grade }); + + i += 2; + continue; + } + } + + // Unable to parse the grades from the tables + if (tries.length === 0) return; + + // Replace link with a chart + linkElm.innerHTML = Graphing.createJExamTryCounter(tries, url); + }); + } + /* @@ -294,7 +350,7 @@ Proabably a proper bundler config would be better */ namespace Graphing { - type GradeStat = { + export type GradeStat = { grade: number; count: number; }; @@ -378,6 +434,7 @@ namespace Graphing { return ` @@ -392,4 +449,66 @@ namespace Graphing { `; } + + export type Try = { date: string; grade: string }; + + export function createJExamTryCounter( + tries: Try[], + url: string, + width = 200, + ): string { + // Spacing in percent of circle width + const spacing = 0.2; + // Stroke width in percent of radius + const strokeWidth = 0.12; + + const filledRadius = (width * (1 - spacing)) / 6; + const strokedRadius = filledRadius * (1 - strokeWidth); + // +1 to prevent weird cut off + const height = Math.ceil(2 * filledRadius) + 1; + + // Drawing the Chart + let svgContent = ""; + + for (let x = 0; x < 3; x++) { + let className = "used"; + let tooltip = ""; + if (x >= tries.length) { + // Mark open try + className = "open"; + } else { + const { date, grade } = tries[x]; + tooltip = `${grade}\n${date}`; + } + + svgContent += ` + + ${tooltip} + + `; + } + + return ` + + + + + + ${svgContent} + + + `; + } } diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index cc2dfa2b..bd4d7a2c 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -35,7 +35,6 @@ tbody > tr > td.tbdata { // The diagram tbody > tr > td > svg { - height: 3lh; display: block; margin-left: auto; margin-right: auto; @@ -47,6 +46,23 @@ tbody > tr > td > svg { .failed { fill: #dd272757; } + + .used { + fill: #315584; + } + + .open { + fill: none; + stroke: #315584aa; + } + + &.distribution-chart { + height: 3lh; + } + + &.tries-counter { + height: 1lh; + } } // Not sure if this helps @@ -55,7 +71,7 @@ tbody > tr > td :has(svg) { } //Courseresults page -tbody > tr > td.tbdata > svg { +tbody > tr > td.tbdata > svg.distribution-chart { height: 2lh; } From f64333dfafdd1e8492825148ed9b17c25c880297 Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:20:23 +0200 Subject: [PATCH 15/22] Add setting to toggle the Theme --- src/contentScripts/other/selma/layout.ts | 486 ++++++++++-------- src/freshContent/settings/Settings.vue | 2 + .../settings/settingPages/SelmajExamTheme.vue | 54 ++ src/freshContent/settings/settings.json | 5 + src/manifest.chrome.json | 9 + src/manifest.firefox.json | 26 +- .../contentScripts/selma/exam_results.scss | 2 +- 7 files changed, 353 insertions(+), 231 deletions(-) create mode 100644 src/freshContent/settings/settingPages/SelmajExamTheme.vue diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index f1af8feb..53379a64 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -25,216 +25,274 @@ function mapGrade(gradeElm: Element) { } } -if (currentView.startsWith("/APP/EXAMRESULTS/")) { - // Prüfungen > Ergebnisse +function injectCSS(filename: string) { + const style = document.createElement("link"); + style.rel = "stylesheet"; + style.type = "text/css"; + style.href = chrome.runtime.getURL( + `styles/contentScripts/selma/${filename}.css`, + ); - // Remove the "gut/befriedigend" section - const headRow = document.querySelector("thead>tr")!; - headRow.removeChild(headRow.children.item(3)!); - headRow.children.item(3)!.textContent = "Notenverteilung"; + (document.head || document.body || document.documentElement).appendChild( + style, + ); +} - const body = document.querySelector("tbody")!; - const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; - for (const row of body.children) { - // Remove useless inline styles which set the vertical alignment - for (const col of row.children) col.removeAttribute("style"); +/* - row.removeChild(row.children.item(3)!); - // Extract script content - const lastCol = row.children.item(3)!; - const scriptElm = lastCol.children.item(1); - if (scriptElm === null) continue; - const scriptContent = scriptElm!.innerHTML; - const url = scriptToURL(scriptContent); - promises.push( - fetch(url).then(async (s) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(await s.text(), "text/html"); - return { doc, elm: lastCol, url }; - }), - ); + + + + +*/ + +(async () => { + const { selmajExamTheme } = await chrome.storage.local.get([ + "selmajExamTheme", + ]); + + if (!selmajExamTheme) return; + + // Apply all custom changes + document.addEventListener("DOMContentLoaded", eventListener, false); +})(); + +function eventListener() { + document.removeEventListener("DOMContentLoaded", eventListener, false); + + // Inject css + injectCSS("base"); + if ( + currentView.startsWith("/APP/EXAMRESULTS/") || + currentView.startsWith("/APP/COURSERESULTS/") + ) { + injectCSS("exam_results"); + } + if (currentView.startsWith("/APP/MYEXAMS/")) { + injectCSS("my_exams"); } - promises.forEach((p) => - p.then(({ doc, elm, url }) => { - const tableBody = doc.querySelector("tbody")!; - const values = [...tableBody.children].map((tr) => { - const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); - const grade = parseFloat(gradeText); - - const countText = tr.children.item(1)!.textContent!; - let count: number; - if (countText === "---") count = 0; - else count = parseInt(countText); - - return { - grade, - count, - }; - }); - // .slice(0, -2); // Remove the 5.0 from all lists + applyChanges(); +} - // Present the bar chart - const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); - elm.innerHTML = graphSVG; - }), - ); +function applyChanges() { + if (currentView.startsWith("/APP/EXAMRESULTS/")) { + // Prüfungen > Ergebnisse - // Remove the inline style that sets a width on the top right table cell - const tableHeadRow = document.querySelector("thead>tr")!; - tableHeadRow.children.item(3)!.removeAttribute("style"); - /* + // Remove the "gut/befriedigend" section + const headRow = document.querySelector("thead>tr")!; + headRow.removeChild(headRow.children.item(3)!); + headRow.children.item(3)!.textContent = "Notenverteilung"; + const body = document.querySelector("tbody")!; + const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = + []; + for (const row of body.children) { + // Remove useless inline styles which set the vertical alignment + for (const col of row.children) col.removeAttribute("style"); + row.removeChild(row.children.item(3)!); + // Extract script content + const lastCol = row.children.item(3)!; + const scriptElm = lastCol.children.item(1); + if (scriptElm === null) continue; + const scriptContent = scriptElm!.innerHTML; + const url = scriptToURL(scriptContent); + promises.push( + fetch(url).then(async (s) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(await s.text(), "text/html"); + + return { doc, elm: lastCol, url }; + }), + ); + } + + promises.forEach((p) => + p.then(({ doc, elm, url }) => { + const tableBody = doc.querySelector("tbody")!; + const values = [...tableBody.children].map((tr) => { + const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); + const grade = parseFloat(gradeText); + + const countText = tr.children.item(1)!.textContent!; + let count: number; + if (countText === "---") count = 0; + else count = parseInt(countText); + + return { + grade, + count, + }; + }); + // .slice(0, -2); // Remove the 5.0 from all lists + + // Present the bar chart + const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); + elm.innerHTML = graphSVG; + }), + ); + + // Remove the inline style that sets a width on the top right table cell + const tableHeadRow = document.querySelector("thead>tr")!; + tableHeadRow.children.item(3)!.removeAttribute("style"); + /* -*/ -} else if (currentView.startsWith("/APP/COURSERESULTS/")) { - // Prüfungen > Ergebnisse - - // Remove the "bestanden" section - const headRow = document.querySelector("thead>tr")!; - headRow.removeChild(headRow.children.item(3)!); - - // Add "Notenverteilung" header - { - headRow.children.item(3)!.removeAttribute("colspan"); - const newHeader = document.createElement("th"); - newHeader.textContent = "Notenverteilung"; - headRow.appendChild(newHeader); - } - // Create the grade distribution graph - const body = document.querySelector("tbody")!; - const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = []; - for (const row of body.children) { - // Remove useless inline styles which set the vertical alignment - for (const col of row.children) col.removeAttribute("style"); - // Remove "Status" column - row.removeChild(row.children.item(3)!); + + + +*/ + } else if (currentView.startsWith("/APP/COURSERESULTS/")) { + // Prüfungen > Ergebnisse + + // Remove the "bestanden" section + const headRow = document.querySelector("thead>tr")!; + headRow.removeChild(headRow.children.item(3)!); + + // Add "Notenverteilung" header { - // Map grade descriptions to emojis - const gradeElm = row.children.item(2)!; - mapGrade(gradeElm); + headRow.children.item(3)!.removeAttribute("colspan"); + const newHeader = document.createElement("th"); + newHeader.textContent = "Notenverteilung"; + headRow.appendChild(newHeader); } - // Extract script content - const lastCol = row.children.item(4)!; - const scriptElm = lastCol.children.item(1); - // Skip courses wihtout grades - if (scriptElm === null) continue; + // Create the grade distribution graph + const body = document.querySelector("tbody")!; + const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = + []; + for (const row of body.children) { + // Remove useless inline styles which set the vertical alignment + for (const col of row.children) col.removeAttribute("style"); + + // Remove "Status" column + row.removeChild(row.children.item(3)!); + + { + // Map grade descriptions to emojis + const gradeElm = row.children.item(2)!; + mapGrade(gradeElm); + } - const scriptContent = scriptElm!.innerHTML; + // Extract script content + const lastCol = row.children.item(4)!; + const scriptElm = lastCol.children.item(1); + // Skip courses wihtout grades + if (scriptElm === null) continue; - const url = scriptToURL(scriptContent); + const scriptContent = scriptElm!.innerHTML; - promises.push( - fetch(url).then(async (s) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(await s.text(), "text/html"); + const url = scriptToURL(scriptContent); + + promises.push( + fetch(url).then(async (s) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(await s.text(), "text/html"); + + return { doc, elm: lastCol, url }; + }), + ); + } - return { doc, elm: lastCol, url }; + promises.forEach((p) => + p.then(({ doc, elm, url }) => { + // Parse the grade distributions + const tableBody = doc.querySelector("tbody")!; + const values = [...tableBody.children].map((tr) => { + const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); + const grade = parseFloat(gradeText); + + const countText = tr.children.item(1)!.textContent!; + let count: number; + if (countText === "---") count = 0; + else count = parseInt(countText); + + return { + grade, + count, + }; + }); + // .slice(0, -2); // Remove the 5.0 from all lists + + // Present the bar chart + const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); + elm.innerHTML = graphSVG; }), ); - } - promises.forEach((p) => - p.then(({ doc, elm, url }) => { - // Parse the grade distributions - const tableBody = doc.querySelector("tbody")!; - const values = [...tableBody.children].map((tr) => { - const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); - const grade = parseFloat(gradeText); - - const countText = tr.children.item(1)!.textContent!; - let count: number; - if (countText === "---") count = 0; - else count = parseInt(countText); - - return { - grade, - count, - }; - }); - // .slice(0, -2); // Remove the 5.0 from all lists + // Remove the inline style that sets a width on the top right table cell + const tableHeadRow = document.querySelector("thead>tr")!; + tableHeadRow.children.item(3)!.removeAttribute("style"); - // Present the bar chart - const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); - elm.innerHTML = graphSVG; - }), - ); + // Draw try counter in the jExam style + for (const row of body.children) { + const linkElm = row.children.item(3)!; + const scriptElm = linkElm.children.item(1); + // Skip courses wihtout grades + if (scriptElm === null) continue; - // Remove the inline style that sets a width on the top right table cell - const tableHeadRow = document.querySelector("thead>tr")!; - tableHeadRow.children.item(3)!.removeAttribute("style"); - - // Draw try counter in the jExam style - for (const row of body.children) { - const linkElm = row.children.item(3)!; - const scriptElm = linkElm.children.item(1); - // Skip courses wihtout grades - if (scriptElm === null) continue; - - // Extract script content - const scriptContent = scriptElm!.innerHTML; - const url = scriptToURL(scriptContent); - - // Center the remaining "> Prüfung" links so it looks better after everything loaded - linkElm.setAttribute("style", "text-align: center;"); - - // Fetch data - fetch(url).then(async (s) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(await s.text(), "text/html"); - - // Extracting the grades of individual tries - const tableBody = doc.querySelector("tbody")!; - const tries: Graphing.Try[] = []; - - // Search for tries - for (let i = 0; i < tableBody.children.length; i++) { - const trElm = tableBody.children.item(i)!; - const firstTd = trElm.querySelector("td.level02"); - - // Before a row with a grade there is always a row containing "Modulprüfung" - if (firstTd !== null && firstTd.textContent === "Modulprüfung") { - // Next row will contain a try with a grade - let nextTrElm = tableBody.children.item(i + 1)!; - // Sometimes there is an extra row - if (nextTrElm.children.length === 1) { - nextTrElm = tableBody.children.item(i + 2)!; - } + // Extract script content + const scriptContent = scriptElm!.innerHTML; + const url = scriptToURL(scriptContent); - // Extract information - const date = nextTrElm.children.item(2)!.textContent!.trim(); - const grade = nextTrElm.children.item(3)!.textContent!.trim(); - tries.push({ date, grade }); + // Center the remaining "> Prüfung" links so it looks better after everything loaded + linkElm.setAttribute("style", "text-align: center;"); + + // Fetch data + fetch(url).then(async (s) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(await s.text(), "text/html"); - i += 2; - continue; + // Extracting the grades of individual tries + const tableBody = doc.querySelector("tbody")!; + const tries: Graphing.Try[] = []; + + // Search for tries + for (let i = 0; i < tableBody.children.length; i++) { + const trElm = tableBody.children.item(i)!; + const firstTd = trElm.querySelector("td.level02"); + + // Before a row with a grade there is always a row containing "Modulprüfung" + if (firstTd !== null && firstTd.textContent === "Modulprüfung") { + // Next row will contain a try with a grade + let nextTrElm = tableBody.children.item(i + 1)!; + // Sometimes there is an extra row + if (nextTrElm.children.length === 1) { + nextTrElm = tableBody.children.item(i + 2)!; + } + + // Extract information + const date = nextTrElm.children.item(2)!.textContent!.trim(); + const grade = nextTrElm.children.item(3)!.textContent!.trim(); + tries.push({ date, grade }); + + i += 2; + continue; + } } - } - // Unable to parse the grades from the tables - if (tries.length === 0) return; + // Unable to parse the grades from the tables + if (tries.length === 0) return; - // Replace link with a chart - linkElm.innerHTML = Graphing.createJExamTryCounter(tries, url); - }); - } + // Replace link with a chart + linkElm.innerHTML = Graphing.createJExamTryCounter(tries, url); + }); + } - /* + /* @@ -250,55 +308,61 @@ if (currentView.startsWith("/APP/EXAMRESULTS/")) { */ -} else if (currentView.startsWith("/APP/MYEXAMS/")) { - // Prüfungen - - const body = document.querySelector("tbody")!; - const rows = [...body.children]; - for (let i = 0; i < rows.length; i += 2) { - const topRow = rows[i]; - const botRow = rows[i + 1]; - - const thElm = topRow.children.item(0)!; - thElm.className += " module-description"; - const [moduleCode, hyperlink, _space, _br, description] = thElm.childNodes; + } else if (currentView.startsWith("/APP/MYEXAMS/")) { + // Prüfungen + + const body = document.querySelector("tbody")!; + const rows = [...body.children]; + for (let i = 0; i < rows.length; i += 2) { + const topRow = rows[i]; + const botRow = rows[i + 1]; + + const thElm = topRow.children.item(0)!; + thElm.className += " module-description"; + const [moduleCode, hyperlink, _space, _br, description] = + thElm.childNodes; + + { + // Move exam type and examinant to the right side + thElm.setAttribute("colspan", "2"); + const newSpacer = document.createElement("th"); + newSpacer.setAttribute("colspan", "2"); + newSpacer.replaceChildren(...botRow.children.item(1)!.children); + topRow.appendChild(newSpacer); + } - { - // Move exam type and examinant to the right side - thElm.setAttribute("colspan", "2"); - const newSpacer = document.createElement("th"); - newSpacer.setAttribute("colspan", "2"); - newSpacer.replaceChildren(...botRow.children.item(1)!.children); - topRow.appendChild(newSpacer); - } + { + // Move the description under the exam title + // Remove useless first element + botRow.removeChild(botRow.children.item(1)!); + const newDescriptionElm = botRow.children.item(0)!; + newDescriptionElm.setAttribute("colspan", "2"); + newDescriptionElm.className += " module-description"; + + // Some entries do not have a description + if (thElm.childNodes.length === 5) { + newDescriptionElm.appendChild(description); + } + } - { - // Move the description under the exam title - // Remove useless first element - botRow.removeChild(botRow.children.item(1)!); - const newDescriptionElm = botRow.children.item(0)!; - newDescriptionElm.setAttribute("colspan", "2"); - newDescriptionElm.className += " module-description"; - - // Some entries do not have a description - if (thElm.childNodes.length === 5) { - newDescriptionElm.appendChild(description); + { + // Remove useless timespans + const dateElm = botRow.children.item(1)!; + dateElm.textContent = dateElm.textContent!.replaceAll( + "00:00-00:00", + "", + ); } - } - { - // Remove useless timespans - const dateElm = botRow.children.item(1)!; - dateElm.textContent = dateElm.textContent!.replaceAll("00:00-00:00", ""); + // Table head "Prüfungsleistung" + document.querySelector("thead > tr > th#Name")!.textContent = ""; + // Table head "Termin" + document.querySelector("thead > tr > th#Date")!.textContent = + "Prüfungsleistung/Termin"; } - - // Table head "Prüfungsleistung" - document.querySelector("thead > tr > th#Name")!.textContent = ""; - // Table head "Termin" - document.querySelector("thead > tr > th#Date")!.textContent = - "Prüfungsleistung/Termin"; } } + /* diff --git a/src/freshContent/settings/Settings.vue b/src/freshContent/settings/Settings.vue index 578ec5bd..bbae094e 100644 --- a/src/freshContent/settings/Settings.vue +++ b/src/freshContent/settings/Settings.vue @@ -78,6 +78,7 @@ import AutoLogin from './settingPages/AutoLogin.vue' import Email from './settingPages/Email.vue' import OpalCourses from './settingPages/OpalCourses.vue' import ImproveOpal from './settingPages/ImproveOpal.vue' +import SelmajExamTheme from './settingPages/SelmajExamTheme.vue' import Shortcuts from './settingPages/Shortcuts.vue' import SearchEngines from './settingPages/SearchEngines.vue' import Rockets from './settingPages/Rockets.vue' @@ -115,6 +116,7 @@ export default defineComponent({ Email, OpalCourses, ImproveOpal, + SelmajExamTheme, Shortcuts, SearchEngines, Rockets, diff --git a/src/freshContent/settings/settingPages/SelmajExamTheme.vue b/src/freshContent/settings/settingPages/SelmajExamTheme.vue new file mode 100644 index 00000000..70f07f53 --- /dev/null +++ b/src/freshContent/settings/settingPages/SelmajExamTheme.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/freshContent/settings/settings.json b/src/freshContent/settings/settings.json index 782c58aa..d878137d 100644 --- a/src/freshContent/settings/settings.json +++ b/src/freshContent/settings/settings.json @@ -19,6 +19,11 @@ "icon": "PhSparkle", "settingsPage": "ImproveOpal" }, + { + "title": "Selma jExam Theme", + "icon": "PhSparkle", + "settingsPage": "SelmajExamTheme" + }, { "title": "Shortcuts", "icon": "PhGauge", diff --git a/src/manifest.chrome.json b/src/manifest.chrome.json index 8aac4225..6d6631b2 100644 --- a/src/manifest.chrome.json +++ b/src/manifest.chrome.json @@ -83,6 +83,11 @@ "run_at": "document_idle", "matches": ["https://selma.tu-dresden.de/*"] }, + { + "js": ["contentScripts/other/selma/layout.js"], + "run_at": "document_start", + "matches": ["https://selma.tu-dresden.de/*"] + }, { "js": ["contentScripts/login/qis.js"], "run_at": "document_idle", @@ -263,6 +268,10 @@ "snowpack/pkg/*" ], "matches": ["https://qis.dez.tu-dresden.de/*"] + }, + { + "resources": ["styles/contentScripts/selma/*"], + "matches": ["https://selma.tu-dresden.de/*"] }], "manifest_version": 3, "commands": { diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index 09816f2d..6f3c1a8d 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -74,31 +74,15 @@ ] }, { - "js": [ - "contentScripts/login/selma.js", - "contentScripts/other/selma/layout.js" - ], + "js": ["contentScripts/login/selma.js"], "run_at": "document_idle", "matches": ["https://selma.tu-dresden.de/*"] }, { - "css": ["styles/contentScripts/selma/base.css"], - "run_at": "document_idle", + "js": ["contentScripts/other/selma/layout.js"], + "run_at": "document_start", "matches": ["https://selma.tu-dresden.de/*"] }, - { - "css": ["styles/contentScripts/selma/my_exams.css"], - "run_at": "document_idle", - "matches": ["https://selma.tu-dresden.de/APP/MYEXAMS/*"] - }, - { - "css": ["styles/contentScripts/selma/exam_results.css"], - "run_at": "document_idle", - "matches": [ - "https://selma.tu-dresden.de/APP/EXAMRESULTS/*", - "https://selma.tu-dresden.de/APP/COURSERESULTS/*" - ] - }, { "js": ["contentScripts/login/qis.js"], "run_at": "document_idle", @@ -274,6 +258,10 @@ { "resources": ["contentScripts/other/hisqis/*", "snowpack/pkg/*"], "matches": ["https://qis.dez.tu-dresden.de/*"] + }, + { + "resources": ["styles/contentScripts/selma/*"], + "matches": ["https://selma.tu-dresden.de/*"] } ], "manifest_version": 3, diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss index bd4d7a2c..2c03dd0d 100644 --- a/src/styles/contentScripts/selma/exam_results.scss +++ b/src/styles/contentScripts/selma/exam_results.scss @@ -44,7 +44,7 @@ tbody > tr > td > svg { } .failed { - fill: #dd272757; + fill: #dd2727aa; } .used { From 2ebff4b2314d643bccac069a29469264f904383d Mon Sep 17 00:00:00 2001 From: AKORA <65976562+A-K-O-R-A@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:43:34 +0200 Subject: [PATCH 16/22] Make tests pass --- src/contentScripts/other/selma/layout.ts | 708 ++++++++---------- .../settings/settingPages/SelmajExamTheme.vue | 34 +- 2 files changed, 332 insertions(+), 410 deletions(-) diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts index 53379a64..b78e03d7 100644 --- a/src/contentScripts/other/selma/layout.ts +++ b/src/contentScripts/other/selma/layout.ts @@ -1,578 +1,500 @@ -const currentView = document.location.pathname; +const currentView = document.location.pathname // Regex for extracting Programm name and arguments from a popup Script // This is used to get the URL which would be opened in a popup const popupScriptsRegex = - /dl_popUp\("\/scripts\/mgrqispi\.dll\?APPNAME=CampusNet&PRGNAME=(\w+)&ARGUMENTS=([^"]+)"/; + /dl_popUp\("\/scripts\/mgrqispi\.dll\?APPNAME=CampusNet&PRGNAME=(\w+)&ARGUMENTS=([^"]+)"/ -function scriptToURL(script: string): string { - const matches = script.match(popupScriptsRegex)!; +function scriptToURL (script: string): string { + const matches = script.match(popupScriptsRegex)! - const porgamName = matches.at(1)!; - const prgArguments = matches.at(2)!; + const porgamName = matches.at(1)! + const prgArguments = matches.at(2)! - return `https://selma.tu-dresden.de/APP/${porgamName}/${prgArguments}`; + return `https://selma.tu-dresden.de/APP/${porgamName}/${prgArguments}` } -function mapGrade(gradeElm: Element) { - const grade = gradeElm.textContent!; +function mapGrade (gradeElm: Element) { + const grade = gradeElm.textContent! - if (grade.includes("be")) { - gradeElm.textContent = "✔"; - gradeElm.setAttribute("title", "Bestanden"); - } else if (grade.includes("noch nicht gesetzt")) { - gradeElm.textContent = "🕓"; - gradeElm.setAttribute("title", "Noch nicht gesetzt"); + if (grade.includes('be')) { + gradeElm.textContent = '✔' + gradeElm.setAttribute('title', 'Bestanden') + } else if (grade.includes('noch nicht gesetzt')) { + gradeElm.textContent = '🕓' + gradeElm.setAttribute('title', 'Noch nicht gesetzt') } } -function injectCSS(filename: string) { - const style = document.createElement("link"); - style.rel = "stylesheet"; - style.type = "text/css"; +function injectCSS (filename: string) { + const style = document.createElement('link') + style.rel = 'stylesheet' + style.type = 'text/css' style.href = chrome.runtime.getURL( - `styles/contentScripts/selma/${filename}.css`, + `styles/contentScripts/selma/${filename}.css` ); (document.head || document.body || document.documentElement).appendChild( - style, - ); + style + ) } /* +--- +Proabably a proper bundler config would be better +--- +*/ +namespace Graphing { + export type GradeStat = { + grade: number; + count: number; + }; + function maxGradeCount (values: GradeStat[]): number { + let max = 0 + for (const { count } of values) { + if (count > max) max = count + } + return max + } + + // Reduce the grade increments + function pickGradeSubset (values: GradeStat[]): GradeStat[] { + const increments = [1, 1.3, 1.7, 2, 2.3, 2.7, 3, 3.3, 3.7, 4, 5] + + const newValues = increments.map((inc) => ({ + grade: inc, + count: 0 + })) + + let currentIncIndex = 0 + for (const { grade, count } of values) { + // Skip to next increment if we reached it's lower end + if (currentIncIndex !== increments.length - 1) { + const nextIncrement = increments[currentIncIndex + 1] + if (grade >= nextIncrement) currentIncIndex++ + } + newValues[currentIncIndex].count += count + } + return newValues + } + export function createSVGGradeDistributionGraph ( + values: GradeStat[], + url: string, + width = 200, + height = 100 + ): string { + // Reduce the bar count / pick bigger intervals + const coarseValues = pickGradeSubset(values) + // Spacing in percent of bar width + const spacing = 0.1 + const barWidth = (width * (1 - spacing)) / coarseValues.length + // Drawing the Chart + let barsSvg = '' + const maxCount = maxGradeCount(coarseValues) + for (let x = 0; x < coarseValues.length; x++) { + const { grade, count } = coarseValues[x] + const barHeight = (count / maxCount) * height + // Allows styling the failed sections differently + let className = 'passed' + if (grade >= 5.0) className = 'failed' + barsSvg += ` + + ${grade.toFixed(2)} + + ` + } + + return ` + + + + + + ${barsSvg} + + + ` + } + + export type Try = { date: string; grade: string }; + + export function createJExamTryCounter ( + tries: Try[], + url: string, + width = 200 + ): string { + // Spacing in percent of circle width + const spacing = 0.2 + // Stroke width in percent of radius + const strokeWidth = 0.12 + + const filledRadius = (width * (1 - spacing)) / 6 + const strokedRadius = filledRadius * (1 - strokeWidth) + // +1 to prevent weird cut off + const height = Math.ceil(2 * filledRadius) + 1 + + // Drawing the Chart + let svgContent = '' + + for (let x = 0; x < 3; x++) { + let className = 'used' + let tooltip = '' + if (x >= tries.length) { + // Mark open try + className = 'open' + } else { + const { date, grade } = tries[x] + tooltip = `${grade}\n${date}` + } + + svgContent += ` + + ${tooltip} + + ` + } + + return ` + + + + + + ${svgContent} + + + ` + } +} + +/* +--- + +Actual logic + +--- */ (async () => { const { selmajExamTheme } = await chrome.storage.local.get([ - "selmajExamTheme", - ]); + 'selmajExamTheme' + ]) - if (!selmajExamTheme) return; + if (!selmajExamTheme) return // Apply all custom changes - document.addEventListener("DOMContentLoaded", eventListener, false); -})(); + document.addEventListener('DOMContentLoaded', eventListener, false) +})() -function eventListener() { - document.removeEventListener("DOMContentLoaded", eventListener, false); +function eventListener () { + document.removeEventListener('DOMContentLoaded', eventListener, false) // Inject css - injectCSS("base"); + injectCSS('base') if ( - currentView.startsWith("/APP/EXAMRESULTS/") || - currentView.startsWith("/APP/COURSERESULTS/") + currentView.startsWith('/APP/EXAMRESULTS/') || + currentView.startsWith('/APP/COURSERESULTS/') ) { - injectCSS("exam_results"); + injectCSS('exam_results') } - if (currentView.startsWith("/APP/MYEXAMS/")) { - injectCSS("my_exams"); + if (currentView.startsWith('/APP/MYEXAMS/')) { + injectCSS('my_exams') } - applyChanges(); + applyChanges() } -function applyChanges() { - if (currentView.startsWith("/APP/EXAMRESULTS/")) { +function applyChanges () { + if (currentView.startsWith('/APP/EXAMRESULTS/')) { // Prüfungen > Ergebnisse // Remove the "gut/befriedigend" section - const headRow = document.querySelector("thead>tr")!; - headRow.removeChild(headRow.children.item(3)!); - headRow.children.item(3)!.textContent = "Notenverteilung"; + const headRow = document.querySelector('thead>tr')! + headRow.removeChild(headRow.children.item(3)!) + headRow.children.item(3)!.textContent = 'Notenverteilung' - const body = document.querySelector("tbody")!; + const body = document.querySelector('tbody')! const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = - []; + [] for (const row of body.children) { // Remove useless inline styles which set the vertical alignment - for (const col of row.children) col.removeAttribute("style"); + for (const col of row.children) col.removeAttribute('style') - row.removeChild(row.children.item(3)!); + row.removeChild(row.children.item(3)!) // Extract script content - const lastCol = row.children.item(3)!; - const scriptElm = lastCol.children.item(1); - if (scriptElm === null) continue; + const lastCol = row.children.item(3)! + const scriptElm = lastCol.children.item(1) + if (scriptElm === null) continue - const scriptContent = scriptElm!.innerHTML; + const scriptContent = scriptElm!.innerHTML - const url = scriptToURL(scriptContent); + const url = scriptToURL(scriptContent) promises.push( fetch(url).then(async (s) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(await s.text(), "text/html"); + const parser = new DOMParser() + const doc = parser.parseFromString(await s.text(), 'text/html') - return { doc, elm: lastCol, url }; - }), - ); + return { doc, elm: lastCol, url } + }) + ) } promises.forEach((p) => p.then(({ doc, elm, url }) => { - const tableBody = doc.querySelector("tbody")!; + const tableBody = doc.querySelector('tbody')! const values = [...tableBody.children].map((tr) => { - const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); - const grade = parseFloat(gradeText); + const gradeText = tr.children.item(0)!.textContent!.replace(',', '.') + const grade = parseFloat(gradeText) - const countText = tr.children.item(1)!.textContent!; - let count: number; - if (countText === "---") count = 0; - else count = parseInt(countText); + const countText = tr.children.item(1)!.textContent! + let count: number + if (countText === '---') count = 0 + else count = parseInt(countText) return { grade, - count, - }; - }); + count + } + }) // .slice(0, -2); // Remove the 5.0 from all lists // Present the bar chart - const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); - elm.innerHTML = graphSVG; - }), - ); + const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url) + elm.innerHTML = graphSVG + }) + ) // Remove the inline style that sets a width on the top right table cell - const tableHeadRow = document.querySelector("thead>tr")!; - tableHeadRow.children.item(3)!.removeAttribute("style"); + const tableHeadRow = document.querySelector('thead>tr')! + tableHeadRow.children.item(3)!.removeAttribute('style') /* - - - - - - - */ - } else if (currentView.startsWith("/APP/COURSERESULTS/")) { + } else if (currentView.startsWith('/APP/COURSERESULTS/')) { // Prüfungen > Ergebnisse // Remove the "bestanden" section - const headRow = document.querySelector("thead>tr")!; - headRow.removeChild(headRow.children.item(3)!); + const headRow = document.querySelector('thead>tr')! + headRow.removeChild(headRow.children.item(3)!) // Add "Notenverteilung" header { - headRow.children.item(3)!.removeAttribute("colspan"); - const newHeader = document.createElement("th"); - newHeader.textContent = "Notenverteilung"; - headRow.appendChild(newHeader); + headRow.children.item(3)!.removeAttribute('colspan') + const newHeader = document.createElement('th') + newHeader.textContent = 'Notenverteilung' + headRow.appendChild(newHeader) } // Create the grade distribution graph - const body = document.querySelector("tbody")!; + const body = document.querySelector('tbody')! const promises: Promise<{ doc: Document; elm: Element; url: string }>[] = - []; + [] for (const row of body.children) { // Remove useless inline styles which set the vertical alignment - for (const col of row.children) col.removeAttribute("style"); + for (const col of row.children) col.removeAttribute('style') // Remove "Status" column - row.removeChild(row.children.item(3)!); + row.removeChild(row.children.item(3)!) { // Map grade descriptions to emojis - const gradeElm = row.children.item(2)!; - mapGrade(gradeElm); + const gradeElm = row.children.item(2)! + mapGrade(gradeElm) } // Extract script content - const lastCol = row.children.item(4)!; - const scriptElm = lastCol.children.item(1); + const lastCol = row.children.item(4)! + const scriptElm = lastCol.children.item(1) // Skip courses wihtout grades - if (scriptElm === null) continue; + if (scriptElm === null) continue - const scriptContent = scriptElm!.innerHTML; + const scriptContent = scriptElm!.innerHTML - const url = scriptToURL(scriptContent); + const url = scriptToURL(scriptContent) promises.push( fetch(url).then(async (s) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(await s.text(), "text/html"); + const parser = new DOMParser() + const doc = parser.parseFromString(await s.text(), 'text/html') - return { doc, elm: lastCol, url }; - }), - ); + return { doc, elm: lastCol, url } + }) + ) } promises.forEach((p) => p.then(({ doc, elm, url }) => { // Parse the grade distributions - const tableBody = doc.querySelector("tbody")!; + const tableBody = doc.querySelector('tbody')! const values = [...tableBody.children].map((tr) => { - const gradeText = tr.children.item(0)!.textContent!.replace(",", "."); - const grade = parseFloat(gradeText); + const gradeText = tr.children.item(0)!.textContent!.replace(',', '.') + const grade = parseFloat(gradeText) - const countText = tr.children.item(1)!.textContent!; - let count: number; - if (countText === "---") count = 0; - else count = parseInt(countText); + const countText = tr.children.item(1)!.textContent! + let count: number + if (countText === '---') count = 0 + else count = parseInt(countText) return { grade, - count, - }; - }); + count + } + }) // .slice(0, -2); // Remove the 5.0 from all lists // Present the bar chart - const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url); - elm.innerHTML = graphSVG; - }), - ); + const graphSVG = Graphing.createSVGGradeDistributionGraph(values, url) + elm.innerHTML = graphSVG + }) + ) // Remove the inline style that sets a width on the top right table cell - const tableHeadRow = document.querySelector("thead>tr")!; - tableHeadRow.children.item(3)!.removeAttribute("style"); + const tableHeadRow = document.querySelector('thead>tr')! + tableHeadRow.children.item(3)!.removeAttribute('style') // Draw try counter in the jExam style for (const row of body.children) { - const linkElm = row.children.item(3)!; - const scriptElm = linkElm.children.item(1); + const linkElm = row.children.item(3)! + const scriptElm = linkElm.children.item(1) // Skip courses wihtout grades - if (scriptElm === null) continue; + if (scriptElm === null) continue // Extract script content - const scriptContent = scriptElm!.innerHTML; - const url = scriptToURL(scriptContent); + const scriptContent = scriptElm!.innerHTML + const url = scriptToURL(scriptContent) // Center the remaining "> Prüfung" links so it looks better after everything loaded - linkElm.setAttribute("style", "text-align: center;"); + linkElm.setAttribute('style', 'text-align: center;') // Fetch data fetch(url).then(async (s) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(await s.text(), "text/html"); + const parser = new DOMParser() + const doc = parser.parseFromString(await s.text(), 'text/html') // Extracting the grades of individual tries - const tableBody = doc.querySelector("tbody")!; - const tries: Graphing.Try[] = []; + const tableBody = doc.querySelector('tbody')! + const tries: Graphing.Try[] = [] // Search for tries for (let i = 0; i < tableBody.children.length; i++) { - const trElm = tableBody.children.item(i)!; - const firstTd = trElm.querySelector("td.level02"); + const trElm = tableBody.children.item(i)! + const firstTd = trElm.querySelector('td.level02') // Before a row with a grade there is always a row containing "Modulprüfung" - if (firstTd !== null && firstTd.textContent === "Modulprüfung") { + if (firstTd !== null && firstTd.textContent === 'Modulprüfung') { // Next row will contain a try with a grade - let nextTrElm = tableBody.children.item(i + 1)!; + let nextTrElm = tableBody.children.item(i + 1)! // Sometimes there is an extra row if (nextTrElm.children.length === 1) { - nextTrElm = tableBody.children.item(i + 2)!; + nextTrElm = tableBody.children.item(i + 2)! } // Extract information - const date = nextTrElm.children.item(2)!.textContent!.trim(); - const grade = nextTrElm.children.item(3)!.textContent!.trim(); - tries.push({ date, grade }); + const date = nextTrElm.children.item(2)!.textContent!.trim() + const grade = nextTrElm.children.item(3)!.textContent!.trim() + tries.push({ date, grade }) - i += 2; - continue; + i += 2 + continue } } // Unable to parse the grades from the tables - if (tries.length === 0) return; + if (tries.length === 0) return // Replace link with a chart - linkElm.innerHTML = Graphing.createJExamTryCounter(tries, url); - }); + linkElm.innerHTML = Graphing.createJExamTryCounter(tries, url) + }) } /* - - - - - - - - - - - - - */ - } else if (currentView.startsWith("/APP/MYEXAMS/")) { + } else if (currentView.startsWith('/APP/MYEXAMS/')) { // Prüfungen - const body = document.querySelector("tbody")!; - const rows = [...body.children]; + const body = document.querySelector('tbody')! + const rows = [...body.children] for (let i = 0; i < rows.length; i += 2) { - const topRow = rows[i]; - const botRow = rows[i + 1]; + const topRow = rows[i] + const botRow = rows[i + 1] - const thElm = topRow.children.item(0)!; - thElm.className += " module-description"; - const [moduleCode, hyperlink, _space, _br, description] = - thElm.childNodes; + const thElm = topRow.children.item(0)! + thElm.className += ' module-description' + // moduleCode, hyperlink, space, br, description + const [, , , , description] = thElm.childNodes { // Move exam type and examinant to the right side - thElm.setAttribute("colspan", "2"); - const newSpacer = document.createElement("th"); - newSpacer.setAttribute("colspan", "2"); - newSpacer.replaceChildren(...botRow.children.item(1)!.children); - topRow.appendChild(newSpacer); + thElm.setAttribute('colspan', '2') + const newSpacer = document.createElement('th') + newSpacer.setAttribute('colspan', '2') + newSpacer.replaceChildren(...botRow.children.item(1)!.children) + topRow.appendChild(newSpacer) } { // Move the description under the exam title // Remove useless first element - botRow.removeChild(botRow.children.item(1)!); - const newDescriptionElm = botRow.children.item(0)!; - newDescriptionElm.setAttribute("colspan", "2"); - newDescriptionElm.className += " module-description"; + botRow.removeChild(botRow.children.item(1)!) + const newDescriptionElm = botRow.children.item(0)! + newDescriptionElm.setAttribute('colspan', '2') + newDescriptionElm.className += ' module-description' // Some entries do not have a description if (thElm.childNodes.length === 5) { - newDescriptionElm.appendChild(description); + newDescriptionElm.appendChild(description) } } { // Remove useless timespans - const dateElm = botRow.children.item(1)!; + const dateElm = botRow.children.item(1)! dateElm.textContent = dateElm.textContent!.replaceAll( - "00:00-00:00", - "", - ); + '00:00-00:00', + '' + ) } // Table head "Prüfungsleistung" - document.querySelector("thead > tr > th#Name")!.textContent = ""; + document.querySelector('thead > tr > th#Name')!.textContent = '' // Table head "Termin" - document.querySelector("thead > tr > th#Date")!.textContent = - "Prüfungsleistung/Termin"; - } - } -} - -/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Proabably a proper bundler config would be better - -*/ - -namespace Graphing { - export type GradeStat = { - grade: number; - count: number; - }; - - function maxGradeCount(values: GradeStat[]): number { - let max = 0; - for (const { count } of values) { - if (count > max) max = count; + document.querySelector('thead > tr > th#Date')!.textContent = + 'Prüfungsleistung/Termin' } - return max; - } - - function totalGradeCount(values: GradeStat[]): number { - return values.map(({ grade, count }) => count).reduce((p, c) => p + c); - } - - function calculateAverage(values: GradeStat[]): number { - return ( - values.map(({ grade, count }) => grade * count).reduce((p, c) => p + c) / - totalGradeCount(values) - ); - } - - // Reduce the grade increments - function pickGradeSubset(values: GradeStat[]): GradeStat[] { - const increments = [1, 1.3, 1.7, 2, 2.3, 2.7, 3, 3.3, 3.7, 4, 5]; - - const newValues = increments.map((inc) => ({ - grade: inc, - count: 0, - })); - - let currentIncIndex = 0; - for (const { grade, count } of values) { - // Skip to next increment if we reached it's lower end - if (currentIncIndex !== increments.length - 1) { - const nextIncrement = increments[currentIncIndex + 1]; - if (grade >= nextIncrement) currentIncIndex++; - } - newValues[currentIncIndex].count += count; - } - - return newValues; - } - - export function createSVGGradeDistributionGraph( - values: GradeStat[], - url: string, - width = 200, - height = 100, - ): string { - // Reduce the bar count / pick bigger intervals - const coarseValues = pickGradeSubset(values); - - // Spacing in percent of bar width - const spacing = 0.1; - const barWidth = (width * (1 - spacing)) / coarseValues.length; - - // Drawing the Chart - let barsSvg = ""; - const maxCount = maxGradeCount(coarseValues); - for (let x = 0; x < coarseValues.length; x++) { - const { grade, count } = coarseValues[x]; - const barHeight = (count / maxCount) * height; - - // Allows styling the failed sections differently - let className = "passed"; - if (grade >= 5.0) className = "failed"; - - barsSvg += ` - - ${grade.toFixed(2)} - - `; - } - - return ` - - - - - - ${barsSvg} - - - `; - } - - export type Try = { date: string; grade: string }; - - export function createJExamTryCounter( - tries: Try[], - url: string, - width = 200, - ): string { - // Spacing in percent of circle width - const spacing = 0.2; - // Stroke width in percent of radius - const strokeWidth = 0.12; - - const filledRadius = (width * (1 - spacing)) / 6; - const strokedRadius = filledRadius * (1 - strokeWidth); - // +1 to prevent weird cut off - const height = Math.ceil(2 * filledRadius) + 1; - - // Drawing the Chart - let svgContent = ""; - - for (let x = 0; x < 3; x++) { - let className = "used"; - let tooltip = ""; - if (x >= tries.length) { - // Mark open try - className = "open"; - } else { - const { date, grade } = tries[x]; - tooltip = `${grade}\n${date}`; - } - - svgContent += ` - - ${tooltip} - - `; - } - - return ` - - - - - - ${svgContent} - - - `; } } diff --git a/src/freshContent/settings/settingPages/SelmajExamTheme.vue b/src/freshContent/settings/settingPages/SelmajExamTheme.vue index 70f07f53..886497d3 100644 --- a/src/freshContent/settings/settingPages/SelmajExamTheme.vue +++ b/src/freshContent/settings/settingPages/SelmajExamTheme.vue @@ -13,39 +13,39 @@