diff --git a/README.md b/README.md
index eeea504..b5ef572 100644
--- a/README.md
+++ b/README.md
@@ -79,13 +79,11 @@ Canvas Refined adds more with more to come!
- global canvas search
- fix darkmode fixer (and automatic)
- grade history with graph
-- more shapes for the better todo ring thing
## Extra features that might be added:
- card grade position, card outline
- streaks
- maybe find a way for faster loading
-- liquid glass theme?
- rotating background, time/weather background overlay
- custom side logo
- goals
@@ -96,10 +94,8 @@ Canvas Refined adds more with more to come!
## Community suggestions (maybe will be done at some point)
- when opening assignments it will show you "if you get a 0 on this your grade will be _"
-- quick modules button on cards
- module sorting (newest, oldest) (maybe grid view)
- grade leaderboard per class (opt in)
-- GPA preset by school name maybe
- sticky notes
## Dev Installation
diff --git a/html/popup.html b/html/popup.html
index 8285159..50a9d62 100644
--- a/html/popup.html
+++ b/html/popup.html
@@ -277,6 +277,14 @@
+
+
+
+
+
+
+
+
Max items to show:
@@ -362,14 +370,6 @@
Full Width Fix
-
-
-
-
-
-
-
Hide recent feedback
-
diff --git a/js/background.js b/js/background.js
index 1ce223e..d1bfa25 100644
--- a/js/background.js
+++ b/js/background.js
@@ -74,7 +74,6 @@ chrome.runtime.onInstalled.addListener(function () {
"card_overdues": false,
"relative_dues": false,
"equal_height_cards": false,
- "hide_feedback": false,
"hide_new_canvas": true,
"quiz_safe_mode": false,
"dark_mode_fix": [],
diff --git a/js/content.js b/js/content.js
index f2d5dd7..126e684 100644
--- a/js/content.js
+++ b/js/content.js
@@ -1,5 +1,31 @@
const domain = window.location.origin;
-const current_page = window.location.pathname;
+let current_page = window.location.pathname;
+
+// Canvas' "New Canvas" UI navigates client-side via history.pushState/
+// replaceState without a full page reload. current_page is captured once at
+// document_start, so without this hook it goes stale and page-specific features
+// (Back to Assignment button, sequence-footer removal, profile logout button)
+// never activate when the user clicks into a page instead of loading it directly.
+function setupNavigationListener() {
+ const update = () => {
+ const next = window.location.pathname;
+ if (next === current_page) return;
+ current_page = next;
+ // Re-run the page-scoped watchers so they evaluate against the new URL.
+ watchSequenceFooter();
+ watchSubmissionPageButton();
+ watchProfileLogoutPageButton();
+ };
+ for (const method of ["pushState", "replaceState"]) {
+ const orig = history[method];
+ history[method] = function (...args) {
+ const ret = orig.apply(this, args);
+ update();
+ return ret;
+ };
+ }
+ window.addEventListener("popstate", update);
+}
function getCurrentCourseId() {
const match = current_page.match(/^\/courses\/(\d+)(?:\/|$)/);
@@ -104,7 +130,10 @@ function ensureProfileLogoutPageButton() {
}
function watchProfileLogoutPageButton() {
- if (!isProfilePage()) return;
+ if (!isProfilePage()) {
+ document.getElementById("canvasrefined-profile-logout")?.remove();
+ return;
+ }
if (ensureProfileLogoutPageButton()) return;
if (profileLogoutButtonObserver) return;
@@ -129,7 +158,12 @@ function ensureSubmissionPageButton() {
if (!assignmentLink) return false;
const content = document.getElementById("content");
if (!content) return false;
- if (content.querySelector("#canvasrefined-assignment-return")) return true;
+ const existing = content.querySelector("#canvasrefined-assignment-return");
+ if (existing) {
+ // Keep the href in sync when navigating between submission pages via SPA routing.
+ existing.href = assignmentLink;
+ return true;
+ }
addSubmissionPageButton();
return Boolean(content.querySelector("#canvasrefined-assignment-return"));
}
@@ -168,7 +202,11 @@ function watchSequenceFooter() {
}
function watchSubmissionPageButton() {
- if (!getSubmissionAssignmentLink()) return;
+ if (!getSubmissionAssignmentLink()) {
+ // Navigated away from a submission page; remove any stale button left in #content.
+ document.getElementById("canvasrefined-assignment-return")?.remove();
+ return;
+ }
if (ensureSubmissionPageButton()) return;
if (submissionPageButtonObserver) return;
@@ -608,6 +646,7 @@ function startExtension() {
watchSequenceFooter();
watchSubmissionPageButton();
watchProfileLogoutPageButton();
+ setupNavigationListener();
setupQuizSafeModeBanner();
@@ -640,6 +679,12 @@ function applyOptionsChanges(changes) {
case "device_dark":
toggleDarkMode();
applyTodoAlternateColors();
+ // "Ignore card colors" picks black vs. theme text color based on dark
+ // mode, so re-render the Better Todo list to keep it in sync.
+ if (options.todo_ignore_card_colors && options.better_todo && document.getElementById("better-todo-main")) {
+ clearTodoList();
+ createTodoSections(document.querySelector("#canvasrefined-todo-list"));
+ }
break;
case "todo_alternate_colors":
applyTodoAlternateColors();
@@ -688,6 +733,16 @@ function applyOptionsChanges(changes) {
equalizeCardHeights();
break;
case "custom_cards":
+ customizeCards();
+ // Hiding/unhiding a card changes which courses appear in the todo
+ // list and the progress display, so re-render them immediately.
+ if (options.better_todo && document.getElementById("better-todo-main")) {
+ moreAnnouncementCount = 0;
+ moreAssignmentCount = 0;
+ clearTodoList();
+ createTodoSections(document.querySelector("#canvasrefined-todo-list"));
+ }
+ break;
case "custom_cards_2":
case "custom_cards_3":
customizeCards();
@@ -700,6 +755,8 @@ function applyOptionsChanges(changes) {
// case "todo_overdues":
case "todo_hide_feedback":
case "todo_full_height":
+ case "todo_ignore_card_colors":
+ case "todo_remove_icons":
case "custom_cards_3":
moreAnnouncementCount = 0;
moreAssignmentCount = 0;
@@ -723,7 +780,6 @@ function applyOptionsChanges(changes) {
case "remlogo":
case "disable_color_overlay":
case "condensed_cards":
- case "hide_feedback":
case "full_width":
case "center_cards":
case "custom_styles":
@@ -802,12 +858,7 @@ function applyOptionsChanges(changes) {
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;
+ const scopedData = getTodoScopedData(data, courseId);
renderProgressRings(placeholder, scopedData);
});
}
@@ -1302,12 +1353,11 @@ function recieveMessage(request, sender, sendResponse) {
switch (request.message) {
case ("getCards"):
if (options["card_method_dashboard"] === true) {
- getCardsFromDashboard();
+ getCardsFromDashboard().then(() => sendResponse(true));
} else {
- getCards();
+ getCards().then(() => sendResponse(true));
}
- sendResponse(true);
- break;
+ return true; // keep the message channel open for async sendResponse
case ("setcolors"): changeColorPreset(request.options); sendResponse(true); break;
case ("getcolors"): sendResponse(getCardColors()); break;
case ("inspect"): sendResponse(inspectDarkMode(true)); break;
@@ -1396,6 +1446,7 @@ function getCardColors() {
function getCardsFromDashboard() {
console.log("getting cards from dashboard")
const dashboard_cards = document.querySelectorAll(".ic-DashboardCard");
+ return new Promise(resolve => {
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"] || {};
@@ -1409,7 +1460,7 @@ function getCardsFromDashboard() {
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 };
+ cards[id] = { "default": card.querySelector(".ic-DashboardCard__header-subtitle").textContent.substring(0, 20), "fullName": card.querySelector(".ic-DashboardCard__header-title")?.textContent?.trim() || "", "name": "", "code": "", "img": "", "hidden": false, "weight": "regular", "credits": 1, "eid": 100000 - count, "gr": null };
let links = [];
for (let i = 0; i < 4; i++) {
@@ -1418,6 +1469,13 @@ function getCardsFromDashboard() {
cards_2[id] = { "links": links };
cards_3[id] = { "url": domain };
+ } else {
+ // backfill full name for cards created before this field existed
+ const full = card.querySelector(".ic-DashboardCard__header-title")?.textContent?.trim() || "";
+ if (full && cards[id].fullName !== full) {
+ cards[id].fullName = full;
+ newCards = true;
+ }
}
count++;
});
@@ -1452,15 +1510,17 @@ function getCardsFromDashboard() {
console.log("Error getting dashboard cards\n", e);
logError(e);
} finally {
- if(newCards !== true) return;
+ if(newCards !== true) { resolve(); return; }
console.log(newCards ? "new cards found" : "");
- chrome.storage.sync.set({ "custom_cards": cards, "custom_cards_2": cards_2, "custom_cards_3": cards_3 });
+ chrome.storage.sync.set({ "custom_cards": cards, "custom_cards_2": cards_2, "custom_cards_3": cards_3 }, () => resolve());
}
});
+ });
}
async function getCards(api = null) {
let dashboard_cards = api ? api : await getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}per_page=100`);
+ await new Promise(resolve => {
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"] || {};
@@ -1479,10 +1539,11 @@ async function getCards(api = null) {
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 };
+ cards[id] = { "default": card.course_code.substring(0, 20), "fullName": card.name || card.course_code || "", "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].fullName = card.name || card.course_code || cards[id].fullName || "";
cards[id].eid = card.enrollment_term_id || 0;
if (!cards[id].code) cards[id].code = "";
}
@@ -1530,9 +1591,10 @@ async function getCards(api = null) {
} 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 } : {});
+ chrome.storage.sync.set(newCards ? { "custom_cards": cards, "custom_cards_2": cards_2, "custom_cards_3": cards_3 } : {}, () => resolve());
}
});
+ });
}
/*
@@ -1643,6 +1705,46 @@ function courseRingLabel(courseId) {
return card?.default || `Course ${courseId}`;
}
+// Planner items for courses the user has hidden from their dashboard should
+// not appear in the Better Todo list or its progress display. Personal
+// tasks (planner notes with no course) are always kept.
+function isCourseHidden(courseId) {
+ if (courseId === undefined || courseId === null) return false;
+ const cards = options.custom_cards || {};
+ const card = cards[String(courseId)] || cards[courseId];
+ return !!card && card.hidden === true;
+}
+
+function filterHiddenCourses(data) {
+ return data.filter(item => {
+ const cid = item.course_id || item.context_id || item?.plannable?.course_id;
+ return !isCourseHidden(cid);
+ });
+}
+
+// Build scoped data for the Better Todo list: drop hidden courses, then (on
+// a course page) restrict to the current course.
+function getTodoScopedData(data, courseId) {
+ const visible = filterHiddenCourses(data);
+ if (!courseId) return visible;
+ return visible.filter(item => {
+ const itemCourseId = parseInt(item.course_id || item.context_id || item?.plannable?.course_id);
+ return itemCourseId === courseId;
+ });
+}
+
+// Returns a Map of courseId (string) -> dashboard position index, read from
+// the live dashboard card DOM order. Empty when not on the dashboard. Used
+// to order the progress display the same way the user ordered their cards.
+function getDashboardCourseOrder() {
+ const order = new Map();
+ document.querySelectorAll('.ic-DashboardCard').forEach((card, idx) => {
+ const id = getCardId(card);
+ if (id && id !== -1 && !order.has(String(id))) order.set(String(id), idx);
+ });
+ return order;
+}
+
// Mode "rings": concentric rings, one per course, each filled by completion.
function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent) {
const containerWidth = wrapper.clientWidth || 240;
@@ -2066,8 +2168,17 @@ function renderProgressRings(container, scopedData) {
if (!entries.length) { container.innerHTML = ""; return; }
- // sort by total desc and limit to 6 courses
- entries.sort((a, b) => b.total - a.total);
+ // Order courses to match the user's dashboard card order. Courses that
+ // aren't on the dashboard (personal tasks, dropped courses) sort after
+ // dashboard courses, keeping their relative order; ties fall back to
+ // most assignments first so the display stays stable.
+ const dashboardOrder = getDashboardCourseOrder();
+ entries.sort((a, b) => {
+ const ai = dashboardOrder.has(a.courseId) ? dashboardOrder.get(a.courseId) : Infinity;
+ const bi = dashboardOrder.has(b.courseId) ? dashboardOrder.get(b.courseId) : Infinity;
+ if (ai !== bi) return ai - bi;
+ return b.total - a.total;
+ });
const shown = entries.slice(0, 6);
const totalAll = shown.reduce((s, e) => s + e.total, 0);
@@ -2690,12 +2801,7 @@ async function createTodoSections(location) {
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;
+ const scopedData = getTodoScopedData(data, courseId);
// Clicking a color in the progress display filters the list to that
// one class. The filter only makes sense where multiple classes show
@@ -3072,6 +3178,16 @@ function populateAssignments(iscompleted = false) {
options.custom_cards_3?.[item.plannable.course_id]?.color ??
"#cccccc";
+ // "Ignore card colors" (Better Todo List): when on, the class name is
+ // rendered black in light mode or the theme text color in dark mode
+ // instead of the course's card color.
+ const classNameColor = options.todo_ignore_card_colors
+ ? (options.dark_mode === true ? "var(--bctext-0)" : "#000000")
+ : courseColor;
+ // "Remove icons" (Better Todo List): when on, the task-type icon is
+ // omitted from the colored strip on the left of each task.
+ const removeIcons = options.todo_remove_icons === true;
+
const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true;
const taskHref = isCustomTask ? customTaskHref(item) : (domain + item.html_url);
const editButtonSvg = isCustomTask
@@ -3079,7 +3195,7 @@ function populateAssignments(iscompleted = false) {
: "";
const iconSize = isCustomTask ? 26 : 20;
const iconLeftOffset = isCustomTask ? 2 : 5;
- const taskIcon = isCustomTask
+ const taskIcon = removeIcons ? "" : isCustomTask
? ``
@@ -3100,7 +3216,7 @@ function populateAssignments(iscompleted = false) {