Blackboard To-Do List

Create a To-Do List for Blackboard that shows upcoming assignments, grades, submitted status

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Blackboard To-Do List
// @namespace    http://yourwebsite.com
// @version      1.1
// @license MIT
// @description  Create a To-Do List for Blackboard that shows upcoming assignments, grades, submitted status
// @author       Your Name
// @match        https://ccccblackboard.blackboard.com/ultra/course*
// @grant        none
// ==/UserScript==

console.log('%cLoaded Brad\'s To Do List!', 'color: yellow; font-weight: bold;font-size:400%;');
function createMenu() {
    const toDoList = document.getElementById('toDoList');
    if (!toDoList) {
        const courseColumnsCurrent = document.getElementById('course-columns-current');
        courseColumnsCurrent.style.width = '75%';

        // Create a new 'toDoList' div
        const newToDoList = document.createElement('div');
        newToDoList.id = 'toDoList';
        courseColumnsCurrent.insertBefore(newToDoList, courseColumnsCurrent.firstChild);
    }

    const tdl = document.getElementById('toDoList');
    tdl.style.cssText = 'position:absolute;left:79%;top:20%;background:white;border-radius:4px;border:1px solid #cdcdcd;width:4%;height:70%;padding: 5px;overflow-y:scroll;overflow-x:hidden;';
}

async function fetchAssignments() {
    const startSearchDate = new Date().toISOString();
    const endSearchDate = new Date();
    endSearchDate.setDate(endSearchDate.getDate() + 7);
    const endSearchDateISO = endSearchDate.toISOString();

    const response = await fetch(`https://ccccblackboard.blackboard.com/learn/api/v1/calendars/calendarItems?since=${startSearchDate}&until=${endSearchDateISO}`);
    const json = await response.json();
    return json.results.map(e => ({
        class: {
            id: e.calendarId,
            name: e.calendarNameLocalizable.rawValue,
            link: `https://ccccblackboard.blackboard.com/ultra/courses/${e.calendarId}/cl/outline`
        },
        assignmentName: e.title,
        assignmentId: e.itemSourceId,
        assignmentLink: `https://ccccblackboard.blackboard.com/ultra/courses/${e.calendarId}/cl/outline?legacyUrl=~2Fwebapps~2Fcalendar~2Flaunch~2Fattempt~2F_blackboard.platform.gradebook2.GradableItem-${e.itemSourceId}`,
        assignmnetCategoryId: e.dynamicCalendarItemProps.categoryId,
        dueDate: new Date(e.endDate),
        assignedDate: new Date(e.startDate)
    }));
}

async function renderToDoList(assignments) {
    const tdl = document.getElementById('toDoList');
    tdl.innerHTML = `<h1 style='font-family: Open Sans,sans-serif;text-align:center;font-weight:600;'>To Do List</h1>`;


    tdl.innerHTML += "<img id='reloadTDL' style='cursor:pointer;position:absolute;top:3%;' src='https://learn.content.blackboardcdn.com/3900.74.0-rel.32+80961aa/images/ci/ng/small_refresh.gif'/>"

    if(localStorage.completedAssignments?.length==0) {
        localStorage.completedAssignments=JSON.stringify([]);
    }
    tdl.innerHTML+=`<h4>Complete Now</h4>`

    let addedTomorrowSpacer = false;
    for(const assignment of assignments) {
      const className = assignment.class.name.split("_")[0];
      const assName = assignment.assignmentName;
      const linkToClass = assignment.assignmentLink;
      let checked = localStorage.completedAssignments.includes(assName)
      const dueTommorrow = (assignment.dueDate - new Date()) / 1000 / 60 / 60 > 24;
      const grade = await getAssignmentGrade(assignment.class.id, assignment.assignmentId);
      const submitted = grade.toString().includes("Submitted");
      if(!checked && submitted) {
        checked = true;
      }

      if (!addedTomorrowSpacer && dueTommorrow) {
          const tomorrowSpacer = document.createElement('h4');
          tomorrowSpacer.textContent = 'Start Today, Complete Tomorrow';
          tdl.appendChild(tomorrowSpacer);
          addedTomorrowSpacer = true;
      }


      const assignmentBox = document.createElement('div');
      assignmentBox.classList.add('assignmentBox');
      assignmentBox.style.outline = '1px solid #cdcdcd';
      assignmentBox.style.width='98%'
      assignmentBox.style.margin = '8px';
      assignmentBox.style.borderRadius = '3px';
      assignmentBox.style.padding = '3px';
      assignmentBox.style.background="rgb(250,250,250)";

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.id = assName;
      checkbox.value = 'test';
      assignmentBox.appendChild(checkbox);

      const label = document.createElement('label');
      label.htmlFor = assName;
      label.style.maxWidth='90%'

      const anchor = document.createElement('a');
      anchor.href = linkToClass;
      anchor.textContent = assName;

      label.appendChild(anchor);
      assignmentBox.appendChild(label);

      const detailsSpan = document.createElement('span');
      detailsSpan.style.color = 'gray';
      detailsSpan.style.fontSize = '90%';
      detailsSpan.innerHTML = `<br>Due ${formatDateTime(assignment.dueDate)} | ${className} ${grade}`;
      assignmentBox.appendChild(detailsSpan);

      tdl.appendChild(assignmentBox);

      checkbox.checked = checked;
      checkbox.oninput = () => {
          let completed = JSON.parse(localStorage.completedAssignments);

          if (checkbox.checked) {
              completed.push(assName)
          } else {
              completed.splice(completed.indexOf(assName), 1)
          }

          localStorage.completedAssignments = JSON.stringify(completed);
      }
      document.getElementById('reloadTDL').onclick = ()=>{
        main();
      }
  }
}

