Schoology Grades

Grabs Grades from Schoology

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Schoology Grades
// @version      2.0
// @description  Grabs Grades from Schoology
// @author       Hafnium780
// @match        https://*.schoology.com/sg
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @license      MIT
// @namespace    hafnium780/sg
// ==/UserScript==

(function () {
  document.title = "sg";
  window.stop();
  document.body = document.createElement("body");
  // Make new page
  document.body.innerHTML = `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
* { margin: 0px; padding: 0px; font-family: Inter; align-items: center; text-align: center; }
.nosel { -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: -moz-none; -o-user-select: none; user-select: none; }
input { outline: none !important }

::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: #1E1F21; }
::-webkit-scrollbar-thumb { background: #363639; border-radius: 5px; }

html { overflow: hidden; }
body { padding: 0px; }

#bar { background-color: #363639; padding: 6px; height: 65px; top: 0px; left: 0px; right: 0px; border-bottom: 2px solid #424244; }

#title { color: #F5F4F3; font-size: 25px; margin: 6px 0px; font-weight: 700; }

#domain { margin-top: 3px; }
#domain-input { width: 50px; height: 12px; padding-bottom: -2px; font-size: 13px; border: none; border-radius: 2px; background-color: #161619; color: #F5F4F3; }
#domain-text { font-size: medium; color: #F5F4F3; }

#secrets { display: flex; position: absolute; height: 50px; width: 450px; transition: left ease 200ms; bottom: 0px; left: 0px; border: none; }
#secrets-input { background-color: #363639; width: 435px; padding: 4px; height: 42px; border: none; border-top: 2px solid #424244; }
#key { font-size: medium; padding-left: 47px; text-align: left; color: #F5F4F3; }
#key-input { width: 310px; height: 14px; background-color: #161619; border: 1px solid black; font-family: monospace; font-size: small; border: none; color: #F5F4F3; }
#signature { font-size: medium; padding-left: 3px; text-align: left; color: #F5F4F3; }
#signature-input { width: 310px; height: 14px; background-color: #161619; border: 1px solid black; font-family: monospace; font-size: small; border: none; color: #F5F4F3; }

#secrets-toggle { writing-mode: vertical-rl; text-orientation: mixed; color: #F5F4F3; width: 15px; background-color: #F9485F; height: 46px; padding: 2px; cursor: pointer; font-size: 12px; line-height: 1.25; border-top-right-radius: 10px; border-bottom-right-radius: 10px; }

#grades { display: flex; align-items: start; }

#course-grades { overflow: auto; width: 450px; height: calc(100vh - 65px); background-color: #2E2E30; border-right: 2px solid #424244; }

.course-grade-element:first-child { margin-top: 10px; }
.course-grade-element { font-size: 14px; display: flex; height: 40px; cursor: pointer; filter: brightness(100%); margin: 8px; background-color: #2E2E30; border-radius: 10px; }
.course-grade-name { width: 80%; text-align: left; padding-left: 10px; color: #F5F4F3; }
.course-grade-grade { width: 14%; text-align: center; padding: 5px; background-color: #E9384F; color: #F5F4F3; border-radius: 10px; margin-left: 16px; }


.course-grade-element:hover { filter: brightness(100%); background-color: #5A5A5C; transition: filter ease 300ms, background-color ease 300ms; }

#assignment-div { width: calc(100vw - 450px); height: calc(100vh - 65px); background-color: #1E1F21; }
#assignment-grades { overflow: auto; width: 100%; height: calc(100vh - 180px); background-color: #1E1F21; }
#calculated-grade-title { font-size: large; color: #F5F4F3; margin: 6px 0px; }
#calculated-grade { font-size: x-large; color: #F5F4F3; padding: 5px; background-color: #E9384F; border-radius: 10px; width: min-content; margin: 6px auto; }
#calculated-grade-error { font-size: small; color: #F5F4F3; margin: 6px 0px; }

.categorytitlediv { border-top: 2px solid #424244; position: sticky; background-color: #2E2E30;
                    top: 0px; display: flex; flex-direction: row; margin-bottom: 3px; padding: 5px; color: #F5F4F3; z-index: 100; }
.categorypoints { font-size: small; flex: 0 0; text-align: center; padding: 5px; background-color: #E9384F; border-radius: 10px; min-width: 100px; margin-left: 10px; }
.categorypercent { font-size: small; flex: 0 0; text-align: center; padding: 5px; background-color: #C1B4AE; color: #1E1F21; border-radius: 10px; min-width: 60px; }
.categorytitle { font-size: large; font-weight: bold; flex: 1 1; text-align: center; position: absolute; left: 0px; right: 0px; pointer-events: none; }
.categoryweight { font-size: small; padding: 5px; text-align: center; background-color: #C1B4AE; color: #1E1F21; border-radius: 10px; flex: 0 0; min-width: 100px; margin-left: 10px; margin-right: calc(100% - 423px); }
.categoryweightmethod { cursor: pointer; font-size: small; padding: 5px; text-align: center; background-color: #E9384F; border-radius: 10px; flex: 0 0; min-width: 100px; transition: background-color ease 200ms; }
.categoryweightmethod:hover { background-color: #F9586F; }
.categoryweight:empty { opacity: 0; }
.categorydiv:last-child { padding-bottom: 15px; }

.assignment-grade-element { padding: 6px; padding-left: 10px; display: flex; min-width: 0; flex-shrink: 0; flex-grow: 0; align-items: center; margin-right: 2px; border-top: 2px solid #424244; }
.assignment-grade-element:first-child { border: none; }
.assignment-grade-input { width: 55px; background-color: #931022; font-size: 13px; color: #F5F4F3; border: none; padding: 4px 3px 4px 4px; height: 18px; flex: 0 0 55px; text-align: right; border-radius: 5px; }
.assignment-grade-input.assignment-grade-input:focus { background-color: #531012 }
.assignment-grade-input:hover { background-color: #731012 }
::placeholder { color: #F5F4F3; font-size: 13px; }
.assignment-grade-max { flex: 0 0 52px; text-align: left; color: #F5F4F3; font-size: 13px; }
.assignment-grade-grade { display: flex; align-items: center; justify-content: end; font-size: small; text-align: right; flex-grow: 0; flex-shrink: 0; vertical-align: middle; text-align: center; padding-right: 10px; padding: 5px; background-color: #931022; border-radius: 10px; width: 100px; height: 16px; overflow: hidden; }
a.assignment-grade-name { color: #F5F4F3 !important; }
.assignment-grade-name { color: #F5F4F3; text-align: left; flex-grow: 1; margin-right: 3px; margin-left: 3px; font-size: medium; font-weight: normal; }
.assignment-grade-reset { width: 16px; height: 16px; background-color: #E9384F; flex: 0 0 16px; margin-right: 10px; cursor: pointer; scale: 1; border-radius: 7px; color: #F5F4F3 }
.assignment-grade-reset:hover { scale: 1.1; transition: scale ease 100ms; }
.assignment-grade-reload { border-radius: 8px; width: 16px; height: 16px; font-size: 12px; background-color: #767679; flex: 0 0 16px; left: 2px; text-align: center; color: #F5F4F3; margin: 0px 2px; cursor: pointer; scale: 1; padding: auto; }
.assignment-grade-reload:hover { scale: 1.1; transition: scale ease 100ms; }

</style>
<div id="bar" class="nosel">
  <div id="title"></div>
  <div id="domain">
    <span id="domain-text">Domain: </span>
    <input type="text" id="domain-input"></input>
    <span id="domain-text">.schoology.com</span>
  </div>
</div>
<div id="secrets" class="nosel">
  <div id="secrets-input">
    <div id="key">
      Key:
      <input id="key-input"></input>
    </div>
    <div id="signature">
      Signature:
      <input id="signature-input"></input>
    </div>
  </div>
  <div id="secrets-toggle">API</div>
</div>
<div id="grades">
  <div id="course-grades" class="nosel">
  </div>
  <div id="assignment-div">
    <div id="assignment-grades">

    </div>
  </div>
</div>
    `;
  const protocol = "https";

  let grades,
    courses,
    assignments,
    assignmentNames,
    assignmentFactors,
    categoryTypes;

  let assignmentShowID = undefined;

  const title = document.getElementById("title");
  const setTitle = (t) => {
    title.innerText = t;
  };

  // Get DOM elements

  const domain_input = document.getElementById("domain-input");
  const secrets = document.getElementById("secrets");
  const key_input = document.getElementById("key-input");
  const signature_input = document.getElementById("signature-input");
  const secrets_toggle = document.getElementById("secrets-toggle");
  const course_grades = document.getElementById("course-grades");
  const assignment_div = document.getElementById("assignment-div");
  const assignment_grades = document.getElementById("assignment-grades");

  // Before loading grades

  const authError = () => {
    setTitle("↙ Enter API Key and Signature");
  };

  const start = () => {
    if (!key || !signature) {
      authError();
      return;
    }
    setTitle("Loading Grades...");
    loadGrades();
  };

  // Load and save input values

  const storeAssignmentDetails = () => {
    GM_setValue("sg_assignment_names", assignmentNames);
    GM_setValue("sg_assignment_factors", assignmentFactors);
  };

  const storeCategoryDetails = () => {
    GM_setValue("sg_category_types", categoryTypes);
  };

  let key = GM_getValue("sg_key");
  let signature = GM_getValue("sg_signature");
  let domain = GM_getValue("sg_domain");

  if (domain) domain_input.value = domain;
  else {
    domain = new URL(window.location.href).host.split(".")[0];
    GM_setValue("sg_domain", domain);
    domain_input.value = domain;
  }

  domain_input.addEventListener("input", () => {
    GM_setValue("sg_domain", domain_input.value);
    domain = domain_input.value;
  });

  if (key) key_input.value = key;
  if (signature) signature_input.value = signature;

  if (key && signature) secrets.style.left = "-435px";

  key_input.addEventListener("input", () => {
    GM_setValue("sg_key", key_input.value);
    key = key_input.value;
    start();
  });

  signature_input.addEventListener("input", () => {
    GM_setValue("sg_signature", signature_input.value);
    signature = signature_input.value;
    start();
  });

  secrets_toggle.addEventListener("click", () => {
    if (secrets.style.left === "-435px") secrets.style.left = "0px";
    else secrets.style.left = "-435px";
  });

  const authHeader = () => {
    return (
      `OAuth realm="Schoology API",oauth_consumer_key="` +
      key +
      `",oauth_token="",oauth_nonce="` +
      Math.floor(Math.random() * 100000000) +
      `",oauth_timestamp="` +
      Math.floor(Date.now() / 1000) +
      `",oauth_signature_method="PLAINTEXT",oauth_version="1.0",oauth_signature="` +
      signature +
      `%26"`
    );
  };

  const schoologyHeaders = (fileType) => {
    return {
      Accept: fileType,
      Host: "api.schoology.com",
      Authorization: authHeader(),
      "Content-Type": "text/xml",
    };
  };

  const setHeaders = (req, fileType) => {
    req.setRequestHeader("Accept", fileType);
    req.setRequestHeader("Host", "api.schoology.com");
    req.setRequestHeader("Authorization", authHeader());
  };

  // API helper functions

  const getContent = (a, xml = false) => {
    return new Promise((res, rej) => {
      let link =
        a.indexOf(protocol + "://api.schoology.com") === -1
          ? protocol + "://api.schoology.com/v1/" + a
          : a;

      GM_xmlhttpRequest({
        url: link,
        method: "GET",
        headers: schoologyHeaders(xml ? "text/xml" : "application/json"),
        onload: (r) => {
          res(xml ? r.responseText : JSON.parse(r.responseText));
        },
        onerror: (e) => {
          rej(e);
        },
      });
    });
  };

  const getHTML = (a) => {
    return new Promise((res, rej) => {
      GM_xmlhttpRequest({
        url: a,
        method: "GET",
        headers: schoologyHeaders("text/html"),
        onload: (r) => {
          res(r.responseText);
        },
        onerror: (e) => {
          rej(e);
        },
      });
    });
  };

  const multiGet = (b) => {
    return new Promise((res, rej) => {
      GM_xmlhttpRequest({
        url: protocol + "://api.schoology.com/v1/multiget",
        method: "POST",
        headers: schoologyHeaders("application/json"),
        data: b,
        onload: (r) => {
          res(JSON.parse(r.responseText));
        },
        onerror: (e) => {
          rej(e);
        },
      });
    });
  };

  const roundGrade = (g) => {
    if (g && !isNaN(g)) return g.toFixed(2) + "%";
    return "N/A%";
  };

  const formatGrade = (a) => {
    return (a.grade ?? "---") + "/" + (a.max_points ?? "---");
  };

  // Custom display elements

  const createCourseGrade = (name, grade, courseI) => {
    let mainDiv = document.createElement("div");
    mainDiv.classList.add("course-grade-element");
    let nameDiv = document.createElement("div");
    nameDiv.innerText = name;
    nameDiv.classList.add("course-grade-name");
    let gradeDiv = document.createElement("div");
    gradeDiv.innerText = grade;
    gradeDiv.classList.add("course-grade-grade");
    mainDiv.appendChild(gradeDiv);
    mainDiv.appendChild(nameDiv);
    course_grades.appendChild(mainDiv);

    mainDiv.addEventListener("click", () => {
      loadAssignments(courses[courseI]);
    });
  };

  const createAssignmentGrade = (name, grade, url = undefined) => {
    let mainDiv = document.createElement("div");
    mainDiv.classList.add("assignment-grade-element");
    let nameDiv = document.createElement(url ? "a" : "div");
    nameDiv.innerText = name;
    nameDiv.classList.add("assignment-grade-name");
    if (url) nameDiv.href = url;
    let gradeDiv = document.createElement("div");
    gradeDiv.innerText = grade;
    gradeDiv.classList.add("assignment-grade-grade");
    mainDiv.appendChild(gradeDiv);
    mainDiv.appendChild(nameDiv);
    assignment_grades.appendChild(mainDiv);
  };

  // Main loading and display

  const displayGrades = () => {
    course_grades.innerHTML = "";
    for (const grade of grades) {
      createCourseGrade(grade.courseName, grade.grade, grade.courseI);
    }
  };

  const displayAssignments = (s, id) => {
    if (assignmentShowID !== id) return;

    categoryTypes = {};
    try {
      categoryTypes = GM_getValue("sg_category_types") ?? {};
    } catch (e) {}
    assignment_grades.innerHTML = "";
    let calculated_grade_title =
      document.getElementById("calculated-grade-title") ??
      document.createElement("div");
    let calculated_grade =
      document.getElementById("calculated-grade") ??
      document.createElement("div");
    let calculated_grade_error =
      document.getElementById("calculated-grade-error") ??
      document.createElement("div");
    calculated_grade_title.id = "calculated-grade-title";
    calculated_grade.id = "calculated-grade";
    calculated_grade_error.id = "calculated-grade-error";
    calculated_grade_title.innerText = "Calculated Grade";
    calculated_grade_error.innerText =
      "Category grades not matching with Schoology? Try switching the weighting method (Percentage or Points) for individual categories.";
    assignment_div.insertBefore(calculated_grade_title, assignment_grades);
    assignment_div.insertBefore(calculated_grade, assignment_grades);
    assignment_div.insertBefore(calculated_grade_error, assignment_grades);
    const categories = [];
    if (!s.grading_category[s.grading_category.length - 1].created) {
      s.grading_category.push({
        id: undefined,
        title: "Other",
        created: true,
      });
    }
    let totalWeight = 0;
    for (const gc of s.grading_category) {
      if (gc.weight) totalWeight += gc.weight;
    }

    for (let i = 0; i < s.grading_category.length; i++) {
      const gc = s.grading_category[i];
      let weight = undefined;
      if (totalWeight === 100) {
        if (s.grading_category[i].weight)
          weight = s.grading_category[i].weight / totalWeight;
        else weight = 0;
      }
      const div = document.createElement("div");
      const title = document.createElement("div");
      const titlediv = document.createElement("div");
      const wdiv = document.createElement("div");
      const methoddiv = document.createElement("div");
      wdiv.classList.add("categoryweight");
      wdiv.innerText = weight
        ? "Weight: " + +parseFloat(weight * 100).toFixed(2) + "%"
        : "";
      methoddiv.classList.add("categoryweightmethod");
      methoddiv.classList.add("nosel");
      methoddiv.innerText =
        (categoryTypes[gc.id] ?? 2) == 1 ? "Percentage" : "Points";

      let id = gc.id;
      methoddiv.addEventListener("click", () => {
        categoryTypes[id] = (categoryTypes[gc.id] ?? 2) == 1 ? 2 : 1;
        methoddiv.innerText =
          categoryTypes[gc.id] == 1 ? "Percentage" : "Points";
        storeCategoryDetails();
        updateGrades(s, categories);
      });

      title.classList.add("categorytitle");
      titlediv.classList.add("categorytitlediv");
      div.classList.add("categorydiv");
      div.style.zIndex = i.toString();
      title.innerText = gc.title;
      assignment_grades.appendChild(div);
      div.appendChild(titlediv);
      titlediv.appendChild(methoddiv);
      titlediv.appendChild(wdiv);
      titlediv.appendChild(title);
      categories.push({
        id: gc.id,
        title: gc.title,
        div: div,
        weight: weight,
        totPts: 0,
        maxPts: 0,
      });
    }

    for (let i = 0; i < assignments.length; i++) {
      const a = assignments[i];
      const div = document.createElement("div");
      const grade = document.createElement("div");
      const gradeMax = document.createElement("div");
      const gradeInput = document.createElement("input");
      const resetGrade = document.createElement("div");
      const reloadName = document.createElement("div");
      let assignmentName;
      if (a.url) {
        assignmentName = document.createElement("a");
        assignmentName.href = a.url;
        assignmentName.target = "_blank";
      } else {
        assignmentName = document.createElement("div");
      }
      div.classList.add("assignment-grade-element");
      grade.classList.add("assignment-grade-grade");
      gradeMax.classList.add("assignment-grade-max");
      gradeInput.classList.add("assignment-grade-input");
      resetGrade.classList.add("assignment-grade-reset");
      assignmentName.classList.add("assignment-grade-name");
      reloadName.classList.add("assignment-grade-reload");

      reloadName.innerText = String.fromCodePoint(8635); // ↻
      gradeMax.innerText = "/ " + (a.rawGrade.max ?? "---");
      gradeInput.value = a.rawGrade.pts ?? "";
      gradeInput.setAttribute("placeholder", "---");
      resetGrade.innerText = String.fromCodePoint(8635); // ↻

      if (a.assignmentName == undefined || a.assignmentName == "le>")
        assignmentName.innerText = "unknown";
      else assignmentName.innerText = a.assignmentName;
      const cat = categories.find((e, i) => {
        return e.id === a.category_id || i == categories.length - 1;
      });

      cat.div.appendChild(div);
      reloadName.addEventListener("click", () => {
        assignmentName.innerText = "";
        getAssignmentTitles([a], s, id, true);
      });

      gradeInput.addEventListener("input", () => {
        updateGrades(s, categories);
      });

      resetGrade.addEventListener("click", () => {
        gradeInput.value = a.rawGrade.pts ?? "";
        updateGrades(s, categories);
      });

      div.appendChild(reloadName);
      div.appendChild(assignmentName);
      div.appendChild(resetGrade);
      div.appendChild(grade);
      grade.appendChild(gradeInput);
      grade.appendChild(gradeMax);
      a.gradeInput = gradeInput;
      a.resetGrade = resetGrade;
    }
    updateGrades(s, categories);
    //        for (const assignment of assignments) {
    //            createAssignmentGrade(assignment.assignmentName, assignment.grade, assignment.url);
    //        }
  };

  const updateGrades = (s, categories, reset = false) => {
    for (const c of categories) {
      c.totPts = 0;
      c.maxPts = 0;
      c.calculation_type = categoryTypes[c.id] ?? 2;
    }

    for (let i = 0; i < assignments.length; i++) {
      const a = assignments[i];
      const cat = categories.find((e, i) => {
        return e.id === a.category_id || i == categories.length - 1;
      });
      const pts = reset ? a.rawGrade.pts : parseFloat(a.gradeInput.value);
      if (reset) a.gradeInput.value = a.rawGrade.pts ?? "";
      if (!isNaN(pts) && pts !== null) {
        if (cat.calculation_type == 2) {
          cat.totPts += pts * a.factor;
          cat.maxPts += a.rawGrade.max * a.factor;
        } else {
          cat.totPts += (pts / a.rawGrade.max) * a.factor;
          cat.maxPts += a.factor;
        }
      }
      if (
        (isNaN(pts) && a.rawGrade.pts !== null) ||
        (!isNaN(pts) && pts !== a.rawGrade.pts)
      ) {
        a.resetGrade.style.display = "block";
      } else {
        a.resetGrade.style.display = "none";
      }
    }

    let calcGrade;
    if (categories[0].weight !== undefined) {
      let totPer = 0;
      let outOf = 1;
      for (const gc of categories) {
        if (gc.maxPts === 0) outOf -= gc.weight;
      }
      for (const gc of categories) {
        if (gc.maxPts !== 0) {
          totPer += ((gc.weight / outOf) * gc.totPts) / gc.maxPts;
        }
      }
      calcGrade = totPer;
    } else {
      let totPts = 0,
        maxPts = 0;
      for (const gc of categories) {
        totPts += gc.totPts;
        maxPts += gc.maxPts;
      }
      calcGrade = totPts / maxPts;
    }
    for (let i = 0; i < s.grading_category.length; i++) {
      const points =
        categories[i].div.firstElementChild.getElementsByClassName(
          "categorypoints"
        )[0] ?? document.createElement("div");
      const percent =
        categories[i].div.firstElementChild.getElementsByClassName(
          "categorypercent"
        )[0] ?? document.createElement("div");
      points.innerText =
        (Number.isInteger(categories[i].totPts)
          ? categories[i].totPts
          : categories[i].totPts.toFixed(2)) +
        "/" +
        (Number.isInteger(categories[i].maxPts)
          ? categories[i].maxPts
          : categories[i].maxPts.toFixed(2));
      percent.innerText = roundGrade(
        (categories[i].totPts / categories[i].maxPts) * 100
      );
      points.classList.add("categorypoints");
      percent.classList.add("categorypercent");
      categories[i].div.firstElementChild.appendChild(percent);
      categories[i].div.firstElementChild.appendChild(points);
    }
    document.getElementById("calculated-grade").innerText = roundGrade(
      calcGrade * 100
    );
  };

  const loadGrades = async () => {
    grades = [];
    courses = [];

    getContent("app-user-info")
      .then(async (v) => {
        const apiID = v.api_uid;
        getContent("users/" + apiID + "/grades").then(async (v) => {
          for (const s of v.section) {
            courses.push(s);
          }

          let request = `<?xml version="1.0" encoding="utf-8" ?><requests>`;
          for (const s of v.section) {
            request += "<request>/v1/sections/" + s.section_id + "</request>";
          }
          request += "</requests>";

          await multiGet(request)
            .then((r) => {
              for (let i = 0; i < r.response.length; i++) {
                const res = r.response[i];
                if (res.response_code === 403) continue;
                else {
                  courses[i].courseName =
                    res.body.course_title + " " + res.body.section_title;
                  grades.push({
                    courseName:
                      res.body.course_title + " " + res.body.section_title,
                    grade: roundGrade(v.section[i].final_grade[0].grade),
                    courseI: i,
                  });
                }
              }
              displayGrades();
            })
            .catch((e) => console.log(e));

          setTitle("Select a Course");
        });
      })
      .catch((e) => {
        if (e === 401) {
          authError();
        } else console.log(e);
      });
  };

  const loadAssignments = async (s) => {
    setTitle("Loading " + s.courseName + "...");
    const thisID = Math.floor(Math.random() * 1000000);
    assignmentShowID = thisID;
    assignments = [];
    assignmentNames = {};
    assignmentFactors = {};
    try {
      assignmentNames = GM_getValue("sg_assignment_names") ?? {};
      assignmentFactors = GM_getValue("sg_assignment_factors") ?? {};
    } catch (e) {}

    for (const p of s.period) {
      p.assignment.sort((a, b) => b.timestamp - a.timestamp);
      for (const a of p.assignment) {
        a.assignmentI = assignments.length;
        a.web_url = a.web_url?.replace("https://app", "https://" + domain);
        if (a.web_url && assignmentNames[a.web_url]) {
          assignments.push({
            grade: formatGrade(a),
            assignmentName: assignmentNames[a.web_url],
            url: a.web_url,
            category_id: a.category_id,
            assignmentI: a.assignmentI,
            factor: assignmentFactors[a.web_url] ?? 1,
            rawGrade: { pts: a.grade, max: a.max_points },
          });
          a.skip = true;
        } else {
          assignments.push({
            grade: formatGrade(a),
            assignmentName: "---",
            category_id: a.category_id,
            assignmentI: a.assignmentI,
            url: a.web_url,
            factor: assignmentFactors[a.web_url] ?? 1,
            rawGrade: { pts: a.grade, max: a.max_points },
          });
          a.skip = false;
        }
      }

      displayAssignments(s, thisID);

      const requests = [];
      for (let i = 0; i < p.assignment.length; i += 45) {
        requests.push(p.assignment.slice(i, i + 45));
      }
      for (const r of requests) {
        let request = `<?xml version="1.0" encoding="utf-8" ?><requests>`;
        const actualI = [];
        for (let i = 0; i < r.length; i++) {
          if (r[i].skip != true) {
            actualI.push(i);
            request += `<request>` + r[i].location + `</request>`;
          }
        }
        request += `</requests>`;
        if (actualI.length === 0) continue;

        const rereq = [];
        await multiGet(request)
          .then((v) => {
            for (let i = 0; i < v.response.length; i++) {
              let ai = actualI[i];
              const res = v.response[i];
              const url = r[ai].web_url;
              if (res.response_code === 403) {
                rereq.push({ assignmentI: r[ai].assignmentI, url: url });
              } else {
                assignments[r[ai].assignmentI].assignmentName = res.body.title;
                if (res.body.factor) {
                  assignments[r[ai].assignmentI].factor = parseFloat(
                    res.body.factor
                  );
                  assignmentFactors[url] = parseFloat(res.body.factor);
                }
                assignments[r[ai].assignmentI].url = url;
                assignmentNames[url] = res.body.title;
              }
            }
          })
          .catch((e) => console.log(e));

        displayAssignments(s, thisID);
        storeAssignmentDetails();
        await getAssignmentTitles(rereq, s, thisID);
      }
    }
    if (thisID == assignmentShowID) {
      setTitle(s.courseName);
    }
  };

  const getAssignmentTitles = (as, s, id, force = false) => {
    return new Promise(async (res, rej) => {
      if (as.length === 0) {
        res();
        return;
      }
      for (const a of as) {
        if (id !== assignmentShowID) break;
        if (assignmentNames[a.url] && !force) {
          assignments[a.assignmentI].assignmentName = assignmentNames[a.url];
          assignments[a.assignmentI].url = a.url;
        } else {
          await getHTML(a.url)
            .then((v) => {
              let i = v.search(/<title>/);
              let j = v.search(/<\/title>/);
              if (i === -1) {
                assignments[a.assignmentI].url = a.url;
              } else {
                const tmp = document.createElement("textarea");
                tmp.innerHTML = v.substring(i + 7, j - 12);

                assignments[a.assignmentI].assignmentName = tmp.value;
                assignmentNames[a.url] = tmp.value;
                assignments[a.assignmentI].url = a.url;
                storeAssignmentDetails();
                displayAssignments(s, id);
                tmp.remove();
              }
            })
            .catch((e) => {});
        }
      }
      storeAssignmentDetails();
      displayAssignments(s, id);
      res();
    });
  };

  start();
})();