const domain = window.location.origin;
const current_page = window.location.pathname;
function getCurrentCourseId() {
const match = current_page.match(/^\/courses\/(\d+)(?:\/|$)/);
return match ? parseInt(match[1]) : null;
}
function getSidebarLayoutMode() {
if (current_page.match(/^\/courses\/(\d+)(?:\/|$)/)) return "course";
if (isProfilePage()) return "course";
if (current_page === "/courses" || current_page === "/courses/") return "dash";
if (current_page === "/" || current_page === "") return "dash";
return "dash";
}
function isGradesPage() {
return /^\/courses\/\d+\/grades(?:\/|$)/.test(current_page);
}
function isCoursesIndexPage() {
return /^\/courses\/?$/.test(current_page);
}
function isGroupsIndexPage() {
return /^\/groups\/?$/.test(current_page);
}
function isConversationsPage() {
return /^\/conversations(?:\/|$)/.test(current_page);
}
function isAccountsPage() {
return /^\/accounts(?:\/|$)/.test(current_page);
}
function isProfilePage() {
return /^\/profile(?:\/|$)/.test(current_page);
}
function getSubmissionAssignmentLink() {
const match = current_page.match(/^\/courses\/(\d+)\/assignments\/(\d+)\/submissions\/(\d+)(?:\/|$)/);
if (!match) return null;
return `${domain}/courses/${match[1]}/assignments/${match[2]}/`;
}
let submissionPageButtonObserver = null;
let profileLogoutButtonObserver = null;
function addSubmissionPageButton() {
const assignmentLink = getSubmissionAssignmentLink();
if (!assignmentLink) return;
const content = document.getElementById("content");
if (!content || content.querySelector("#canvasrefined-assignment-return")) return;
makeElement("a", content, {
id: "canvasrefined-assignment-return",
className: "canvasrefined-custom-btn",
href: assignmentLink,
textContent: "Back to Assignment",
style: "display:inline-flex;align-items:center;justify-content:center;align-self:flex-start;margin:0 0 12px 0;padding:10px 14px;text-decoration:none;font-weight:700;",
}, true);
}
function addProfileLogoutPageButton() {
if (!isProfilePage()) return;
const content = document.getElementById("content");
if (!content || content.querySelector("#canvasrefined-profile-logout")) return;
makeElement("a", content, {
id: "canvasrefined-profile-logout",
className: "canvasrefined-custom-btn",
href: `${domain}/logout`,
textContent: "Logout",
style: "display:inline-flex;align-items:center;justify-content:center;align-self:flex-start;margin:0 0 12px 0;padding:10px 14px;text-decoration:none;font-weight:700;",
}, true);
}
function ensureProfileLogoutPageButton() {
if (!isProfilePage()) return false;
const content = document.getElementById("content");
if (!content) return false;
if (content.querySelector("#canvasrefined-profile-logout")) return true;
addProfileLogoutPageButton();
return Boolean(content.querySelector("#canvasrefined-profile-logout"));
}
function watchProfileLogoutPageButton() {
if (!isProfilePage()) return;
if (ensureProfileLogoutPageButton()) return;
if (profileLogoutButtonObserver) return;
profileLogoutButtonObserver = new MutationObserver(() => {
if (ensureProfileLogoutPageButton() && profileLogoutButtonObserver) {
profileLogoutButtonObserver.disconnect();
profileLogoutButtonObserver = null;
}
});
profileLogoutButtonObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => {
if (profileLogoutButtonObserver) {
profileLogoutButtonObserver.disconnect();
profileLogoutButtonObserver = null;
}
}, 10000);
}
function ensureSubmissionPageButton() {
const assignmentLink = getSubmissionAssignmentLink();
if (!assignmentLink) return false;
const content = document.getElementById("content");
if (!content) return false;
if (content.querySelector("#canvasrefined-assignment-return")) return true;
addSubmissionPageButton();
return Boolean(content.querySelector("#canvasrefined-assignment-return"));
}
function watchSequenceFooter() {
if (!isAssignmentPage()) return;
if (removeSequenceFooter()) return;
if (sequenceFooterObserver) return;
sequenceFooterObserver = new MutationObserver(() => {
if (removeSequenceFooter() && sequenceFooterObserver) {
sequenceFooterObserver.disconnect();
sequenceFooterObserver = null;
}
});
sequenceFooterObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => {
if (sequenceFooterObserver) {
sequenceFooterObserver.disconnect();
sequenceFooterObserver = null;
}
}, 10000);
}
function watchSubmissionPageButton() {
if (!getSubmissionAssignmentLink()) return;
if (ensureSubmissionPageButton()) return;
if (submissionPageButtonObserver) return;
submissionPageButtonObserver = new MutationObserver(() => {
if (ensureSubmissionPageButton() && submissionPageButtonObserver) {
submissionPageButtonObserver.disconnect();
submissionPageButtonObserver = null;
}
});
submissionPageButtonObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => {
if (submissionPageButtonObserver) {
submissionPageButtonObserver.disconnect();
submissionPageButtonObserver = null;
}
}, 10000);
}
async function getActiveCustomBackground() {
const syncOpts = await chrome.storage.sync.get([
"customBackgroundDaily",
"customBackgroundNasaDaily",
"customBackgroundLink",
"customBackgroundScale",
]);
console.log("[CanvasRefined] getActiveCustomBackground:", syncOpts);
if (syncOpts.customBackgroundNasaDaily === true) {
console.log("[CanvasRefined] Using NASA APOD");
return await getNasaDailyBackground();
}
if (syncOpts.customBackgroundDaily === true) {
console.log("[CanvasRefined] Using Wikimedia Featured");
const dailyPreset = await getDailyBackgroundPreset();
if (dailyPreset) {
console.log("[CanvasRefined] Wikimedia URL:", dailyPreset.url);
return {
url: dailyPreset.url,
scale: dailyPreset.scale,
};
}
console.log("[CanvasRefined] Wikimedia returned null");
}
if (syncOpts.customBackgroundLink && syncOpts.customBackgroundLink !== "") {
console.log("[CanvasRefined] Using custom link");
return {
url: syncOpts.customBackgroundLink,
scale: syncOpts.customBackgroundScale || 100,
};
}
console.log("[CanvasRefined] No custom background");
return null;
}
async function getDailyBackgroundPreset() {
const today = new Date();
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
const cacheKey = `picsum_daily_${dateStr}`;
const cached = await chrome.storage.local.get(cacheKey);
if (cached[cacheKey]) return cached[cacheKey];
const url = `https://picsum.photos/seed/${dateStr}/1920/1080`;
const result = { url, scale: 100 };
await chrome.storage.local.set({ [cacheKey]: result });
return result;
}
async function getNasaDailyBackground() {
try {
return await chrome.runtime.sendMessage({ type: "getNasaBackground" });
} catch (error) {
console.error("[CanvasRefined] Failed to fetch NASA APOD:", error);
return null;
}
}
let nasaInfoOverlayEl = null;
function isDashboardPage() {
return !!document.querySelector("#DashboardCard_Container");
}
function createNasaInfoOverlay() {
if (options.customBackgroundNasaDaily !== true) return;
if (nasaInfoOverlayEl || !isDashboardPage()) return;
const contentMain = document.querySelector("#content.ic-Layout-contentMain, .ic-Layout-contentMain");
if (!contentMain) return;
if (getComputedStyle(contentMain).position === "static") {
contentMain.style.position = "relative";
}
nasaInfoOverlayEl = document.createElement("div");
nasaInfoOverlayEl.id = "canvasrefined-nasa-info-overlay";
nasaInfoOverlayEl.style.cssText = "position:absolute;right:24px;bottom:24px;z-index:9999;";
nasaInfoOverlayEl.innerHTML = `
`;
const icon = nasaInfoOverlayEl.querySelector("#nasa-info-icon");
const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel");
const populatePanel = async () => {
const dateStr = new Date().toISOString().slice(0, 10);
const cacheKey = `nasa_apod_${dateStr}`;
const cached = await chrome.storage.local.get(cacheKey);
const metaDate = cached[cacheKey]?.date || dateStr;
const metadataKey = `nasa_apod_meta_${metaDate}`;
const metadata = await chrome.storage.local.get(metadataKey);
const meta = metadata[metadataKey];
if (!meta) return false;
document.getElementById("nasa-info-title").textContent = meta.title || "";
document.getElementById("nasa-info-date").textContent = `Date: ${meta.date}`;
document.getElementById("nasa-info-credit").textContent = meta.copyright ? `Credit: ${meta.copyright}` : "";
document.getElementById("nasa-info-explanation").textContent = meta.explanation || "No description available.";
return true;
};
let pinned = false;
const showPanel = async () => {
if (pinned) return;
if (await populatePanel()) panel.style.display = "block";
};
const hidePanel = () => {
if (pinned) return;
panel.style.display = "none";
};
const togglePanel = async () => {
if (pinned) {
pinned = false;
panel.style.display = "none";
} else {
pinned = true;
if (await populatePanel()) panel.style.display = "block";
}
};
icon.addEventListener("mouseenter", showPanel);
icon.addEventListener("mouseleave", hidePanel);
panel.addEventListener("mouseenter", showPanel);
panel.addEventListener("mouseleave", hidePanel);
icon.addEventListener("click", togglePanel);
contentMain.appendChild(nasaInfoOverlayEl);
}
function removeNasaInfoOverlay() {
if (nasaInfoOverlayEl) {
nasaInfoOverlayEl.remove();
nasaInfoOverlayEl = null;
}
}
function getSidebarStateMode(mode = getSidebarLayoutMode()) {
return mode === "course" ? "course" : "dashboard";
}
function getSidebarStateKey(mode = getSidebarLayoutMode()) {
return `better_sidebar_expanded_${getSidebarStateMode(mode)}`;
}
async function getSidebarExpandedState(mode = getSidebarLayoutMode()) {
const key = getSidebarStateKey(mode);
const result = await chrome.storage.local.get(key);
return result[key] ?? false;
}
function setSidebarExpandedState(mode, expanded) {
chrome.storage.local.set({ [getSidebarStateKey(mode)]: expanded });
}
let assignments = null;
let grades = null;
let announcements = [];
let completed = [];
let assignmentsDue = [];
let options = {};
let timeCheck = null;
let reminderCheck = null;
let betterSidebarLoading = false;
let dashboardReadyTimer = null;
//let assignmentData = null;
/*
Start
*/
/*
// only works if a course has no quizzes...
function getClassAverages() {
if (true) { // check if option is enabled
let match = current_page.match(/courses\/(?\d*)\/grades/);
if (match) {
let course_grades = getData(`${domain}/api/v1/courses/${match.groups.id}/assignments?include[]=score_statistics&include[]=submission`);
let course_quizzes = getData(`${domain}/api/v1/courses/${match.groups.id}/quizzes`);
let course_groups = getData(`${domain}/api/v1/courses/${match.groups.id}/assignment_groups`);
course_grades.then(grades => {
course_groups.then(groups => {
course_quizzes.then(quizzes => {
let total_weight = 0;
let total_points = 0;
let weights = {};
groups.forEach(group => {
weights[group.id] = group.group_weight;
total_weight += group.group_weight;
});
groups.forEach(group => {
weights[group.id] = total_weight === 0 ? 1 : weights[group.id] / total_weight;
});
let min = 0, lowq = 0, mean = 0, median = 0, upq = 0, max = 0, earned = 0;
grades.forEach(grade => {
if (!grade.score_statistics) return;
console.log("\nthis:", grade.name, grade.score_statistics.lower_q, grade.score_statistics.mean, grade.score_statistics.upper_q);
console.log("totals:", lowq, upq, total_points);
min += grade.score_statistics.min * weights[grade.assignment_group_id];
lowq += grade.score_statistics.lower_q * weights[grade.assignment_group_id];
mean += grade.score_statistics.mean * weights[grade.assignment_group_id];
median += grade.score_statistics.median * weights[grade.assignment_group_id];
upq += grade.score_statistics.upper_q * weights[grade.assignment_group_id];
max += grade.score_statistics.max * weights[grade.assignment_group_id];
total_points += grade.points_possible * weights[grade.assignment_group_id];
earned += grade.submission.score * weights[grade.assignment_group_id];
});
course_quizzes.forEach(quiz => {
// is it even possible to get quiz statistics?
});
// absolute minimum is if the same student got the lowest score on every assignment
// absolute maximum is if the same student got the highest score on every assignment
// it doesn't really tell you much because both are unlikely
console.log("\nabsolute minimum:", min / total_points, "\nabsolute maximum:", max / total_points, "\nlower quartile:", lowq / total_points, "\nmean:", mean / total_points, "\nupper quartile:", upq / total_points);
min = (min / total_points);
lowq = (lowq / total_points);
mean = (mean / total_points);
upq = (upq / total_points);
max = (max / total_points);
earned = (earned / total_points);
console.log(weights);
const width = 150;
let inner = `| Class Averages | |
|---|
| Mean: ${(mean * 100).toFixed(2)} | Upper Quartile: ${(upq * 100).toFixed(2)} | Lower Quartile: ${(lowq * 100).toFixed(2)} | |
| `;
makeElement("tr", document.querySelector("#grades_summary tbody"), { "innerHTML": inner });
});
});
});
}
}
}
*/
/*
Todo Reminders
*/
const canvas_svg = ``;
async function insertReminders(reminders) {
const toAdd = [];
const storage = await chrome.storage.sync.get("reminders");
// overrides = if theres a item that needs to update, but already exists
let overrides = false;
for (const insert of reminders) {
let found = false;
for (let i = 0; i < storage["reminders"].length; i++) {
// check if item was recently submitted
if (insert.c === -1 && insert.h === storage["reminders"][i].h) {
overrides = true;
storage["reminders"][i] = insert;
} else if (insert.h === storage["reminders"][i].h) {
found = true;
}
}
if (found === false) toAdd.push(insert);
}
if (toAdd.length > 0 || overrides === true) chrome.storage.sync.set({ "reminders": [...storage["reminders"], ...toAdd] });
}
async function hideReminder(href) {
const storage = await chrome.storage.sync.get("reminders");
for (let i = 0; i < storage["reminders"].length; i++) {
if (storage["reminders"][i]["h"] === href) {
storage["reminders"][i]["c"]++;
chrome.storage.sync.set({ "reminders": storage["reminders"] });
break;
}
}
}
function createReminder(reminder, location) {
const remaining = getRelativeDate(new Date(reminder.d));
const wrapper = makeElement("div", location, { "className": "canvasrefined-reminder-wrapper" });
const container = makeElement("div", wrapper, { "className": "canvasrefined-reminder-container" });
const svg = makeElement("div", container, { "innerHTML": canvas_svg });
const content = makeElement("a", container, { "className": "canvasrefined-reminder-content", "href": reminder.h, "target": "_blank" });
const title = makeElement("h2", content, { "className": "canvasrefined-reminder-title", "textContent": reminder.t });
const due = makeElement("p", content, { "className": "canvasrefined-reminder-due", "textContent": `Assignment due in ${remaining.time}` });
const hidebtn = makeElement("btn", wrapper, { "className": "canvasrefined-reminder-hide", "textContent": "x" });
hidebtn.addEventListener("click", () => {
hideReminder(reminder.h);
wrapper.remove();
});
return container;
}
async function reminderWatch() {
const sync = await chrome.storage.sync.get("remind");
if (sync["remind"] !== true) {
if (document.getElementById("canvasrefined-reminders")) document.getElementById("canvasrefined-reminders").style.display = "none";
return;
}
const container = document.getElementById("canvasrefined-reminders") || makeElement("div", document.body, { "id": "canvasrefined-reminders" });
container.style.display = "flex";
container.textContent = "";
const alertPeriod = 1000 * 60 * 60 * 6; // 6 hours
const alertPeriod2 = 1000 * 60 * 60 * 2; // 2 hours
const storage = await chrome.storage.sync.get(["reminders", "reminder_count"]);
const now = (new Date()).getTime();
storage["reminders"].forEach((reminder, index) => {
if (reminder.d < now) {
storage["reminders"].splice(index, 1);
} else if ((reminder.c == 0 && reminder.d < now + alertPeriod) || (reminder.c == 1 && reminder.d < now + alertPeriod2)) {
createReminder(reminder, container);
}
});
chrome.storage.sync.set({ "reminders": storage["reminders"] });
}
function updateReminders() {
if (!assignments || typeof assignments.then !== "function") return;
const fiveDays = 1000 * 60 * 60 * 24 * 5;
const now = (new Date()).getTime();
const list = [];
assignments.then(data => {
data.forEach(item => {
const due = (new Date(item.plannable_date)).getTime();
if (item.plannable_type === "announcement") return;
if (due < now) return;
if (due > now + fiveDays * 2) return;
// { due, title, href, hide count }
// hide count of -1 indicates the item has a submission
list.push({ "d": due, "t": item.plannable.title, "h": domain + item.html_url, "c": item?.submissions?.submitted || false ? -1 : 0 });
});
insertReminders(list);
});
}
function showExampleReminder() {
const location = document.getElementById("canvasrefined-reminders") || makeElement("div", document.body, { "id": "canvasrefined-reminders" });
if (options.remind !== true) {
location.remove();
return;
}
location.textContent = "";
const example = createReminder({ "d": new Date(), "t": "This is an example reminder", }, location);
example.querySelector(".canvasrefined-reminder-due").textContent = "This notification will pop up in other pages to remind you of incomplete assignments that are due in less than 6 hours." /*It will notify again at 2 hours if the 'Remind 2x' option is on."*/;
}
// async function ScheduledReminderCheck() {
// let date = new Date();
// let currentHour = date.getHours();
// let currentMinute = date.getMinutes();
// if (options.scheduledReminderTime) {
// let [hour, minute] = options.scheduledReminderTime.split(":");
// if (parseInt(hour) == currentHour && parseInt(minute) == currentMinute) {
// const container = document.getElementById("canvasrefined-reminders") || makeElement("div", document.body, { "id": "canvasrefined-reminders" });
// container.style.display = "flex";
// container.textContent = "";
// const storage = await chrome.storage.sync.get("reminders");
// const now = (new Date()).getTime();
// storage["reminders"].forEach(reminder => {
// if (reminder.d >= now) {
// createReminder(reminder, container);
// }
// });
// }
// }
// }
// function toggleScheduledReminders() {
// clearInterval(reminderCheck);
// if (options.scheduledReminder !== true) return;
// ScheduledReminderCheck();
// reminderCheck = setInterval(ScheduledReminderCheck, 60000);
// }
isDomainCanvasPage();
function isDomainCanvasPage() {
chrome.storage.sync.get(['custom_domain', 'dark_mode', 'dark_preset', 'device_dark', 'remind'/*, 'scheduledReminder', 'scheduledReminderTime'*/], result => {
options = result;
if (result.custom_domain.length && result.custom_domain[0] !== "") {
for (let i = 0; i < result.custom_domain.length; i++) {
if (domain.includes(result.custom_domain[i])) {
startExtension();
return;
}
}
// if the code reaches this point, its not a canvas page so run the reminders
setTimeout(reminderWatch, 2000);
setInterval(reminderWatch, 60000);
// toggleScheduledReminders();
// turn the reminders on/off if the option is changed
chrome.storage.onChanged.addListener((changes) => {
Object.keys(changes).forEach(key => {
if (key === "remind") reminderWatch();
if (key === "scheduledReminder" || key === "scheduledReminderTime") {
options[key] = changes[key].newValue;
// toggleScheduledReminders();
}
})
})
} else {
setupCustomURL();
}
});
}
function startExtension() {
// Remove footer robustly - run first so a crash below can't block it
const removeFooter = () => {
const footer = document.querySelector('footer#footer.ic-app-footer, footer#footer');
if (footer) footer.remove();
};
removeFooter();
const footerObserver = new MutationObserver(() => {
removeFooter();
});
footerObserver.observe(document.documentElement, { childList: true, subtree: true });
toggleDarkMode();
chrome.storage.sync.get(["better_sidebar", "sidebar_scale"], result => {
options = { ...options, ...result };
ensureBetterSidebar();
});
chrome.storage.sync.get(null, result => {
options = { ...options, ...result };
toggleAutoDarkMode();
// toggleScheduledReminders();
getApiData();
checkDashboardReady();
loadCustomFont();
applyAestheticChanges();
changeFavicon();
updateReminders();
applyCustomBackground();
ensureBetterSidebar();
watchSequenceFooter();
watchSubmissionPageButton();
watchProfileLogoutPageButton();
//getClassAverages();
setTimeout(() => runDarkModeFixer(false), 800);
setTimeout(() => runDarkModeFixer(false), 4500);
});
chrome.runtime.onMessage.addListener(recieveMessage);
chrome.storage.onChanged.addListener(applyOptionsChanges);
console.log("Canvas Refined - running");
}
function applyOptionsChanges(changes) {
let rewrite = {};
Object.keys(changes).forEach(key => {
rewrite[key] = changes[key].newValue;
});
options = { ...options, ...rewrite };
// when an option is updated it will call the necessary functions again
// so any changes made in the menu no longer require a refresh to apply
Object.keys(changes).forEach(key => {
console.log(key + " changed");
switch (key) {
case "dark_mode":
case "dark_preset":
case "device_dark":
toggleDarkMode();
break;
case "auto_dark":
case "auto_dark_start":
case "auto_dark_end":
toggleAutoDarkMode();
break;
case "gradient_cards":
changeGradientCards();
break;
case "dashboard_notes":
loadDashboardNotes();
break;
case "dashboard_grades":
case "grade_hover":
if (!grades) getGrades();
insertGrades();
break;
case "assignments_due":
case "num_assignments":
if (!assignments) getAssignments();
if (
document.querySelectorAll(".canvasrefined-card-assignment")
.length === 0
)
setupCardAssignments();
loadCardAssignments();
break;
case "custom_assignments":
case "assignment_date_format":
case "card_overdues":
case "relative_dues":
cardAssignments = preloadAssignmentEls();
loadCardAssignments();
break;
case "equal_height_cards":
// No need to rebuild the assignment rows — just stretch (or reset)
// the card heights in place for a snappy toggle.
equalizeCardHeights();
break;
case "custom_cards":
case "custom_cards_2":
case "custom_cards_3":
customizeCards();
break;
case "todo_hr24":
case "todo_separate_scrollbar":
case "num_todo_items":
case "hover_preview":
// case "todo_overdues":
case "todo_hide_feedback":
case "todo_full_height":
case "custom_cards_3":
moreAnnouncementCount = 0;
moreAssignmentCount = 0;
// loadBetterTodo();
clearTodoList();
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
break;
case "gpa_calc":
case "gpa_calc_prepend":
case "gpa_calc_weighted":
case "gpa_calc_cumulative":
if (!grades) getGrades();
setupGPACalc();
break;
case "gpa_calc_bounds":
calculateGPA2();
break;
case "custom_font":
loadCustomFont();
break;
case "remlogo":
case "disable_color_overlay":
case "condensed_cards":
case "hide_feedback":
case "full_width":
case "custom_styles":
applyAestheticChanges();
break;
case "customBackgroundScale":
applyCustomBackground();
break;
case "customBackgroundDaily":
applyCustomBackground();
removeNasaInfoOverlay();
break;
case "customBackgroundNasaDaily":
applyCustomBackground();
if (options.customBackgroundNasaDaily === true) {
createNasaInfoOverlay();
} else {
removeNasaInfoOverlay();
}
break;
case "fitImageToScreen":
applyCustomBackground();
break;
// case "show_updates":
// showUpdateMsg();
// break;
case "remind":
showExampleReminder();
break;
// case "scheduledReminder":
// case "scheduledReminderTime":
// toggleScheduledReminders();
// break;
case "imageSize":
case "cardRoundness":
case "cardSpacing":
case "cardWidth":
case "cardHeight":
case "customCardStyles":
applyAestheticChanges();
break;
case "customBackgroundLink":
applyCustomBackground();
break;
case "better_todo":
if (options.better_todo) {
setupBetterTodo();
} else {
window.location.reload();
}
break;
case "todo_progress_rings": {
// toggle progress rings immediately
const placeholder = document.getElementById("better-todo-progress-placeholder");
if (!placeholder) break;
if (changes[key].newValue === true || changes[key].newValue === undefined) {
if (typeof assignments?.then === 'function') {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
? data.filter(item => {
const itemCourseId = parseInt(item.course_id || item.context_id || item?.plannable?.course_id);
return itemCourseId === courseId;
})
: data;
renderProgressRings(placeholder, scopedData);
});
}
} else {
placeholder.innerHTML = "";
}
break;
}
case "better_sidebar":
if (options.better_sidebar) {
ensureBetterSidebar();
} else {
resetBetterSidebarLayout();
}
break;
case "sidebar_scale": {
const existingSidebar = document.getElementById("better-sidebar-container");
if (existingSidebar) {
const expander = existingSidebar.querySelector(".better-sidebar-expander");
updateSidebar(existingSidebar.dataset.expanded === "true", existingSidebar, expander);
}
break;
}
}
});
}
function resetBetterSidebarLayout() {
document.getElementById("header")?.style.removeProperty("display");
document.querySelector(".ic-Layout-wrapper")?.style.removeProperty("margin-left");
document.querySelector("#main")?.style.removeProperty("margin-left");
document.querySelector(".ic-app-nav-toggle-and-crumbs")?.style.removeProperty("display");
document.getElementById("not_right_side")?.style.removeProperty("display");
document.getElementById("not_right_side")?.style.removeProperty("flex");
document.getElementById("not_right_side")?.style.removeProperty("min-width");
document.getElementById("right-side-wrapper")?.style.removeProperty("flex");
document.getElementById("right-side-wrapper")?.style.removeProperty("width");
document.getElementById("right-side-wrapper")?.style.removeProperty("max-width");
document.querySelector(".ic-Layout-contentWrapper")?.style.removeProperty("display");
document.querySelector(".ic-Layout-contentWrapper")?.style.removeProperty("align-items");
document.querySelector(".ic-Layout-contentWrapper")?.style.removeProperty("min-width");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("flex");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("min-width");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("margin");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("padding");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("background");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("backdrop-filter");
document.querySelector(".ic-Layout-contentMain")?.style.removeProperty("-webkit-backdrop-filter");
document.getElementById("left-side")?.style.removeProperty("display");
document.getElementById("left-side")?.style.removeProperty("padding-top");
document.getElementById("left-side")?.style.removeProperty("padding-left");
document.getElementById("section-tabs")?.style.removeProperty("padding-top");
document.getElementById("better-sidebar-container")?.remove();
clearBetterSidebarLayoutFix();
}
function ensureBetterSidebar() {
if (!options.better_sidebar) return;
if (document.querySelector("#better-sidebar-container")) return;
if (!document.querySelector("#wrapper") || !document.querySelector(".ic-Layout-contentWrapper")) return;
setupBetterSidebar(getSidebarLayoutMode());
}
async function applyCustomBackground() {
// let style = document.querySelector("#DashboardCard_Container")
let style = document.querySelector("#canvasrefined-background") || document.createElement('style');
style.id = "canvasrefined-background";
const activeBackground = await getActiveCustomBackground();
console.log("[CanvasRefined] activeBackground:", activeBackground);
if (!activeBackground) {
if (style.isConnected) style.remove();
return;
}
const backgroundScale = Number(activeBackground.scale) || 100;
const backgroundUrl = JSON.stringify(activeBackground.url);
const fitToScreen = options.fitImageToScreen === true;
console.log("[CanvasRefined] Applying background:", activeBackground.url, "fitToScreen:", fitToScreen);
style.textContent = `
#wrapper {
background-image: url(${backgroundUrl}) !important;
background-repeat: no-repeat !important;
background-position: center center !important;
background-attachment: fixed !important;
}
@media (orientation: landscape) {
#wrapper { background-size: ${fitToScreen ? 'cover' : backgroundScale + '% auto'} !important; }
}
@media (orientation: portrait) {
#wrapper { background-size: cover !important; }
}
.ic-Dashboard-header__layout {
background: none !important;
/* backdrop-filter: blur(10px) !important; */
border-radius: 5px;
padding-left: 20px !important;
}
#dashboard_header_container {
margin-left: -35px !important;
margin-right: -35px !important;
box-sizing: border-box !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 20%) !important;
border: 1px solid color-mix(in srgb, var(--bcborders) 60%, transparent) !important;
border-radius: 10px !important;
position: sticky !important;
top: 0 !important;
z-index: 1000 !important;
backdrop-filter: blur(8px) saturate(120%) !important;
-webkit-backdrop-filter: blur(8px) saturate(120%) !important;
}
#right-side-wrapper {
// backdrop-filter: blur(10px) !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%);
border-radius: 5px;
}
.header-bar {
background: none !important;
padding: 0 !important;
border: none !important;
}
.item-group-condensed,
.item-group-container {
background: transparent !important;
/* backdrop-filter: blur(14px) saturate(120%) !important;
-webkit-backdrop-filter: blur(14px) saturate(120%) !important; */
border-radius: 12px !important;
border: 1px solid color-mix(in srgb, var(--bcborders) 75%, transparent) !important;
/* box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important; */
}
#context_modules_sortable_container {
border: none !important;
background: none !important;
padding: 0 !important;
/* backdrop-filter: blur(0) !important; */
}
.item-group-condensed .ig-header,
.item-group-condensed .ig-row,
.item-group-container .ig-header,
.item-group-container .ig-row,
.item-group-condensed .header,
.item-group-container .header {
background: transparent !important;
}
.item-group-condensed .ig-header.header,
.item-group-container .ig-header.header {
background: none !important;
border: none !important;
border-radius: 0 !important;
}
#assignments.ui-tabs-panel {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%) !important;
border-radius: 5px !important;
}
#assignments {
padding-top: 0px !important;
padding-bottom: 0px !important;
padding-left: 10px !important;
padding-right: 10px !important;
}
${isCoursesIndexPage() ? `
#content {
margin: 36px 48px 48px !important;
padding: 10px !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%) !important;
border-radius: 5px !important;
box-sizing: border-box !important;
}
` : ""}
${isGroupsIndexPage() ? `
#content {
margin: 36px 48px 48px !important;
padding: 10px !important;
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%) !important;
border-radius: 5px !important;
box-sizing: border-box !important;
}
` : ""}
${isConversationsPage() ? `
.css-1nh4pc4-view-flexItem {
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%) !important;
border-radius: 5px !important;
box-sizing: border-box !important;
}
.css-1nh4pc4-view-flexItem svg,
.css-1nh4pc4-view-flexItem svg * {
fill: currentColor !important;
stroke: currentColor !important;
color: var(--bctext-0) !important;
}
` : ""}
.item-group-condensed .ig-row.ig-published.no-estimated-duration {
color: var(--bctext-1) !important;
border: 1px solid color-mix(in srgb, var(--bcborders) 60%, transparent) !important;
border-radius: 0 !important;
padding: 10px 12px !important;
}
.item-group-condensed .context_module_item,
.item-group-container .context_module_item {
background: transparent !important;
/* backdrop-filter: blur(10px) saturate(115%) !important;
-webkit-backdrop-filter: blur(10px) saturate(115%) !important; */
}
.item-group-condensed .context_module_item:hover,
.item-group-container .context_module_item:hover,
.item-group-condensed .context_module_item.context_module_item_hover,
.item-group-container .context_module_item.context_module_item_hover {
background: transparent !important;
border-radius: 10px !important;
}
.item-group-container {
background: transparent !important;
border-radius: 12px !important;
border: 1px solid color-mix(in srgb, var(--bcborders) 75%, transparent) !important;
}
.ig-header {
/* backdrop-filter: blur(10px) !important; */
}
.item-group-condensed.context_module,
.item-group-condensed.context_module_item,
.item-group-condensed[class~="context_module"] {
margin-bottom: 10px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.item-group-condensed .ig-header.header,
.item-group-container .ig-header.header {
padding-top: 0 !important;
}
/* Apply backdrop blur only to module panels, not to all headers */
.item-group-condensed.context_module,
.item-group-condensed.context_module_item,
.item-group-condensed.context_module:hover,
.item-group-condensed.context_module_item:hover,
.item-group-condensed.context_module.context_module_item_hover,
.item-group-condensed.context_module_item.context_module_item_hover {
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
}
.canvasrefined-gpa-card,
.canvasrefined-gpa,
.ic-DashboardCard {
background: var(--bcbackground-0) !important;
}
tr.student_assignment.assignment_graded.editable > * {
border:none!important
}`;
// TODO: liquid glass?
document.documentElement.appendChild(style);
}
function clearCustomBackground() {
let style = document.querySelector("#canvasrefined-background");
if (style) style.remove();
}
function applyBetterSidebarLayoutFix() {
let style = document.querySelector("#canvasrefined-sidebar-layout-fix") || document.createElement("style");
style.id = "canvasrefined-sidebar-layout-fix";
style.textContent = `
#wrapper,
.ic-Layout-wrapper,
#main {
margin-left: 0 !important;
}
`;
document.documentElement.appendChild(style);
}
function clearBetterSidebarLayoutFix() {
let style = document.querySelector("#canvasrefined-sidebar-layout-fix");
if (style) style.remove();
}
let insertTimer;
function resetTimer() {
clearTimeout(insertTimer);
insertTimer = setTimeout(() => {
if (document.querySelectorAll(".ic-DashboardCard__link").length > 0) {
loadCardAssignments();
loadBetterTodo();
} else {
resetTimer();
}
}, 1);
}
function checkDashboardReady() {
const callback = (mutationList) => {
for (const mutation of mutationList) {
if (mutation.type !== "childList") continue;
if (current_page == "/" || current_page == "" || current_page.match(/^\/courses\/(\d+)(?:\/|$)/)) {
if (dashboardReadyTimer) continue;
const dashboardCards = document.querySelector("#DashboardCard_Container");
if (dashboardCards) {
}
dashboardReadyTimer = setTimeout(() => {
dashboardReadyTimer = null;
const c = document.querySelector("#DashboardCard_Container");
if (c) {
let cards = document.querySelectorAll(".ic-DashboardCard");
changeGradientCards();
setupCardAssignments();
loadCardAssignments();
customizeCards(cards);
insertGrades();
loadDashboardNotes();
setupGPACalc();
showUpdateMsg();
createNasaInfoOverlay();
}
const rightSide = document.querySelector("#right-side");
if (rightSide && !rightSide.querySelector(".canvasrefined-todosidebar")) {
setupBetterTodo();
setupBetterSidebar(getSidebarLayoutMode());
}
if (options.better_sidebar) {
ensureBetterSidebar();
}
}, 0);
} else {
console.log("I am outside", current_page);
if (options.better_sidebar) {
ensureBetterSidebar();
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(document.querySelector('html'), { childList: true, subtree: true });
}
function recieveMessage(request, sender, sendResponse) {
switch (request.message) {
case ("getCards"):
if (options["card_method_dashboard"] === true) {
getCardsFromDashboard();
} else {
getCards();
}
sendResponse(true);
break;
case ("setcolors"): changeColorPreset(request.options); sendResponse(true); break;
case ("getcolors"): sendResponse(getCardColors()); break;
case ("inspect"): sendResponse(inspectDarkMode(true)); break;
case ("fixdm"): sendResponse(runDarkModeFixer(true)); break;
case ("updateBackground"): applyCustomBackground(); sendResponse(true); break;
default: sendResponse(true);
}
}
function hexToRgb(hex) {
let match = (/#(.{2})(.{2})(.{2})/).exec(hex);
if (match) {
return { "r": parseInt(match[1], 16), "g": parseInt(match[2], 16), "b": parseInt(match[3], 16) };
}
}
function inspectDarkMode(withOutput = false) {
let output = "";
let bgcount = 0, textcount = 0, time = performance.now();
let bg0 = hexToRgb(options.dark_preset["background-0"]);
let bg1 = hexToRgb(options.dark_preset["background-1"]);
let txt = hexToRgb(options.dark_preset["text-0"]);
let bdr = hexToRgb(options.dark_preset["borders"]);
let lnk = hexToRgb(options.dark_preset["links"]);
document.querySelectorAll("*").forEach(el => {
let style = getComputedStyle(el);
let bgcolor = style.getPropertyValue("background").match(/rgb\((?\d*)\, ?(?\d*)\, ?(?\d*)\) none/);
let selector = "class=." + el.className + ",id=#" + el.id;
if (bgcolor) {
const r = parseInt(bgcolor.groups["r"]);
const g = parseInt(bgcolor.groups["g"]);
const b = parseInt(bgcolor.groups["b"]);
/*
if (el.classList.contains("no-touch")) {
console.log({ "r": r, "g": g, "b": b }, { "r": r === bg0.r, "g": g === bg0.g, "b": b === bg0.b });
}
*/
if (r > 245 && g > 245 && b > 245 && !(r === bg0.r && g === bg0.g && b === bg0.b) && !(r === lnk.r && g === lnk.g && b === lnk.b)) {
el.style.cssText = (";background:" + options.dark_preset["background-0"] + "!important;color" + options.dark_preset["text-0"] + "!important;") + el.style.cssText;
if (withOutput === true) output += selector + "{background: background-0, color: text-0}\n";
bgcount++;
} else if (r > 225 && r < 245 && g > 225 && g < 245 && b > 225 && b < 245 && !(r === bg1.r && g === bg1.g && b === bg1.b) && !(r === lnk.r && g === lnk.g && b === lnk.b)) {
el.style.cssText = (";background:" + options.dark_preset["background-1"] + "!important;color" + options.dark_preset["text-0"] + "!important;") + el.style.cssText;
if (withOutput === true) output += selector + "{background: background-1, color: text-0}";
bgcount++;
}
}
let bordercolor = style.getPropertyValue("border-color").match(/rgb\((?\d*)\, ?(?\d*)\, ?(?\d*)/);
if (bordercolor) {
const r = parseInt(bordercolor.groups["r"]);
const g = parseInt(bordercolor.groups["g"]);
const b = parseInt(bordercolor.groups["b"]);
if (r > 195 && g > 195 && b > 195 && !(r === bdr.r && g === bdr.g && b === bdr.b) && !(r === lnk.r && g === lnk.g && b === lnk.b)) {
el.style.cssText = "border-color:" + options.dark_preset["borders"] + "!important;" + el.style.cssText;
if (withOutput === true) output += selector + "{border: borders}";
}
}
let text = style.getPropertyValue("color").match(/rgb\((?\d*)\, ?(?\d*)\, ?(?\d*)/);
if (text) {
const r = parseInt(text.groups["r"]);
const g = parseInt(text.groups["g"]);
const b = parseInt(text.groups["b"]);
if (r <= 70 && g <= 70 && b <= 70 && !(r === txt.r && g === txt.g && b === txt.b)) {
el.style.cssText = "color:" + options.dark_preset["text-0"] + "!important;" + el.style.cssText;
if (withOutput === true) output += selector + "{text: text-0}";
textcount++;
}
}
});
console.log("done fixing dark mode - time:", performance.now() - time, "total backgrounds changed: ", bgcount, ", total colors changed: ", textcount);
return { "selectors": output === "" ? "no gaps determined" : output, "time": performance.now() - time };
}
function getCardColors() {
let cards = document.querySelectorAll(".ic-DashboardCard__header");
let colors = [];
cards.forEach(card => {
let rgbColor = card.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor;
colors.push({ "href": card.querySelector(".ic-DashboardCard__link").href, "color": rgbToHex(rgbColor) });
});
colors.sort((a, b) => a.href > b.href ? 1 : -1);
colors = colors.map(x => x.color);
return colors;
}
function getCardsFromDashboard() {
console.log("getting cards from dashboard")
const dashboard_cards = document.querySelectorAll(".ic-DashboardCard");
chrome.storage.sync.get(["custom_cards", "custom_cards_2", "custom_cards_3"], storage => {
let cards = storage["custom_cards"] || {};
let cards_2 = storage["custom_cards_2"] || {};
let cards_3 = storage["custom_cards_3"] || {};
let newCards = false;
let count = 0;
try {
dashboard_cards.forEach(card => {
const id = card.querySelector(".ic-DashboardCard__link").href.split("courses/")[1];
if (count >= (options["card_limit"] || 25)) return;
if (!cards[id]) {
newCards = true;
cards[id] = { "default": card.querySelector(".ic-DashboardCard__header-subtitle").textContent.substring(0, 20), "name": "", "code": "", "img": "", "hidden": false, "weight": "regular", "credits": 1, "eid": 100000 - count, "gr": null };
let links = [];
for (let i = 0; i < 4; i++) {
links.push({ "path": "default", "is_default": true });
}
cards_2[id] = { "links": links };
cards_3[id] = { "url": domain };
}
count++;
});
// there shouldn't be 0 cards
if (count === 0) return;
//delete cards that aren't on the dashboard anymore
Object.keys(cards).forEach(key => {
let found = false;
// ignore cards that are not for the current url
if (cards_3[key] && cards_3[key].url !== domain) {
found = true;
} else {
dashboard_cards.forEach(card => {
const id = card.querySelector(".ic-DashboardCard__link").href.split("courses/")[1];
if (parseInt(key) === parseInt(id)) found = true;
});
}
if (found === false) {
console.log("Deleting " + key);
cards[key] && delete cards[key];
cards_2[key] && delete cards_2[key];
cards_3[key] && delete cards_3[key];
newCards = true;
}
});
} catch (e) {
console.log("Error getting dashboard cards\n", e);
logError(e);
} finally {
if(newCards !== true) return;
console.log(newCards ? "new cards found" : "");
chrome.storage.sync.set({ "custom_cards": cards, "custom_cards_2": cards_2, "custom_cards_3": cards_3 });
}
});
}
async function getCards(api = null) {
let dashboard_cards = api ? api : await getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}per_page=100`);
chrome.storage.sync.get(["custom_cards", "custom_cards_2", "custom_cards_3"], storage => {
let cards = storage["custom_cards"] || {};
let cards_2 = storage["custom_cards_2"] || {};
let cards_3 = storage["custom_cards_3"] || {};
let newCards = false;
let count = 0;
// sort cards by enrollment id (i think the higher the id, the more recent it is)
if (options["card_method_date"] === true) {
dashboard_cards.sort((a, b) => (b?.created_at) > (a?.created_at) ? 1 : -1);
} else {
dashboard_cards.sort((a, b) => (b?.enrollment_term_id || 0) - (a?.enrollment_term_id || 0));
}
try {
dashboard_cards.forEach(card => {
if (!card.course_code || count >= (options["card_limit"] || 25)) return;
let id = card.id;
if (!cards || !cards[id]) {
newCards = true;
cards[id] = { "default": card.course_code.substring(0, 20), "name": "", "code": "", "img": "", "hidden": false, "weight": "regular", "credits": 1, "eid": card.enrollment_term_id || 0, "gr": null };
} else if (cards && cards[id]) {
newCards = true;
cards[id].default = card.course_code.substring(0, 20);
cards[id].eid = card.enrollment_term_id || 0;
if (!cards[id].code) cards[id].code = "";
}
if (!cards_2 || !cards_2[id]) {
newCards = true;
let links = [];
for (let i = 0; i < 4; i++) {
links.push({ "path": "default", "is_default": true });
}
cards_2[id] = { "links": links };
}
if (!cards_3 || !cards_3[id]) {
newCards = true;
cards_3[id] = { "url": domain };
}
count++;
});
//delete cards that aren't on the dashboard anymore
Object.keys(cards).forEach(key => {
let found = false;
// ignore cards that are not for the current url
if (cards_3[key] && cards_3[key].url !== domain) {
found = true;
} else {
dashboard_cards.forEach(card => {
if (parseInt(key) === card.id) found = true;
});
}
if (found === false) {
console.log("Deleting " + key + " from custom_cards...", cards[key]);
cards[key] && delete cards[key];
cards_2[key] && delete cards_2[key];
cards_3[key] && delete cards_3[key];
newCards = true;
}
});
} catch (e) {
console.log(e);
} finally {
return chrome.storage.sync.set(newCards ? { "custom_cards": cards, "custom_cards_2": cards_2, "custom_cards_3": cards_3 } : {});
}
});
}
/*
Better todo list
*/
// function setAssignmentState(id, updates) {
// let states = options.assignment_states;
// let length = JSON.stringify(states).length;
// // remove the oldest states if the size is approaching the storage limit
// if (length > 7400) {
// let keys = Object.keys(states).sort((a, b) => states[b].expire - states[a].expire);
// keys.splice(-5);
// let newStates = {};
// keys.forEach(key => {
// newStates[key] = states[key];
// });
// states = newStates;
// }
// states[id] = states[id] ? { ...states[id], ...updates } : updates;
// chrome.storage.sync.set({ assignment_states: states }).then(() => { cardAssignments = preloadAssignmentEls(); loadBetterTodo(); loadCardAssignments(); });
// }
function createTodoCreateBtn(location) {
let confirmButton = makeElement("button", location, { "className": "canvasrefined-custom-btn", "textContent": "Create" });
confirmButton.addEventListener("click", () => {
chrome.storage.sync.get("custom_assignments_overflow", overflow => {
chrome.storage.sync.get(overflow["custom_assignments_overflow"], storage => {
let course_id = parseInt(location.querySelector("#canvasrefined-custom-course").value);
const assignment = {
"plannable_id": new Date().getTime(),
"context_name": options.custom_cards[location.querySelector("#canvasrefined-custom-course").value].default,
"plannable": { "title": location.querySelector("#canvasrefined-custom-name").value },
"plannable_date": location.querySelector("#canvasrefined-custom-date").value + "T" + location.querySelector("#canvasrefined-custom-time").value + ":00",
"planner_override": { "marked_complete": false, "custom": true },
"plannable_type": "assignment",
"submissions": { "submitted": false },
"course_id": course_id,
"html_url": `/courses/${course_id}/assignments`
};
/* handling overflow since the limit is 8kb per key */
let found = false;
let reload = () => {
location.classList.toggle("canvasrefined-custom-open");
loadBetterTodo();
loadCardAssignments();
}
/* find the first available overflow with space */
/* or create a new one if all are full */
let findOpenOverflow = (num) => {
let current_overflow = overflow["custom_assignments_overflow"][num];
storage[current_overflow].push(assignment);
chrome.storage.sync.set({ [current_overflow]: storage[current_overflow] }, () => {
/* assuming any error is because the limit is exceeded */
if (chrome.runtime.lastError) {
if (num === overflow["custom_assignments_overflow"].length - 1) {
console.log("all overflows are full! creating new overflow " + (overflow["custom_assignments_overflow"].length + 1));
let new_overflow = "custom_assignments_" + (overflow["custom_assignments_overflow"].length + 1);
overflow["custom_assignments_overflow"].push(new_overflow);
chrome.storage.sync.set({ [new_overflow]: [assignment], "custom_assignments_overflow": overflow["custom_assignments_overflow"] }).then(reload);
} else {
console.log("overflow " + (num + 1) + " full...");
findOpenOverflow(num + 1);
}
} else {
console.log("overflow " + (num + 1) + " has space!");
reload();
}
});
}
findOpenOverflow(0);
});
})
});
}
// better todo html layer 1
// function createTodoHeader(location) {
// let todoHeader = makeElement("h2", location, { "className": "todo-list-header", "style": "display: flex; align-items:center; justify-content:space-between;" });
// //todoHeader.style = "display: flex; align-items:center; justify-content:space-between;";
// if (!options.custom_cards || Object.keys(options.custom_cards).length === 0) return;
// let addFillout = makeElement("div", location, { "className": "canvasrefined-add-assignment" });
// let now = new Date();
// let year = now.getFullYear();
// let month = now.getMonth() + 1;
// let day = now.getDate();
// month = month < 10 ? "0" + month : month;
// day = day < 10 ? "0" + day : day;
// addFillout.innerHTML = '';
// addFillout.querySelector("#canvasrefined-custom-date").value = year + "-" + month + "-" + day;
// let selectCourse = document.querySelector("#canvasrefined-custom-course");
// Object.keys(options.custom_cards).forEach(id => {
// let card = options.custom_cards[id];
// let courseName = makeElement("option", selectCourse, { "className": "canvasrefined-select-course-option", "textContent": card.default });
// courseName.value = id;
// });
// createTodoCreateBtn(addFillout);
// let headerText = makeElement("span", todoHeader, { "className": "canvasrefined-todo-header", "textContent": "To Do" });
// let addButton = makeElement("button", todoHeader, { "className": "canvasrefined-custom-btn", "textContent": "+ Add" });
// addButton.addEventListener("click", () => {
// addFillout.classList.toggle("canvasrefined-custom-open");
// });
// headerText.addEventListener("click", () => {
// if (filter === "todo") {
// filter = "done";
// headerText.textContent = "Done";
// } else {
// filter = "todo";
// headerText.textContent = "To Do";
// }
// moreAssignmentCount = 0;
// moreAnnouncementCount = 0;
// loadBetterTodo();
// });
// }
function convertToDueDate(dueAt) {
final = "due ";
let date = new Date(dueAt);
final += date.toLocaleString("en-US", { month: "short", day: "numeric" });
final += " at " + date.toLocaleString("en-US", { hour: "numeric", minute: "numeric", hour12: !options.todo_hr24 });
return final;
}
function updateIndicator(element) {
const indicator = document.getElementById("better-todo-indicator");
indicator.style.width = `${element.offsetWidth*2}px`;
indicator.style.left = `${element.offsetLeft - (element.offsetWidth * .5)}px`;
const buttons = ["announcement", "assignments", "completed"];
buttons.forEach(button => {
const btn = document.getElementById(`better-todo-${button}`);
if (btn == element) {
btn.firstElementChild.style.opacity = "1";
// btn.style.filter = "none";
}
else {
btn.firstElementChild.style.opacity = ".5";
// btn.style.filter = "grayscale(100%)";
}
})
}
// better todo html
betterTodoFilter = "tasks";
let domContainers = {};
function formatDateForInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function formatTimeForInput(date) {
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
}
function renderProgressRings(container, scopedData) {
const allAssignments = scopedData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note"));
// exclude items older than one month
const oneMonthAgo = Date.now() - 1000 * 60 * 60 * 24 * 30;
const recentAssignments = allAssignments.filter(item => {
const dateStr = item.plannable_date || item.todo_date || item.plannable?.due_at || item.plannable?.plannable_date;
if (!dateStr) return true; // keep items without a date
const ts = Date.parse(dateStr);
if (Number.isNaN(ts)) return true;
return ts >= oneMonthAgo;
});
const groups = {};
allAssignments.forEach(item => {
const cid = String(item.course_id || item.context_id || item.plannable?.course_id || "personal");
groups[cid] = groups[cid] || [];
groups[cid].push(item);
});
const entries = Object.keys(groups).map(cid => {
const arr = groups[cid];
const completed = arr.filter(it => (it.submissions?.submitted || it.planner_override?.marked_complete)).length;
return { courseId: cid, total: arr.length, completed };
}).filter(e => e.total > 0);
if (!entries.length) {
container.innerHTML = "";
return;
}
// sort by total desc and limit rings to 6
entries.sort((a, b) => b.total - a.total);
const shown = entries.slice(0, 6);
const totalAll = shown.reduce((s, e) => s + e.total, 0);
const completedAll = shown.reduce((s, e) => s + e.completed, 0);
const percent = totalAll === 0 ? 0 : Math.round((completedAll / totalAll) * 100);
// build SVG rings with visible gaps and a larger central hole.
// calculate available width from the container so the outer ring is slightly inset
const containerWidth = (container.clientWidth || 240);
const maxSize = Math.min(280, Math.floor(containerWidth * 0.99));
const size = maxSize; // svg square size
const cx = size / 2;
const cy = size / 2;
let svgInner = "";
const ringCount = shown.length;
const stroke = 8; // ring thickness (thinner)
const gap = 4; // visible gap between rings (reduced)
const decrement = stroke + gap; // radius difference per ring ensures gap
// make outer ring extend closer to container edges by using a small padding
const padding = 2;
const startRadius = Math.floor((size / 2) - padding - (stroke / 2));
// ensure radii stay positive; if too small, reduce stroke/gap
const minCenterRadius = 28; // minimum desired central hole radius
const requiredSpace = (ringCount - 1) * decrement + stroke / 2 + minCenterRadius;
let adjustFactor = 1;
if (requiredSpace > startRadius) {
// scale down decrement to fit
adjustFactor = (startRadius - minCenterRadius - stroke / 2) / Math.max(1, (ringCount - 1) * decrement);
}
shown.forEach((entry, idx) => {
const effectiveDecrement = Math.max(1, Math.floor(decrement * adjustFactor));
const radius = startRadius - idx * effectiveDecrement;
if (radius <= 0) return;
const circumference = 2 * Math.PI * radius;
const prog = entry.total === 0 ? 0 : (entry.completed / entry.total);
const color = options.custom_cards_3?.[String(entry.courseId)]?.color || options.custom_cards_3?.[entry.courseId]?.color || `hsl(${(idx * 60) % 360} 70% 50%)`;
// background ring (faded course color)
svgInner += ``;
// progress ring (rotate -90 to start at top) - initialize empty and animate to target
const dasharrayVal = circumference.toFixed(3);
const dashoffsetTarget = (circumference * (1 - prog)).toFixed(3);
// start with full offset (empty) and store target in data attribute; we'll animate after inserting into DOM
svgInner += ``;
});
// center overlay text positioned inside the hole
// Reuse existing elements when possible to avoid DOM replacement flicker
let wrapper = container.querySelector('.canvasrefined-progress-wrapper');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.className = 'canvasrefined-progress-wrapper';
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
wrapper.style.alignItems = 'center';
wrapper.style.position = 'relative';
container.appendChild(wrapper);
}
// svg container
let svg = wrapper.querySelector('svg.canvasrefined-progress-svg');
if (!svg) {
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'canvasrefined-progress-svg');
svg.setAttribute('width', String(size));
svg.setAttribute('height', String(size));
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
svg.style.display = 'block';
wrapper.appendChild(svg);
} else {
svg.setAttribute('width', String(size));
svg.setAttribute('height', String(size));
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
}
// ensure overlay text exists
let overlay = wrapper.querySelector('.canvasrefined-progress-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'canvasrefined-progress-overlay';
overlay.style.position = 'absolute';
overlay.style.left = '0';
overlay.style.top = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.pointerEvents = 'none';
const textWrap = document.createElement('div');
textWrap.style.textAlign = 'center';
textWrap.style.color = 'var(--bctext-0)';
textWrap.innerHTML = `${percent}%
${completedAll}/${totalAll} done
`;
overlay.appendChild(textWrap);
wrapper.appendChild(overlay);
} else {
const pc = overlay.querySelector('.canvasrefined-progress-percent');
const cnt = overlay.querySelector('.canvasrefined-progress-count');
if (pc) pc.textContent = `${percent}%`;
if (cnt) cnt.textContent = `${completedAll}/${totalAll} done`;
}
// Update or create rings in-place
const existingBg = svg.querySelectorAll('circle.canvasrefined-ring-bg');
const existingFg = svg.querySelectorAll('circle.canvasrefined-progress-ring');
// reuse or create circles per shown entry
shown.forEach((entry, idx) => {
const radius = startRadius - idx * Math.max(1, Math.floor(decrement * adjustFactor));
const circumference = 2 * Math.PI * radius;
const prog = entry.total === 0 ? 0 : (entry.completed / entry.total);
const color = options.custom_cards_3?.[String(entry.courseId)]?.color || options.custom_cards_3?.[entry.courseId]?.color || `hsl(${(idx * 60) % 360} 70% 50%)`;
// background circle
let bg = svg.querySelector(`circle.canvasrefined-ring-bg[data-idx='${idx}']`);
if (!bg) {
bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bg.classList.add('canvasrefined-ring-bg');
bg.setAttribute('data-idx', String(idx));
svg.appendChild(bg);
}
bg.setAttribute('cx', String(cx));
bg.setAttribute('cy', String(cy));
bg.setAttribute('r', String(radius));
bg.setAttribute('stroke', color);
bg.setAttribute('stroke-opacity', '0.25');
bg.setAttribute('stroke-width', String(stroke));
bg.setAttribute('fill', 'none');
// foreground (progress) circle
let fg = svg.querySelector(`circle.canvasrefined-progress-ring[data-idx='${idx}']`);
const dasharrayVal = circumference.toFixed(3);
const dashoffsetTarget = (circumference * (1 - prog)).toFixed(3);
if (!fg) {
fg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
fg.classList.add('canvasrefined-progress-ring');
fg.setAttribute('data-idx', String(idx));
fg.setAttribute('stroke-linecap', 'round');
fg.setAttribute('transform', `rotate(-90 ${cx} ${cy})`);
fg.setAttribute('stroke-dasharray', dasharrayVal);
fg.setAttribute('stroke-dashoffset', dasharrayVal); // start empty
fg.style.transition = 'stroke-dashoffset .8s cubic-bezier(.2,.9,.2,1), opacity .3s ease';
svg.appendChild(fg);
}
fg.setAttribute('cx', String(cx));
fg.setAttribute('cy', String(cy));
fg.setAttribute('r', String(radius));
fg.setAttribute('stroke', color);
fg.setAttribute('stroke-width', String(stroke));
fg.setAttribute('fill', 'none');
fg.setAttribute('stroke-dasharray', dasharrayVal);
fg.setAttribute('data-target', dashoffsetTarget);
// request animation frame to set dashoffset to target (triggers transition)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
fg.setAttribute('stroke-dashoffset', dashoffsetTarget);
});
});
});
// remove any extra existing circles
const maxIdx = shown.length - 1;
svg.querySelectorAll('circle.canvasrefined-ring-bg, circle.canvasrefined-progress-ring').forEach(c => {
const idx = parseInt(c.getAttribute('data-idx'));
if (Number.isNaN(idx) || idx > maxIdx) c.remove();
});
}
function buildPlannerNotePayload(form) {
const title = form.querySelector("#better-todo-new-task-title")?.value?.trim();
const details = form.querySelector("#better-todo-new-task-details")?.value?.trim();
const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value;
const dateValue = form.querySelector("#better-todo-new-task-date")?.value;
const timeValue = form.querySelector("#better-todo-new-task-time")?.value;
if (!title) {
throw new Error("Task title is required.");
}
if (!dateValue || !timeValue) {
throw new Error("Please choose both a date and time.");
}
const localDateTime = new Date(`${dateValue}T${timeValue}:00`);
if (Number.isNaN(localDateTime.getTime())) {
throw new Error("Invalid task date.");
}
return {
title,
details,
courseId: courseIdRaw ? parseInt(courseIdRaw) : null,
// Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes.
todoDate: `${dateValue}T${timeValue}:00`,
};
}
async function createCanvasPlannerNote(payload) {
const csrfToken = CSRFtoken();
const plannerNote = {
title: payload.title,
todo_date: payload.todoDate,
};
if (payload.details) plannerNote.details = payload.details;
if (payload.courseId) plannerNote.course_id = payload.courseId;
const attempts = [
{
headers: {
"content-type": "application/json",
"accept": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({ planner_note: plannerNote }),
},
{
headers: {
"content-type": "application/json",
"accept": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify(plannerNote),
},
{
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"accept": "application/json",
"X-CSRF-Token": csrfToken,
},
body: (() => {
const formBody = new URLSearchParams();
formBody.set("planner_note[title]", plannerNote.title);
formBody.set("planner_note[todo_date]", plannerNote.todo_date);
if (plannerNote.details) formBody.set("planner_note[details]", plannerNote.details);
if (plannerNote.course_id) formBody.set("planner_note[course_id]", plannerNote.course_id);
return formBody.toString();
})(),
},
];
let lastError = "Canvas rejected task creation.";
for (const attempt of attempts) {
const response = await fetch(domain + "/api/v1/planner_notes", {
method: "POST",
headers: attempt.headers,
body: attempt.body,
});
if (response.status === 200 || response.status === 201) {
return response.json();
}
try {
const errData = await response.json();
if (errData?.errors?.length) {
lastError = errData.errors.join(" ");
} else if (errData?.message) {
lastError = errData.message;
}
} catch (_) {
// Keep prior error text when body is not JSON.
}
}
throw new Error(lastError || "Canvas rejected task creation.");
}
function fillTaskCourseOptions(courseSelect) {
const cards = options.custom_cards || {};
const courseColors = options.custom_cards_3 || {};
const currentCourseId = getCurrentCourseId();
const entries = Object.entries(cards)
.map(([id, card]) => ({
id,
label: card?.default || `Course ${id}`,
color:
courseColors?.[String(id)]?.color ??
courseColors?.[id]?.color ??
"#c7cdd1",
}))
.sort((a, b) => a.label.localeCompare(b.label));
courseSelect.innerHTML = '';
courseSelect.options[0].dataset.color = "#c7cdd1";
entries.forEach(entry => {
const option = makeElement("option", courseSelect, {
value: entry.id,
textContent: entry.label,
});
option.dataset.color = entry.color;
option.style.color = entry.color;
if (currentCourseId && String(currentCourseId) === String(entry.id)) {
option.selected = true;
}
});
}
function updateTaskCourseSelectColor(courseSelect) {
const selectedOption = courseSelect?.options?.[courseSelect.selectedIndex];
const color = selectedOption?.dataset?.color || "#c7cdd1";
courseSelect.style.borderLeft = `4px solid ${color}`;
courseSelect.style.paddingLeft = "8px";
}
function ensureTodoTaskMenu(location, feedbackElement) {
let actionsRow = location.querySelector("#better-todo-actions-row");
if (!actionsRow) {
actionsRow = makeElement("div", location, {
id: "better-todo-actions-row",
style: "display:flex;flex-direction:column;gap:8px;margin-top:14px;",
});
const addTaskButton = makeElement("button", actionsRow, {
id: "better-todo-add-task-btn",
className: "canvasrefined-custom-btn",
textContent: "+ Add Task",
style: "width:100%;padding:6px 8px;cursor:pointer;",
});
const menu = makeElement("div", actionsRow, {
id: "better-todo-add-task-menu",
className: "canvasrefined-add-assignment",
});
menu.innerHTML = `
`;
const today = new Date();
menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(today);
menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(today);
const courseSelect = menu.querySelector("#better-todo-new-task-course");
fillTaskCourseOptions(courseSelect);
updateTaskCourseSelectColor(courseSelect);
courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect));
addTaskButton.addEventListener("click", () => {
menu.classList.toggle("canvasrefined-custom-open");
});
menu.querySelector("#better-todo-add-task-submit").addEventListener("click", async () => {
const status = menu.querySelector("#better-todo-add-task-status");
const submitButton = menu.querySelector("#better-todo-add-task-submit");
status.textContent = "";
submitButton.disabled = true;
try {
const payload = buildPlannerNotePayload(menu);
await createCanvasPlannerNote(payload);
status.textContent = "Task created.";
status.style.color = "#198754";
menu.querySelector("#better-todo-new-task-title").value = "";
menu.querySelector("#better-todo-new-task-details").value = "";
menu.classList.remove("canvasrefined-custom-open");
getAssignments();
clearTodoList();
createTodoSections(location);
} catch (e) {
status.textContent = e?.message || "Could not create task.";
status.style.color = "#db3754";
} finally {
submitButton.disabled = false;
}
});
}
if (feedbackElement) {
if (actionsRow.nextSibling !== feedbackElement) {
location.insertBefore(actionsRow, feedbackElement);
}
} else if (actionsRow.parentElement !== location) {
location.append(actionsRow);
}
}
async function createTodoSections(location) {
if (!location.querySelector("#better-todo-header")) {
let header = makeElement("div", location, { id: "better-todo-header" });
header.style = "display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--bcbackground-1);padding-bottom:-2px;";
let today = new Date();
today.setHours(0,0,0,0);
const todayString = today.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" });
header.innerHTML = `
Tasks
${todayString}
`;
// placeholder for progress rings above the tab/filter control
makeElement("div", location, { id: "better-todo-progress-placeholder", style: "display:flex;justify-content:center;margin-top:8px;" });
let filterControl = makeElement("div", location, { "id": "better-todo-filter" });
filterControl.innerHTML = `
`;
setTimeout(() => updateIndicator(document.getElementById("better-todo-assignments")), 10);
document.getElementById("better-todo-announcement").addEventListener("click", (e) => {
betterTodoFilter = "announcements";
moreAnnouncementCount = 0;
updateIndicator(e.currentTarget);
clearTodoList();
createTodoSections(location);
});
document.getElementById("better-todo-assignments").addEventListener("click", (e) => {
betterTodoFilter = "tasks";
moreAssignmentCount = 0;
updateIndicator(e.currentTarget);
clearTodoList();
createTodoSections(location);
});
document.getElementById("better-todo-completed").addEventListener("click", (e) => {
betterTodoFilter = "completed";
moreCompletedCount = 0;
updateIndicator(e.currentTarget);
clearTodoList();
createTodoSections(location);
});
let mainSection = makeElement("div", location, {
id: "better-todo-main",
});
mainSection.style = "display:flex;flex-direction:column;";
}
let mainSection = location.querySelector("#better-todo-main");
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
? data.filter(item => {
const itemCourseId = parseInt(item.course_id || item.context_id || item?.plannable?.course_id);
return itemCourseId === courseId;
})
: data;
announcements = scopedData.filter(item => item.plannable_type == "announcement");
assignmentsDue = scopedData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note") && !item.submissions?.submitted && !item.planner_override?.marked_complete);
completed = scopedData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note") && (item.submissions?.submitted || item.planner_override?.marked_complete));
// console.log("assignments", assignmentsDue);
// console.log("announcements", announcements);
// console.log("completed", completed);
if (document.getElementById("better-todo-announcement-badge")) {
document.getElementById("better-todo-announcement-badge").remove();
}
let isAnnoucementBadge = 0;
announcements.forEach(item => {
if (item.plannable.read_state == "unread") {
isAnnoucementBadge++;
return;
}
})
if (isAnnoucementBadge > 0) {
makeElement("div", document.getElementById("better-todo-announcement"), {
id: "better-todo-announcement-badge",
style: "background-color:#ff0000;width:15px;height:15px;border-radius:50%;font-size:12px;position:absolute;top:-7px;left:16px;display:flex;justify-content:center;align-items:center;", // TODO: theme compatibility
innerHTML: `${isAnnoucementBadge}`
})
}
domContainers = {};
const groupKeys = ["-1", "0", "1", "2", "3", "4", "5", "6", "7", "14", "21", "30", "Later", "New", "Seen", "Ungraded", "Graded"];
for (const key of groupKeys) {
let wrapper = makeElement("div", mainSection, {
style: "display:none;margin-top:10px;",
className: "better-todo-dueheader",
});
let label = "";
if (key == "Later") label = "Due Later";
if (key == "-1") label = "Overdue";
else if (key == "0") label = "Due Today";
else if (key == "1") label = "Due Tommorow";
else if (key >= 2 && key < 7) label = "Due " + key + " days";
else if (key >= 7 && key < 30) label = "Due " + key/7 + " weeks";
else if (key == "30") label = "Due 1 month";
else label = "" + key + "";
makeElement("div", wrapper, {
innerHTML: "" + label + "",
style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);"
})
let listContainer = makeElement("div", wrapper, { className: "todo-group-list" });
listContainer.style = "display:flex;flex-direction:column;gap:10px;";
domContainers[key] = { wrapper, listContainer };
}
if (betterTodoFilter == "tasks") {
populateAssignments();
}
if (betterTodoFilter == "announcements") {
populateAnnouncements();
}
if (betterTodoFilter == "completed") {
populateAssignments(true);
}
const feedbackElement = location.querySelector(".recent_feedback");
// populate progress rings placeholder (respect user toggle)
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder) {
if (options.todo_progress_rings === undefined || options.todo_progress_rings === true) {
renderProgressRings(progressPlaceholder, scopedData);
} else {
progressPlaceholder.innerHTML = "";
}
}
// Only show the Add Task control on the Assignments (tasks) tab.
if (betterTodoFilter === "tasks") {
ensureTodoTaskMenu(location, feedbackElement);
} else {
const existing = location.querySelector("#better-todo-actions-row");
if (existing) existing.remove();
}
if (feedbackElement) {
if (options.todo_hide_feedback == true) {
feedbackElement.style.display = "none";
} else {
feedbackElement.style.display = "block";
}
}
const sidebar = document.getElementById("right-side-wrapper");
ensureRightSideWrapperScrollbarHidden();
sidebar.style.setProperty("scrollbar-width", "none");
sidebar.style.setProperty("-ms-overflow-style", "none");
if (options.todo_full_height) {
sidebar.style.minHeight = "100vh";
} else {
sidebar.style.minHeight = "";
}
if (options.todo_separate_scrollbar) {
sidebar.style.position = "sticky";
sidebar.style.top = "0";
sidebar.style.height = "100vh";
sidebar.style.overflowY = "auto";
} else {
sidebar.style.position = "";
sidebar.style.top = "";
sidebar.style.height = "";
sidebar.style.overflowY = "";
// maybe invisible scrollbar?
}
});
}
function ensureRightSideWrapperScrollbarHidden() {
let style = document.getElementById("canvasrefined-hide-right-sidebar-scrollbar") || document.createElement("style");
style.id = "canvasrefined-hide-right-sidebar-scrollbar";
style.textContent = `
#right-side-wrapper {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
#right-side-wrapper::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
}
`;
document.head.append(style);
}
function clearTodoList() {
const seeMoreBtn = document.getElementById("better-todo-see-more");
if (seeMoreBtn) {
seeMoreBtn.remove();
}
document.getElementById("better-todo-main").querySelectorAll(".todo-group-list").forEach(list => {
list.innerHTML = "";
});
document.querySelectorAll(".better-todo-dueheader").forEach(header => {
header.remove();
});
}
function populateAssignments(iscompleted = false) {
const today = new Date();
today.setHours(0,0,0,0);
let assignments = (iscompleted ? completed : assignmentsDue).slice();
if (iscompleted) {
assignments.sort((a, b) => {
const aIsGraded = Boolean(a.submissions?.graded);
const bIsGraded = Boolean(b.submissions?.graded);
if (aIsGraded !== bIsGraded) {
return aIsGraded - bIsGraded;
}
return new Date(b.plannable_date) - new Date(a.plannable_date);
});
}
let assignmentCount = 0;
const maxElements = options.num_todo_items;
assignments.forEach((item) => {
let dueGroup = -1;
if (!iscompleted) {
let dueDate = new Date(item.plannable_date);
dueDate.setHours(0,0,0,0);
const diffDays = Math.round((dueDate - today) / (1000 * 60 * 60 * 24));
if (diffDays < 0) {dueGroup = -1;}
else if (diffDays <= 1) { dueGroup = diffDays.toString(); }
else if (diffDays <= 7) { dueGroup = diffDays.toString(); }
else if (diffDays <= 14) {dueGroup = 14;}
else if (diffDays <= 21) {dueGroup = 21;}
else if (diffDays <= 30) {dueGroup = 30;}
else {dueGroup = "Later"};
} else {
dueGroup = item.submissions?.graded ? "Graded" : "Ungraded";
}
let assignment
const targetContainer = domContainers[dueGroup];
assignmentCount++;
let isHidden = assignmentCount > maxElements;
if (targetContainer) {
if (!isHidden) {
targetContainer.wrapper.style.display = "block";
targetContainer.wrapper.setAttribute("data-has-visible", "true");
}
else {
if (!targetContainer.wrapper.hasAttribute("data-has-visible")) {
targetContainer.wrapper.classList.add(
"better-todo-hidden-wrapper",
);
}
}
// targetContainer.wrapper.style.display = "block";
assignment = makeElement("div", targetContainer.listContainer, {
class: "better-todo-assignment",
});
if (isHidden) {
assignment.style.display = "none";
assignment.classList.add("better-todo-hidden-assignment");
}
}
const courseColor =
options.custom_cards_3?.[String(item.course_id)]?.color ??
options.custom_cards_3?.[item.course_id]?.color ??
options.custom_cards_3?.[item.plannable.course_id]?.color ??
"#cccccc";
const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true;
const iconSize = isCustomTask ? 26 : 20;
const iconLeftOffset = isCustomTask ? 2 : 5;
const taskIcon = isCustomTask
? ``
: ``;
assignment.style.overflowX = "hidden";
assignment.innerHTML = `
`;
assignment.querySelector(".better-todo-assignment-checkmark").addEventListener("click", () => {
console.log("marking ", item.plannable.title, " as complete");
markAs(item, assignment.firstElementChild);
});
});
if (document.getElementById("better-todo-see-more")) {
document.getElementById("better-todo-see-more").remove();
}
if (assignmentCount > maxElements) {
let isExpanded = false;
let seeMoreButton = makeElement("button", document.getElementById("better-todo-main"), {
textContent: `View More (${assignmentCount - maxElements})`,
className: "canvasrefined-custom-btn",
id: "better-todo-see-more",
style: "width:100%;margin-top:15px;cursor:pointer;"
})
seeMoreButton.addEventListener("click", () => {
if (!isExpanded) {
document.querySelectorAll(".better-todo-hidden-assignment").forEach(element => element.style.display = "block");
document.querySelectorAll(".better-todo-hidden-wrapper").forEach(element => element.style.display = "block");
seeMoreButton.textContent = "View Less";
} else {
document.querySelectorAll(".better-todo-hidden-assignment").forEach(element => element.style.display = "none");
document.querySelectorAll(".better-todo-hidden-wrapper").forEach(element => element.style.display = "none");
seeMoreButton.textContent = `View More (${assignmentCount - maxElements})`;
}
isExpanded = !isExpanded;
})
}
}
function populateAnnouncements() {
const today = new Date();
today.setHours(0,0,0,0);
announcements.forEach((item) => {
let dueGroup = item.plannable.read_state == "read" ? "Seen" : "New";
let announcement;
// console.log(domContainers)
const targetContainer = domContainers[dueGroup];
if (targetContainer) {
targetContainer.wrapper.style.display = "block";
announcement = makeElement("div", targetContainer.listContainer, {
class: "better-todo-announcement",
});
}
const courseColor =
options.custom_cards_3?.[String(item.course_id)]?.color ??
options.custom_cards_3?.[item.course_id]?.color ??
options.custom_cards_3?.[item.plannable.course_id]?.color ??
"#cccccc";
let filter = "";
if (item.plannable.read_state == "read") {
filter = "filter: grayscale(40%);"
}
announcement.innerHTML = `
`;
});
}
function createConfettiBurst(targetElement, opts = {}) {
try {
if (options.todo_confetti === false) return;
const count = opts.count || 48;
const colors = opts.colors || ['#ff4d4f', '#ffc107', '#28a745', '#17a2b8', '#6f42c1', '#ff6b6b', '#ff8a65', '#ffd54f'];
const rect = targetElement.getBoundingClientRect();
const container = document.createElement('div');
container.className = 'canvasrefined-confetti-container';
container.style.position = 'fixed';
container.style.left = '0';
container.style.top = '0';
container.style.pointerEvents = 'none';
container.style.overflow = 'visible';
container.style.zIndex = '2147483647';
document.body.appendChild(container);
const originX = rect.left + rect.width / 2;
const originY = rect.top + rect.height * 0.35;
const particles = [];
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.className = 'canvasrefined-confetti';
const w = 4 + Math.floor(Math.random() * 7); // smaller pieces
const h = Math.max(3, Math.floor(w * (0.4 + Math.random() * 0.8)));
el.style.position = 'absolute';
el.style.width = w + 'px';
el.style.height = h + 'px';
el.style.background = colors[Math.floor(Math.random() * colors.length)];
el.style.left = (originX - w / 2) + 'px';
el.style.top = (originY - h / 2) + 'px';
el.style.opacity = '1';
el.style.borderRadius = Math.random() > 0.75 ? '50%' : '2px';
el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.18)';
el.style.transformOrigin = 'center center';
el.style.willChange = 'transform, opacity';
container.appendChild(el);
const duration = 850 + Math.floor(Math.random() * 500);
const delay = Math.floor(Math.random() * 90);
const spread = opts.spread || 110;
const horizontalBias = (Math.random() - 0.5) * 2;
// Arc stays lower and wider than the old cone-shaped burst.
const endX = originX + horizontalBias * (spread * (0.7 + Math.random() * 0.6));
const endY = originY - (16 + Math.random() * 34);
const ctrlX = originX + horizontalBias * (spread * 0.25) + (Math.random() - 0.5) * 14;
const ctrlY = originY - (30 + Math.random() * 55);
particles.push({
el,
delay,
duration,
originX,
originY,
ctrlX,
ctrlY,
endX,
endY,
rotate: (Math.random() * 260) - 130,
scale: 0.8 + Math.random() * 0.5,
});
}
const startTime = performance.now();
let rafId = null;
const animate = now => {
let active = false;
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
const elapsed = now - startTime - particle.delay;
if (elapsed < 0) {
active = true;
continue;
}
const progress = Math.min(1, elapsed / particle.duration);
const eased = 1 - Math.pow(1 - progress, 3);
const x = (1 - eased) * (1 - eased) * particle.originX + 2 * (1 - eased) * eased * particle.ctrlX + eased * eased * particle.endX;
const y = (1 - eased) * (1 - eased) * particle.originY + 2 * (1 - eased) * eased * particle.ctrlY + eased * eased * particle.endY;
particle.el.style.transform = `translate(${Math.round(x - particle.originX)}px, ${Math.round(y - particle.originY)}px) rotate(${particle.rotate * eased}deg) scale(${particle.scale * (1 - eased * 0.15)})`;
particle.el.style.opacity = String(1 - progress);
if (progress < 1) {
active = true;
} else {
particle.el.remove();
particles.splice(i, 1);
}
}
if (active) {
rafId = requestAnimationFrame(animate);
} else {
try { container.remove(); } catch (e) { /* ignore */ }
if (rafId) cancelAnimationFrame(rafId);
}
};
rafId = requestAnimationFrame(animate);
// cleanup container after animations
setTimeout(() => {
try { container.remove(); } catch (e) { /* ignore */ }
}, 2400);
} catch (e) {
console.error('confetti error', e);
}
}
function markAs(item, element) {
const csrfToken = CSRFtoken();
const completeState = item.planner_override ? !item.planner_override.marked_complete : true;
fetch(domain + "/api/v1/planner/overrides" + (item.planner_override ? "/" + item.planner_override.id : ""), {
method: item.planner_override ? "PUT" : "POST",
headers: {
"content-type":"application/json",
"accept":"application/json",
"X-CSRF-Token": csrfToken
},
body: JSON.stringify({
id: item.planner_override ? item.planner_override.id : null,
marked_complete: completeState,
plannable_id: item.plannable_id,
plannable_type: item.plannable_type
})
})
.then(resp => {
if (resp.status == 200 || resp.status == 201 || resp.status == 204) {
console.log("marked as complete");
item.planner_override = item.planner_override || {};
item.planner_override.marked_complete = completeState;
element.style.transform = "translate(100%)";
element.style.opacity = "0";
// fire confetti only when marking complete (not when unmarking)
if (completeState) {
try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
}
// update progress rings immediately so they animate while the item slides/fades
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder && typeof assignments?.then === 'function' && (options.todo_progress_rings === undefined || options.todo_progress_rings === true)) {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
? data.map(d => Object.assign({}, d)) // shallow copy
.filter(d => {
const itemCourseId = parseInt(d.course_id || d.context_id || d?.plannable?.course_id);
return itemCourseId === courseId;
})
: data.map(d => Object.assign({}, d));
// reflect the updated state for this item in the snapshot
for (let i = 0; i < scopedData.length; i++) {
if (scopedData[i].plannable_id === item.plannable_id && scopedData[i].plannable_type === item.plannable_type) {
scopedData[i].planner_override = scopedData[i].planner_override || {};
scopedData[i].planner_override.marked_complete = item.planner_override.marked_complete;
break;
}
}
renderProgressRings(progressPlaceholder, scopedData);
});
}
setTimeout(() => {
clearTodoList();
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
}, 400);
}
})
.catch(err => console.error("error marking as complete", err));
}
function createTodoViewMore(location, type) {
let viewMoreButton = makeElement("button", location, { "className": "canvasrefined-custom-btn canvasrefined-viewmore-btn", "textContent": "View More" });
//viewMoreButton.classList.add("canvasrefined-viewmore-btn");
const showMoreCount = 3;
viewMoreButton.addEventListener("click", function (e) {
if (type === "announcement") {
moreAnnouncementCount += showMoreCount;
} else {
moreAssignmentCount += showMoreCount;
}
loadBetterTodo();
});
}
// better todo init
function setupBetterTodo() {
if (options.better_todo !== true || isGradesPage()) return;
if (document.querySelector('#canvasrefined-todo-list')) return;
let list = document.querySelector("#right-side");
if (!list) return;
//if (!list || list.childElementCount === 0 || list.children[0].id === "canvasrefined-todo-list") return;
try {
/* save the feedback to append it later */
const feedback = list.querySelector(".events_list.recent_feedback");
list.textContent = "";
list = makeElement("div", list, { "className": "canvasrefined-todosidebar","id": "canvasrefined-todo-list"});
createTodoSections(list);
if (feedback) list.append(feedback);
} catch (e) {
logError(e);
}
}
function getSidebarScale() {
const rawScale = parseInt(options.sidebar_scale || 100);
if (isNaN(rawScale)) return 1;
return Math.max(0.7, Math.min(1.5, rawScale / 100));
}
function applySidebarScaleStyles(sidebarList) {
const scale = getSidebarScale();
sidebarList.style.setProperty("--bc-sidebar-icon-size", `${Math.round(20 * scale)}px`);
sidebarList.style.setProperty("--bc-sidebar-btn-height", `${Math.round(30 * scale)}px`);
sidebarList.style.setProperty("--bc-sidebar-btn-gap", `${Math.round(8 * scale)}px`);
sidebarList.style.setProperty("--bc-sidebar-label-size", `${Math.round(14 * scale)}px`);
}
async function setupBetterSidebar(mode = getSidebarLayoutMode()) {
if (!options.better_sidebar) return;
if (document.querySelector('#better-sidebar-container')) return;
let wrapper = document.querySelector("#wrapper");
if (!wrapper || betterSidebarLoading) return;
betterSidebarLoading = true;
try {
const layoutMode = mode === "course" || mode === "dash" ? mode : getSidebarLayoutMode();
let expanded = await getSidebarExpandedState(layoutMode);
const outerWrapper = document.getElementById("main");
outerWrapper?.style.setProperty("display", "flex", "important");
// document.getElementById("not_right_side").style.setProperty("display", "none", "important");
const leftSide = document.getElementById("left-side");
leftSide?.style.setProperty("opacity", "1");
leftSide?.style.setProperty("position", "static");
const mainWrapper = document.querySelector(".ic-Layout-contentWrapper");
if (!mainWrapper) return;
applyBetterSidebarLayoutFix();
mainWrapper.style.display = "flex";
mainWrapper.style.alignItems = "stretch";
mainWrapper.style.minWidth = "0";
const contentMain = document.querySelector(".ic-Layout-contentMain");
contentMain?.style.setProperty("flex", "1 1 auto");
contentMain?.style.setProperty("min-width", "0");
const notRightSide = document.getElementById("not_right_side");
if (notRightSide && isAccountsPage()) {
notRightSide.style.setProperty("width", "100%");
notRightSide.style.setProperty("max-width", "100%");
notRightSide.style.setProperty("min-width", "0");
}
if (layoutMode === "course" && leftSide) {
const rightSideWrapper = document.getElementById("right-side-wrapper");
const sectionTabs = document.getElementById("section-tabs");
leftSide.style.setProperty("padding-top", "0", "important");
leftSide.style.setProperty("padding-left", "0", "important");
if (sectionTabs) {
if (getCurrentCourseId() !== null || isProfilePage()) {
sectionTabs.style.setProperty("padding-top", "40px", "important");
} else {
sectionTabs.style.removeProperty("padding-top");
}
}
leftSide.style.flex = "0 0 250px";
leftSide.style.width = "250px";
leftSide.style.maxWidth = "250px";
if (notRightSide) {
notRightSide.style.display = "flex";
notRightSide.style.flex = "1 1 auto";
notRightSide.style.minWidth = "0";
}
if (rightSideWrapper) {
rightSideWrapper.style.flex = "0 0 280px";
rightSideWrapper.style.width = "280px";
rightSideWrapper.style.maxWidth = "280px";
}
contentMain?.style.setProperty("margin", "26px 38px 38px", "important");
contentMain?.style.setProperty("padding", "10px", "important");
contentMain?.style.setProperty("border-radius", "10px", "important");
contentMain?.style.setProperty("background", "color-mix(in srgb, var(--bcbackground-0) 45%, transparent)", "important");
contentMain?.style.setProperty("backdrop-filter", "blur(5px)", "important");
contentMain?.style.setProperty("-webkit-backdrop-filter", "blur(5px)", "important");
}
const sidebarParent = layoutMode === "course" && leftSide ? leftSide : mainWrapper;
if (layoutMode === "course" && leftSide) {
leftSide.style.display = "flex";
leftSide.style.flexDirection = "row";
leftSide.style.alignItems = "stretch";
leftSide.style.minWidth = "0";
leftSide.style.gap = "0";
}
document.querySelector(".ic-app-nav-toggle-and-crumbs")?.style.setProperty("display", "none");
if (layoutMode !== "course") {
document.getElementById("left-side")?.style.removeProperty("display");
}
if (layoutMode == "dash") {
document.getElementById("header")?.style.setProperty("display", "none");
}
else if (layoutMode == "course") {
document.getElementById("header")?.style.setProperty("display", "none");
}
let sidebarList = makeElement("div", sidebarParent, { id: "better-sidebar-container",
style: `display:flex;flex-direction:column;width:50px;justify-content:center;align-items:center;box-sizing:border-box;position:relative;background-color:var(--bcbackground-0);height:100vh;position:sticky;top:0;left:0;`
}, true);
let sidebarContent = makeElement("div", sidebarList, {
style: "display:flex;flex-direction:column;gap:20px;width:100%;flex:1;justify-content:flex-start;align-items:center;margin:40px;"
});
applySidebarScaleStyles(sidebarList);
let expander = makeElement("div", sidebarList, {
className: "better-sidebar-expander",
style: "display:flex;flex-direction:column;gap:0px;margin-top:auto;width:100%;justify-content:center;align-items:center;cursor:pointer;",
});
expander.innerHTML = `
`
sidebarList.dataset.expanded = expanded ? "true" : "false";
updateSidebar(expanded, sidebarList, expander);
setSidebarExpandedState(layoutMode, expanded);
requestAnimationFrame(() => {
populateSidebarFromNav(sidebarContent);
updateSidebar(expanded, sidebarList, expander);
});
expander.addEventListener("click", () => {
expanded = !expanded;
sidebarList.dataset.expanded = expanded ? "true" : "false";
setSidebarExpandedState(layoutMode, expanded);
updateSidebar(expanded, sidebarList, expander);
})
} catch (e) {
logError(e);
} finally {
betterSidebarLoading = false;
}
}
function createSidebarButton(text, url, parent, icon) {
let button = makeElement("a", parent, {
style: "width:40%;height:var(--bc-sidebar-btn-height,30px);cursor:pointer;text-align:center;text-decoration:none;display:inline-flex;justify-content:center;align-items:center;gap:var(--bc-sidebar-btn-gap,8px);color:var(--bctext-0) !important;font-weight:bold;position:relative;",
className: "canvasrefined-custom-btn better-sidebar-btn",
href: url,
});
button.innerHTML = `${icon ? `${icon}` : ``}`;
return button;
}
function getNavBadgeCount(item) {
const badge = item.querySelector(".menu-item__badge");
if (!badge) return 0;
const badgeText = badge.querySelector('[aria-hidden="true"]')?.textContent?.trim() || badge.textContent?.trim() || "";
const count = parseInt(badgeText, 10);
return Number.isFinite(count) && count > 0 ? count : 0;
}
function addSidebarButtonBadge(button, count) {
if (!button || !count) return;
button.querySelector(".better-sidebar-badge")?.remove();
makeElement("div", button, {
className: "better-sidebar-badge",
style: "position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background-color:#ff0000;color:white;font-size:11px;line-height:16px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;pointer-events:none;",
textContent: String(count),
});
}
function populateSidebarFromNav(sidebarContent) {
const excludeIds = ["global_nav_help_link", "global_nav_history_link"];
const customIcons = {
"global_nav_profile_link": ``,
"global_nav_dashboard_link": ``,
"global_nav_conversations_link": ``,
"global_nav_calendar_link": ``,
"global_nav_courses_link": ``,
"global_nav_groups_link": ``,
"globalNavExternalTool-69": ``,
};
const navMenu = document.getElementById("menu");
let hasDashboardButton = false;
if (navMenu) {
const menuItems = navMenu.querySelectorAll("a[id^='global_nav'], .globalNavExternalTool a");
menuItems.forEach(item => {
const itemId = item.id;
if (excludeIds.includes(itemId)) return;
const href = item.getAttribute("href");
let textEl = item.querySelector(".menu-item__text");
let text = textEl?.textContent?.trim();
// If text not found, try other sources
if (!text) {
text = item.getAttribute("aria-label")?.trim() ||
item.getAttribute("title")?.trim() ||
item.textContent?.trim();
}
if (!text || !href) return;
let icon = customIcons[itemId] || "";
if (!icon) {
const svg = item.querySelector("svg");
if (svg) {
icon = svg.outerHTML;
// Detect and scale down large viewBox SVGs
const viewBoxMatch = icon.match(/viewBox="([^"]+)"/);
if (viewBoxMatch) {
const [, viewBox] = viewBoxMatch;
const parts = viewBox.split(/\s+/);
const width = parseFloat(parts[2]);
const height = parseFloat(parts[3]);
// If viewBox is large, add fixed size to scale it down
if (width > 32 || height > 32) {
// Check if svg already has a style attribute
if (icon.includes('style="')) {
// Append to existing style
icon = icon.replace(/style="([^"]*)"/, `style="$1 width:20px;height:20px;flex-shrink:0;fill:white;stroke:white;"`);
} else {
// Add new style attribute
icon = icon.replace("