function formatDateTime(date) {
    const now = new Date();
    const tomorrow = new Date(now);
    tomorrow.setDate(tomorrow.getDate() + 1);

    const hours = date.getHours()%12;
    const minutes = date.getMinutes();

    let formattedDate;

    if (date.toDateString() === now.toDateString()) {
        formattedDate = "";
    } else if (date.toDateString() === tomorrow.toDateString()) {
        formattedDate = "tmrw";
    } else {
        const options = { month: "long", day: "numeric" };
        formattedDate = date.toLocaleDateString(undefined, options);
    }

    const formattedTime = `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}`;
    const ampm = date.getHours() >= 12 ? 'PM' : 'AM';

    return `${formattedDate} at <span style='font-weight:600;'>${formattedTime} ${ampm}</span>   ${calculateTimeDifference(now,date)}`;
}

function calculateTimeDifference(startDate, endDate) {
    const timeDifferenceMs = endDate - startDate;
    const hoursDifference = Math.floor(timeDifferenceMs / (1000 * 60 * 60));
    const minutesDifference = Math.floor((timeDifferenceMs / 60000) % 60);

    return hoursDifference >= 8 ? '' : `(${hoursDifference || minutesDifference} ${hoursDifference ? 'hrs' : 'min'})`;
}
const badgeCss = 'position:absolute;right:2%;display: inline-block;width:74px;text-align:center;border-radius:5px;padding:2px;font-size:90%;'
async function getAssignmentGrade(classId, assId) {
    let ungraded = `<span style='${badgeCss}background:#d1d1d1;color:#555;'>Incomplete</span>`;
    let submittedText = `<span style='${badgeCss}background:#95deb0;color:black;'>Submitted</span>`;

    try {
        const response = await fetch(`https://ccccblackboard.blackboard.com/learn/api/v1/courses/${classId}/gradebook/grades?limit=100&userId=${window.__initialContext.user.id}`);
        const content = await response.json();
        const result = content.results.find(a => a.columnId === assId);
        return result ? doColorGrade(result.displayGrade.score) : ungraded;
    } catch (e) {
        try{
          const response = await fetch(`https://ccccblackboard.blackboard.com/webapps/calendar/launch/attempt/_blackboard.platform.gradebook2.GradableItem-${assId}`);
          const content = await response.text();
          const submitted = content.includes("Review Submission History");
          return submitted ? submittedText : ungraded;
        } catch(a) {
          return ungraded;
        }
    }
}
function doColorGrade(grade) {
  let color = "#ff3b30";
  if(grade>=90) color="#39e379";
  else if(grade>=80) color="#cdee4b";
  else if(grade>=70) color='#ffe300';
  else if(grade>=60) color='#ff9600';

  return `<span style='${badgeCss}background:${color};color:#000;'>${grade.toFixed(2)+"%"}</span>`;
}
let loadedMenu = false;
setInterval(()=>{
  if(document.getElementsByClassName('element-details summary').length<1 || location.href!="https://ccccblackboard.blackboard.com/ultra/course") {
    loadedMenu = false;
    return;
  }

  if(location.href=="https://ccccblackboard.blackboard.com/ultra/course" && !loadedMenu) {
    loadedMenu = true;
    main();
  }
},100)

async function main() {
    createMenu();
    const assignments = await fetchAssignments();

    const sortedAssignments = assignments.filter(assignment => {
        const hours = (assignment.dueDate - new Date()) / 1000 / 60 / 60;
        return hours < 48 && hours > 0;
    }).sort((a, b) => (a.dueDate - b.dueDate) / 1000 / 60 / 60);

    renderToDoList(sortedAssignments);
}