lots of stuff

This commit is contained in:
Guy Sandler 2026-08-26 18:42:04 -07:00
parent e1fdfcd41d
commit 11bdfb0b3b
5 changed files with 241 additions and 73 deletions

View File

@ -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

View File

@ -277,6 +277,14 @@
<input type="checkbox" id="todo_alternate_colors" name="todo_alternate_colors">
<label for="todo_alternate_colors" class="sub-text" data-i18n="todo_alternate_colors">Alternate colors</label>
</div>
<div class="sub-option">
<input type="checkbox" id="todo_ignore_card_colors" name="todo_ignore_card_colors">
<label for="todo_ignore_card_colors" class="sub-text">Ignore card colors</label>
</div>
<div class="sub-option">
<input type="checkbox" id="todo_remove_icons" name="todo_remove_icons">
<label for="todo_remove_icons" class="sub-text">Remove icons</label>
</div>
<div style="margin-top: 5px">
<span class="sub-text" data-i18n="max_items">Max items to show: </span><span
id="numTodoItems"></span>
@ -362,14 +370,6 @@
</div>
<span class="option-name" data-i18n="full_width">Full Width Fix</span>
</div>
<div class="option" id="hide_feedback">
<input type="radio" id="off" name="hide_feedback">
<input type="radio" id="on" name="hide_feedback">
<div class="slider">
<div class="sliderknob"></div>
<div class="sliderbg"></div>
</div><span class="option-name" data-i18n="hide_feedback">Hide recent feedback</span>
</div>
<div class="option" id="tab_icons">
<input type="radio" id="off" name="tab_icons">
<input type="radio" id="on" name="tab_icons">

View File

@ -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": [],

View File

