diff --git a/src/background.ts b/src/background.ts
index a96cb847..8750b6b5 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -21,7 +21,8 @@ chrome.runtime.onInstalled.addListener(async (details) => {
theme: 'system',
studiengang: 'general',
hisqisPimpedTable: true,
- bannersShown: ['mv3UpdateNotice']
+ bannersShown: ['mv3UpdateNotice'],
+ improveSelma: true,
})
await openSettingsPage('first_visit')
break
@@ -36,6 +37,7 @@ chrome.runtime.onInstalled.addListener(async (details) => {
'theme',
'studiengang',
'hisqisPimpedTable',
+ 'improveSelma',
'savedClickCounter',
'saved_click_counter', // legacy
'Rocket', // legacy
@@ -57,6 +59,7 @@ chrome.runtime.onInstalled.addListener(async (details) => {
if (typeof currentSettings.dashboardDisplay === 'undefined') updateObj.dashboardDisplay = 'favoriten'
if (typeof currentSettings.fwdEnabled === 'undefined') updateObj.fwdEnabled = true
if (typeof currentSettings.hisqisPimpedTable === 'undefined') updateObj.hisqisPimpedTable = true
+ if (typeof currentSettings.improveSelma === 'undefined') updateObj.improveSelma = true
if (typeof currentSettings.theme === 'undefined') updateObj.theme = 'system'
if (typeof currentSettings.studiengang === 'undefined') updateObj.studiengang = 'general'
if (typeof currentSettings.selectedRocketIcon === 'undefined') updateObj.selectedRocketIcon = JSON.stringify(rockets.default)
diff --git a/src/contentScripts/other/selma/layout.ts b/src/contentScripts/other/selma/layout.ts
new file mode 100644
index 00000000..0f0a03b8
--- /dev/null
+++ b/src/contentScripts/other/selma/layout.ts
@@ -0,0 +1,550 @@
+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=([^"]+)"/
+
+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}`
+}
+
+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')
+ }
+}
+
+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`
+ );
+
+ (document.head || document.body || document.documentElement).appendChild(
+ 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 `
+
+ `
+ }
+
+ 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 `
+
+ `
+ }
+}
+
+/*
+---
+
+Actual logic
+
+---
+*/
+
+// Create a small banner that indicates the user that the site was modified
+// It also adds a small toggle to disable the table
+async function createCreditsBanner() {
+ const { improveSelma: settingEnabled } = await chrome.storage.local.get(['improveSelma'])
+
+ const imgUrl = chrome.runtime.getURL('/assets/images/tufast48.png')
+ const credits = document.createElement('p')
+
+ credits.style.margin = 'auto'
+ credits.style.marginRight = '0'
+ credits.style.color = '#002557' // Selma theme color
+ credits.id = 'TUfastCredits'
+ credits.innerHTML = `Table ${settingEnabled ? 'powered by' : 'disabled'}
+
+ TUfast
+ by AKORA
+ `
+
+ const disableButton = document.createElement('button')
+ // Similiar style to logout button
+ disableButton.setAttribute(
+ 'style',
+ `
+ border: 1px solid rgb(255, 255, 255);
+ color: rgb(221, 39, 39);
+ text-decoration: none;
+ padding: 0.5rem 1rem;
+ margin: 0 1rem;
+ border-radius: 0px;
+ `
+ )
+
+ // Tooltip
+ disableButton.title =
+ 'Toggle the "ImproveSelma" feature and reload the page to apply the change.'
+ disableButton.textContent = settingEnabled ? 'Deactivate' : 'Activate'
+ disableButton.onclick = async (event) => {
+ event.preventDefault()
+ await chrome.storage.local.set({ improveSelma: !settingEnabled })
+ window.location.reload()
+ }
+ credits.appendChild(disableButton)
+
+ return credits
+}
+
+(async () => {
+ const { improveSelma } = await chrome.storage.local.get(['improveSelma'])
+
+ // Apply all custom changes
+ document.addEventListener('DOMContentLoaded', async () => {
+ // Add Credit banner with toggle button
+ const creditElm = await createCreditsBanner()
+ document.querySelector('.semesterChoice')!.appendChild(creditElm)
+
+ if (!improveSelma) return
+
+ eventListener();
+ })
+})()
+
+async function eventListener () {
+ document.removeEventListener('DOMContentLoaded', eventListener)
+
+ // Inject css
+ injectCSS('base')
+ if (
+ currentView.startsWith('/APP/EXAMRESULTS/') ||
+ currentView.startsWith('/APP/COURSERESULTS/')
+ ) {
+ injectCSS('exam_results')
+ }
+ if (currentView.startsWith('/APP/MYEXAMS/')) {
+ injectCSS('my_exams')
+ }
+
+ applyChanges()
+}
+
+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 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)!)
+
+ {
+ // 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)
+ // Skip courses wihtout grades
+ 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 }) => {
+ // 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
+ })
+ )
+
+ // 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 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)
+ })
+ }
+
+ /*
+
+*/
+ } 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'
+ // 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)
+ }
+
+ {
+ // 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',
+ ''
+ )
+ }
+
+ // 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..756a7e48 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 ImproveSelma from './settingPages/ImproveSelma.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,
+ ImproveSelma,
Shortcuts,
SearchEngines,
Rockets,
diff --git a/src/freshContent/settings/components/SettingTile.vue b/src/freshContent/settings/components/SettingTile.vue
index 21b93791..9e7c2ca9 100644
--- a/src/freshContent/settings/components/SettingTile.vue
+++ b/src/freshContent/settings/components/SettingTile.vue
@@ -19,7 +19,7 @@
+
+
diff --git a/src/freshContent/settings/settings.json b/src/freshContent/settings/settings.json
index 782c58aa..a3ef9cad 100644
--- a/src/freshContent/settings/settings.json
+++ b/src/freshContent/settings/settings.json
@@ -19,6 +19,11 @@
"icon": "PhSparkle",
"settingsPage": "ImproveOpal"
},
+ {
+ "title": "Selma verbessern",
+ "icon": "PhChartBar",
+ "settingsPage": "ImproveSelma"
+ },
{
"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 0f706290..6f3c1a8d 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/*"]
},
+ {
+ "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",
@@ -223,47 +223,47 @@
"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/*"]
+ },
+ {
+ "resources": ["styles/contentScripts/selma/*"],
+ "matches": ["https://selma.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..3b19e273
--- /dev/null
+++ b/src/styles/contentScripts/selma/base.scss
@@ -0,0 +1,5 @@
+.pageContent {
+ min-width: unset;
+ max-width: unset;
+ padding: 3rem 5vw 5rem 5rem;
+}
diff --git a/src/styles/contentScripts/selma/exam_results.scss b/src/styles/contentScripts/selma/exam_results.scss
new file mode 100644
index 00000000..2c03dd0d
--- /dev/null
+++ b/src/styles/contentScripts/selma/exam_results.scss
@@ -0,0 +1,107 @@
+// 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;
+ background: unset;
+}
+
+tbody > tr > td {
+ background: unset;
+ border-bottom: 1px solid #ccc;
+}
+
+tbody :nth-child(2n of tr) {
+ background: #e7eaf0;
+}
+
+tbody > tr ::first-line {
+ color: #002557;
+}
+
+// Make the bottom text gray in the "Prüfungsleistung section"
+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 > td > svg {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+
+ .passed {
+ fill: #315584;
+ }
+
+ .failed {
+ fill: #dd2727aa;
+ }
+
+ .used {
+ fill: #315584;
+ }
+
+ .open {
+ fill: none;
+ stroke: #315584aa;
+ }
+
+ &.distribution-chart {
+ height: 3lh;
+ }
+
+ &.tries-counter {
+ height: 1lh;
+ }
+}
+
+// Not sure if this helps
+tbody > tr > td :has(svg) {
+ vertical-align: middle;
+}
+
+//Courseresults page
+tbody > tr > td.tbdata > svg.distribution-chart {
+ 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 :nth-child(3 of td) {
+ vertical-align: middle;
+ 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;
+ }
+}
diff --git a/src/styles/contentScripts/selma/my_exams.scss b/src/styles/contentScripts/selma/my_exams.scss
new file mode 100644
index 00000000..15bd6533
--- /dev/null
+++ b/src/styles/contentScripts/selma/my_exams.scss
@@ -0,0 +1,24 @@
+//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;
+
+ &.module-description {
+ padding-right: 2rem;
+ }
+ }
+}
+
+tbody :nth-child(4n of tr),
+tbody :nth-child(4n - 1 of tr) {
+ background: #e7eaf0;
+}