@ -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
? `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;display:block;">
<path d="M19.8201 14H15.6001C15.04 14 14.76 14 14.5461 14.109C14.3579 14.2049 14.2049 14.3578 14.1091 14.546C14.0001 14.7599 14.0001 15.0399 14.0001 15.6V19.82M20 12.7269V7.2C20 6.0799 20 5.51984 19.782 5.09202C19.5903 4.71569 19.2843 4.40973 18.908 4.21799C18.4802 4 17.9201 4 16.8 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40973 4.40973 4.71569 4.21799 5.09202C4 5.51984 4 6.0799 4 7.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H12.9496C13.4578 20 13.7118 20 13.9498 19.9407C14.1608 19.8882 14.3618 19.8016 14.5449 19.6844C14.7515 19.5522 14.926 19.3675 15.2751 18.9983L19.1254 14.9252C19.4486 14.5833 19.6101 14.4124 19.7255 14.2156C19.8278 14.041 19.903 13.8519 19.9486 13.6548C20 13.4325 20 13.1973 20 12.7269Z" stroke="var(--cr-todo-icon)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>`
@ -3100,7 +3216,7 @@ function populateAssignments(iscompleted = false) {
</div>
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;position:relative;">
<div style="display:flex;flex-direction:column;gap:3px;">
<span style="color:${courseColor};font-size:12px;margin-top:-2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;box-sizing:border-box;padding-right:22px;">${item.context_name}</span>
<span style="color:${classNameColor};font-size:12px;margin-top:-2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;box-sizing:border-box;padding-right:22px;">${item.context_name}</span>
<a href="${taskHref}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
</div>
@ -3181,6 +3297,13 @@ function populateAnnouncements() {
options.custom_cards_3?.[item.plannable.course_id]?.color ??
"#cccccc";
// "Ignore card colors": black in light mode, theme text color in dark.
const classNameColor = options.todo_ignore_card_colors
? (options.dark_mode === true ? "var(--bctext-0)" : "#000000")
: courseColor;
// "Remove icons": drop the announcement icon from the colored strip.
const removeIcons = options.todo_remove_icons === true;
let filter = "";
if (item.plannable.read_state == "read") {
filter = "filter: grayscale(40%);"
@ -3190,17 +3313,17 @@ function populateAnnouncements() {
<div style="display:flex;align-items:center;gap:5px;width:100%;height:60px;background:var(--bcbackground-2);border-radius:5px;${filter}">
<div style="width:40px;display:flex;align-items:center;justify-content:center;background-color:${courseColor};height:100%;border-radius:5px 0 0 5px;">
<div style="width:23px;height:23px;display:flex;margin-left:0px;">
<svg fill="var(--cr-todo-icon)" viewBox="0 0 1920 1920" xmlns="http://www.w3.org/2000/svg" style="transition:all .3s ease;">
${removeIcons ? "" : `<svg fill="var(--cr-todo-icon)" viewBox="0 0 1920 1920" xmlns="http://www.w3.org/2000/svg" style="transition:all .3s ease;">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M1587.162 31.278c11.52-23.491 37.27-35.689 63.473-29.816 25.525 6.099 43.483 28.8 43.483 55.002V570.46C1822.87 596.662 1920 710.733 1920 847.053c0 136.32-97.13 250.503-225.882 276.705v513.883c0 26.202-17.958 49.016-43.483 55.002a57.279 57.279 0 0 1-12.988 1.468c-21.12 0-40.772-11.745-50.485-31.171C1379.238 1247.203 964.18 1242.347 960 1242.347H564.706v564.706h87.755c-11.859-90.127-17.506-247.003 63.473-350.683 52.405-67.087 129.657-101.082 229.948-101.082v112.941c-64.49 0-110.57 18.861-140.837 57.487-68.781 87.868-45.064 263.83-30.269 324.254 4.18 16.828.34 34.673-10.277 48.34-10.73 13.665-27.219 21.684-44.499 21.684H508.235c-31.171 0-56.47-25.186-56.47-56.47v-621.177h-56.47c-155.747 0-282.354-126.607-282.354-282.353v-56.47h-56.47C25.299 903.523 0 878.336 0 847.052c0-31.172 25.299-56.471 56.47-56.471h56.471v-56.47c0-155.634 126.607-282.354 282.353-282.354h564.593c16.941-.112 420.48-7.002 627.275-420.48Zm-5.986 218.429c-194.71 242.371-452.216 298.164-564.705 311.04v572.724c112.489 12.876 369.995 68.556 564.705 311.04ZM903.53 564.7H395.294c-93.402 0-169.412 76.01-169.412 169.411v225.883c0 93.402 76.01 169.412 169.412 169.412H903.53V564.7Zm790.589 123.444v317.93c65.618-23.379 112.94-85.497 112.94-159.021 0-73.525-47.322-135.53-112.94-158.909Z" fill-rule="evenodd"></path>
</g>
</svg>
</svg>`}
</div>
</div>
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;">
<div style="display:flex;flex-direction:column;gap:3px;">
<span style="color:${courseColor};font-size:12px;margin-top:-2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;box-sizing:border-box;padding-right:22px;">${item.context_name}</span>
<span style="color:${classNameColor};font-size:12px;margin-top:-2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;box-sizing:border-box;padding-right:22px;">${item.context_name}</span>
<a href="${domain + item.html_url}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
</div>
@ -3353,13 +3476,7 @@ function markAs(item, element) {
if (progressPlaceholder && typeof assignments?.then === 'function' && progressRingsEnabled()) {
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));
const scopedData = getTodoScopedData(data.map(d => Object.assign({}, d)), courseId);
// reflect the updated state for this item in the snapshot
for (let i = 0; i < scopedData.length; i++) {
@ -5249,7 +5366,6 @@ function applyAestheticChanges() {
if (options.condensed_cards === true) style.textContent += ".ic-DashboardCard__header_hero {height:60px!important}.ic-DashboardCard__header-subtitle, .ic-DashboardCard__header-term{display:none}";
if (options.remlogo === true) style.textContent += ".ic-app-header__logomark-container{display:none}";
if (options.disable_color_overlay === true) style.textContent += ".ic-DashboardCard__header_hero{opacity: 0!important} .ic-DashboardCard__header-button-bg{opacity: 1!important}";
if (options.hide_feedback === true) style.textContent += ".recent_feedback {display: none}";
if (options.full_width === true) style.textContent += "#wrapper,.ic-Layout-wrapper{max-width:100%!important}";
if (options.center_cards === true) style.textContent += ".ic-DashboardCard__box__container{display:flex!important;flex-wrap:wrap!important;justify-content:center!important;align-items:flex-start!important}";
if (options.customCardStyles === true) {

View File

@ -1,4 +1,4 @@
const syncedSwitches = ['remind', 'tab_icons', 'hide_feedback', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards', 'quiz_safe_mode'];
const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards', 'quiz_safe_mode'];
const syncedSubOptions = [
"todo_hide_feedback",
"todo_full_height",
@ -21,6 +21,8 @@ const syncedSubOptions = [
"todo_hr24",
"todo_separate_scrollbar",
"todo_alternate_colors",
"todo_ignore_card_colors",
"todo_remove_icons",
"grade_hover",
// "hide_completed",
"num_todo_items",
@ -51,9 +53,9 @@ const localSwitches = [];
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding"];
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_feedback", "tab_icons"];
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "tab_icons"];
const exportSidebar = ["better_sidebar", "sidebar_scale"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_timeframe", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "hover_preview"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_timeframe", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "hover_preview"];
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted"];
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "card_transparency", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur", "card_opacity", "card_blur"];
// Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,
@ -112,6 +114,8 @@ const defaultOptions = {
"todo_hr24": false,
"todo_separate_scrollbar": false,
"todo_alternate_colors": false,
"todo_ignore_card_colors": false,
"todo_remove_icons": false,
"condensed_cards": false,
"center_cards": false,
"custom_cards": {},
@ -145,7 +149,6 @@ const defaultOptions = {
"card_overdues": false,
"relative_dues": false,
"equal_height_cards": false,
"hide_feedback": false,
"hide_new_canvas": true,
"quiz_safe_mode": false,
"dark_mode_fix": [],
@ -557,14 +560,6 @@ function toggleCardTransparencyOptions() {
if (blurRow) blurRow.style.display = on ? "" : "none";
}
// Hide the standalone feedback toggle when Better Todo's own sub-option replaces it.
function toggleBetterTodoSubOptions(betterTodoOn) {
const hideFeedbackEl = document.getElementById("hide_feedback");
if (hideFeedbackEl) {
hideFeedbackEl.style.display = betterTodoOn ? "none" : "flex";
}
}
// "Alternate colors" (Better Todo List sub-option) only makes sense in light
// mode, so hide its checkbox whenever dark mode is on.
function toggleAlternateColorsVisibility(darkModeOn) {
@ -619,6 +614,9 @@ function setupFeatureSearch(menu) {
if (label) return label.textContent.trim();
const st = sub.querySelector(".sub-text");
if (st) return st.textContent.trim();
// Bare <span> label (e.g. Export Settings checkboxes have no label/.sub-text).
const span = sub.querySelector("span");
if (span) return span.textContent.trim();
return "";
}
@ -769,6 +767,50 @@ function setupFeatureSearch(menu) {
add({ key: "color:" + (btnId || "?") + ":" + text, text, el: h, action: () => goToTabElement(h) });
});
// Tab: range sliders that live in dedicated rows (Opacity & Blur, Image scale).
// These aren't .sub-option, so the generic blocks above miss them.
document.querySelectorAll(".tab .opacity-slider-row, .tab .background-scale-row").forEach(row => {
if (shouldSkip(row)) return;
const label = row.querySelector(".sub-text");
const input = row.querySelector("input[type='range']");
if (!label || !input) return;
const text = label.textContent.trim();
if (!text) return;
const btnId = tabElToBtnId.get(row.closest(".tab"));
add({ key: "slider:" + (btnId || "?") + ":" + (input.id || text), text, el: row, action: () => goToTabElement(row) });
});
// Tab: preset buttons (dark mode presets + popular color palettes).
document.querySelectorAll(".tab .preset-button").forEach(btn => {
if (shouldSkip(btn)) return;
const text = btn.textContent.trim().replace(/\s+/g, " ");
if (!text || text === "placeholder") return;
const btnId = tabElToBtnId.get(btn.closest(".tab"));
add({ key: "preset:" + (btnId || "?") + ":" + (btn.id || text), text, el: btn, action: () => goToTabElement(btn) });
});
// Tab: standalone action buttons with distinct labels (GPA scale presets, revert colors).
["gpa-plus-minus", "gpa-by-letter", "revert-colors", "clearCustomBackground"].forEach(id => {
const btn = document.getElementById(id);
if (!btn || shouldSkip(btn)) return;
const text = btn.textContent.trim();
if (!text) return;
add({ key: "action:" + id, text, el: btn, action: () => goToTabElement(btn) });
});
// Tab: checkbox/radio controls whose label is a bare sibling span and that
// aren't wrapped in .sub-option (e.g. "Get Active Cards From Dashboard",
// sidebar background "Solid/Gradient/Image").
document.querySelectorAll(".tab input[type='checkbox'], .tab input[type='radio']").forEach(input => {
if (!input.id) return;
if (input.closest(".sub-option") || input.closest(".option")) return;
if (shouldSkip(input)) return;
const text = (input.parentElement && input.parentElement.textContent.trim()) || "";
if (!text) return;
const btnId = tabElToBtnId.get(input.closest(".tab"));
add({ key: "control:" + (btnId || "?") + ":" + input.id, text, el: input, action: () => goToTabElement(input) });
});
return index;
}
@ -900,6 +942,8 @@ function setup() {
"todo_hr24",
"todo_separate_scrollbar",
"todo_alternate_colors",
"todo_ignore_card_colors",
"todo_remove_icons",
"grade_hover",
// "hide_completed",
"hover_preview",
@ -1058,9 +1102,6 @@ function setup() {
if (option === "better_sidebar") {
toggleBetterSidebarSubOptions(status);
}
if (option === "better_todo") {
toggleBetterTodoSubOptions(status);
}
if (option === "dark_mode") {
toggleAlternateColorsVisibility(status);
}
@ -1068,7 +1109,6 @@ function setup() {
});
});
toggleBetterSidebarSubOptions(sync["better_sidebar"] === true);
toggleBetterTodoSubOptions(sync["better_todo"] === true);
["gpa_calc", "assignments_due", "better_todo", "auto_dark"].forEach(opt => {
toggleSubOptionsVisibility(opt, sync[opt] === true);
});
@ -1715,6 +1755,9 @@ function saveCurrentTheme() {
"todo_timeframe": current["todo_timeframe"],
"todo_hr24": current["todo_hr24"],
"todo_separate_scrollbar": current["todo_separate_scrollbar"],
"todo_alternate_colors": current["todo_alternate_colors"],
"todo_ignore_card_colors": current["todo_ignore_card_colors"],
"todo_remove_icons": current["todo_remove_icons"],
"better_sidebar": current["better_sidebar"],
"sidebar_scale": current["sidebar_scale"],
"imageSize": current["imageSize"],
@ -1983,8 +2026,18 @@ function setCustomImage(key, val) {
updateCards(key, { "img": val });
}
function displayAdvancedCards() {
sendFromPopup("getCards");
async function displayAdvancedCards() {
// Sync from Canvas only when needed: when there are no cards yet (fresh
// install) or when older cards predate the stored fullName field. Once
// every card has a fullName this is a no-op and the grid renders instantly.
const needsSync = await new Promise(resolve => {
chrome.storage.sync.get("custom_cards", s => {
const c = s.custom_cards || {};
const ids = Object.keys(c);
resolve(!ids.length || ids.some(id => !c[id].fullName));
});
});
if (needsSync) await sendFromPopup("getCards");
chrome.storage.sync.get(["custom_cards", "custom_cards_2"], storage => {
@ -2024,12 +2077,14 @@ function displayAdvancedCards() {
}
});
// showCardEditMenu sets an inline display:none on the grid; clear it
// so reopening the menu shows the cards again (CSS default = grid).
cardGrid.style.display = "";
const editMenu = document.getElementById("card-edit-menu");
if (editMenu) {
editMenu.style.display = "none";
}
});
sendFromPopup("getCards");
}
function createCourseButton(courseId, courseData) {
@ -2037,6 +2092,7 @@ function createCourseButton(courseId, courseData) {
button.className = "course-card-button";
const displayName =
courseData.name ||
courseData.fullName ||
courseData.default ||
courseData.code ||
`Course ${courseId}`;
@ -2064,6 +2120,7 @@ function showCardEditMenu(courseId, courseData) {
const displayName =
courseData.name ||
courseData.fullName ||
courseData.default ||
courseData.code ||
`Course ${courseId}`;