mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 22:07:18 +01:00
card grid: flex grid + gpa calc comp + hidden mode + small gradient fix
This commit is contained in:
parent
a4a4db7fb9
commit
b8bcf821e0
@ -6,8 +6,9 @@ h1, h2, p, body{margin:0;padding:0}
|
|||||||
h1 {font-size:30px;}
|
h1 {font-size:30px;}
|
||||||
h2 {font-size:24px;}
|
h2 {font-size:24px;}
|
||||||
.option input{display:none;}
|
.option input{display:none;}
|
||||||
.options input:nth-of-type(1).checked ~ .slider .sliderknob{background-color:#e1e1e12e;}
|
/* Knob state for every big toggle (main options AND tab pages like Report issue) */
|
||||||
.options input:nth-of-type(2).checked ~ .slider .sliderknob{transform:translate(18px);background-color:#8dd28d;}
|
.option input:nth-of-type(1).checked ~ .slider .sliderknob{background-color:#e1e1e12e;}
|
||||||
|
.option input:nth-of-type(2).checked ~ .slider .sliderknob{transform:translate(18px);background-color:#8dd28d;}
|
||||||
.option{display:flex;align-items:center;}
|
.option{display:flex;align-items:center;}
|
||||||
.option span{margin-left:8px;}
|
.option span{margin-left:8px;}
|
||||||
.main{position: relative;padding: 20px}
|
.main{position: relative;padding: 20px}
|
||||||
|
|||||||
@ -880,6 +880,10 @@
|
|||||||
<input type="checkbox" id="card_grid_center_rows" name="card_grid_center_rows">
|
<input type="checkbox" id="card_grid_center_rows" name="card_grid_center_rows">
|
||||||
<label for="card_grid_center_rows" class="sub-text">Center uneven rows</label>
|
<label for="card_grid_center_rows" class="sub-text">Center uneven rows</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sub-option" style="margin-top:10px">
|
||||||
|
<input type="checkbox" id="card_grid_flex" name="card_grid_flex">
|
||||||
|
<label for="card_grid_flex" class="sub-text">Flexible grid</label>
|
||||||
|
</div>
|
||||||
<p id="card-grid-warning" style="display:none;margin:10px 0 0;color:#ffd166;font-size:12px;font-weight:600;"></p>
|
<p id="card-grid-warning" style="display:none;margin:10px 0 0;color:#ffd166;font-size:12px;font-weight:600;"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1236,6 +1240,24 @@
|
|||||||
<button class="big-button back-btn" data-i18n="back">Back</button>
|
<button class="big-button back-btn" data-i18n="back">Back</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="option-container">You can also email <b style="font-weight:600">sandlerguy5@gmail.com</b> for any issues or feedback. Thank you!</p>
|
<p class="option-container">You can also email <b style="font-weight:600">sandlerguy5@gmail.com</b> for any issues or feedback. Thank you!</p>
|
||||||
|
<div class="option-container">
|
||||||
|
<h3 class="header-small">Privacy</h3>
|
||||||
|
<div class="option" id="hide_personal_details">
|
||||||
|
<input type="radio" id="off" name="hide_personal_details">
|
||||||
|
<input type="radio" id="on" name="hide_personal_details">
|
||||||
|
<div class="slider">
|
||||||
|
<div class="sliderknob"></div>
|
||||||
|
<div class="sliderbg"></div>
|
||||||
|
</div><span class="option-name">Hide personal details</span>
|
||||||
|
</div>
|
||||||
|
<p class="sub-text" style="margin-top:6px;">Replaces course names, teacher names, grades and assignment titles with fake data from a pregenerated list, on the dashboard cards, the Better Todo List and recent feedback. Perfect for screenshots — nothing real is shown. Turn it off to restore everything (no refresh needed).</p>
|
||||||
|
<div class="sub-options">
|
||||||
|
<div class="sub-option">
|
||||||
|
<input type="checkbox" id="hide_course_images" name="hide_course_images">
|
||||||
|
<label for="hide_course_images" class="sub-text">Temporarily remove all course images</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p id="dev-alert"></p>
|
<p id="dev-alert"></p>
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
<h3 class="header-small">Storage reset</h3>
|
<h3 class="header-small">Storage reset</h3>
|
||||||
|
|||||||
495
js/content.js
495
js/content.js
@ -941,6 +941,7 @@ function startExtension() {
|
|||||||
watchSequenceFooter();
|
watchSequenceFooter();
|
||||||
watchProfileLogoutPageButton();
|
watchProfileLogoutPageButton();
|
||||||
watchGradeAnalytics();
|
watchGradeAnalytics();
|
||||||
|
setupPersonalDetailsHiding();
|
||||||
|
|
||||||
setupQuizSafeModeBanner();
|
setupQuizSafeModeBanner();
|
||||||
|
|
||||||
@ -1087,6 +1088,26 @@ function applyOptionsChanges(changes) {
|
|||||||
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "hide_personal_details":
|
||||||
|
// Starts/stops the DOM watcher; with it off, restores the real
|
||||||
|
// text/attributes stashed in dataset markers.
|
||||||
|
setupPersonalDetailsHiding();
|
||||||
|
// Todo item text, card grades and card assignment names are baked
|
||||||
|
// in at render time, so re-render them to swap fake/real data.
|
||||||
|
if (options.better_todo && document.getElementById("better-todo-main")) {
|
||||||
|
moreAnnouncementCount = 0;
|
||||||
|
moreAssignmentCount = 0;
|
||||||
|
clearTodoList();
|
||||||
|
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
||||||
|
}
|
||||||
|
if (!grades) getGrades();
|
||||||
|
insertGrades();
|
||||||
|
cardAssignments = preloadAssignmentEls();
|
||||||
|
loadCardAssignments();
|
||||||
|
break;
|
||||||
|
case "hide_course_images":
|
||||||
|
applyCourseImageHiding();
|
||||||
|
break;
|
||||||
case "gpa_calc":
|
case "gpa_calc":
|
||||||
case "gpa_calc_prepend":
|
case "gpa_calc_prepend":
|
||||||
case "gpa_calc_weighted":
|
case "gpa_calc_weighted":
|
||||||
@ -1174,6 +1195,7 @@ function applyOptionsChanges(changes) {
|
|||||||
case "card_grid_column_gap":
|
case "card_grid_column_gap":
|
||||||
case "card_grid_row_gap":
|
case "card_grid_row_gap":
|
||||||
case "card_grid_center_rows":
|
case "card_grid_center_rows":
|
||||||
|
case "card_grid_flex":
|
||||||
case "customCardStyles":
|
case "customCardStyles":
|
||||||
// Coalesce rapid card-style edits (e.g. holding the arrow keys on a
|
// Coalesce rapid card-style edits (e.g. holding the arrow keys on a
|
||||||
// number input) into a single applyAestheticChanges() call. Each
|
// number input) into a single applyAestheticChanges() call. Each
|
||||||
@ -1853,6 +1875,22 @@ function recieveMessage(request, sender, sendResponse) {
|
|||||||
return true; // keep the message channel open for async sendResponse
|
return true; // keep the message channel open for async sendResponse
|
||||||
case ("setcolors"): changeColorPreset(request.options); sendResponse(true); break;
|
case ("setcolors"): changeColorPreset(request.options); sendResponse(true); break;
|
||||||
case ("getcolors"): getCardColors().then(colors => sendResponse(colors)); return true; // keep the message channel open for async sendResponse
|
case ("getcolors"): getCardColors().then(colors => sendResponse(colors)); return true; // keep the message channel open for async sendResponse
|
||||||
|
case ("getGridCardCount"): {
|
||||||
|
// Popup card-grid warning asks the real page instead of guessing
|
||||||
|
// from stored card data: only cards actually rendered on this
|
||||||
|
// dashboard that aren't display:none (hidden via the card menu)
|
||||||
|
// occupy grid cells. Null means "no dashboard here" so the popup
|
||||||
|
// falls back to its storage-derived count.
|
||||||
|
try {
|
||||||
|
const dashCards = Array.from(document.querySelectorAll(".ic-DashboardCard"));
|
||||||
|
if (!dashCards.length) { sendResponse(null); break; }
|
||||||
|
const visible = dashCards.filter(c => getComputedStyle(c).display !== "none");
|
||||||
|
sendResponse({ count: visible.length });
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse(null);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case ("inspect"): sendResponse(inspectDarkMode(true)); break;
|
case ("inspect"): sendResponse(inspectDarkMode(true)); break;
|
||||||
case ("fixdm"): sendResponse(runDarkModeFixer(true)); break;
|
case ("fixdm"): sendResponse(runDarkModeFixer(true)); break;
|
||||||
case ("updateBackground"): applyCustomBackground(); sendResponse(true); break;
|
case ("updateBackground"): applyCustomBackground(); sendResponse(true); break;
|
||||||
@ -3928,14 +3966,15 @@ async function showTodoPreview(anchor, item) {
|
|||||||
const el = getTodoPreviewEl();
|
const el = getTodoPreviewEl();
|
||||||
const title = el.querySelector(".canvasrefined-preview-title");
|
const title = el.querySelector(".canvasrefined-preview-title");
|
||||||
const text = el.querySelector(".canvasrefined-preview-text");
|
const text = el.querySelector(".canvasrefined-preview-text");
|
||||||
title.textContent = item.plannable && item.plannable.title ? item.plannable.title : "";
|
title.textContent = anonTitleForItem(item);
|
||||||
text.textContent = "Loading…";
|
text.textContent = "Loading…";
|
||||||
el.style.display = "block";
|
el.style.display = "block";
|
||||||
positionTodoPreview(el, anchor);
|
positionTodoPreview(el, anchor);
|
||||||
const content = await getTodoPreviewText(item);
|
const content = await getTodoPreviewText(item);
|
||||||
if (token !== todoPreviewToken) return; // a newer hover (or hide) superseded this one
|
if (token !== todoPreviewToken) return; // a newer hover (or hide) superseded this one
|
||||||
if (el.style.display !== "block") return; // user already moved away
|
if (el.style.display !== "block") return; // user already moved away
|
||||||
text.textContent = content;
|
// "Hide personal details": don't reveal real assignment descriptions.
|
||||||
|
text.textContent = options.hide_personal_details === true ? "Preview hidden while personal details are hidden." : content;
|
||||||
positionTodoPreview(el, anchor); // reposition now that the height is known
|
positionTodoPreview(el, anchor); // reposition now that the height is known
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3995,8 +4034,10 @@ function todoScoreBadgeHtml(item) {
|
|||||||
// hasn't run yet (a cached null score means the fetch confirmed no grade).
|
// hasn't run yet (a cached null score means the fetch confirmed no grade).
|
||||||
const graded = cached ? cached.score != null : item.submissions?.graded === true;
|
const graded = cached ? cached.score != null : item.submissions?.graded === true;
|
||||||
let text;
|
let text;
|
||||||
|
// "Hide personal details": never reveal the real earned score.
|
||||||
|
const shownScore = options.hide_personal_details === true ? null : cached?.score;
|
||||||
if (cached?.excused) text = "Excused";
|
if (cached?.excused) text = "Excused";
|
||||||
else text = `${cached?.score != null ? formatTodoScoreNumber(cached.score) : "–"}/${pts} pts`;
|
else text = `${shownScore != null ? formatTodoScoreNumber(shownScore) : "–"}/${pts} pts`;
|
||||||
const attrs = `data-todo-course="${item.course_id ?? ""}" data-todo-plannable="${item.plannable_id}" data-todo-points="${pts}"`;
|
const attrs = `data-todo-course="${item.course_id ?? ""}" data-todo-plannable="${item.plannable_id}" data-todo-points="${pts}"`;
|
||||||
return `<span class="better-todo-score" ${attrs} style="margin-left:6px;opacity:.85;">${text}</span>${graded ? TODO_GRADED_BADGE_HTML : ""}`;
|
return `<span class="better-todo-score" ${attrs} style="margin-left:6px;opacity:.85;">${text}</span>${graded ? TODO_GRADED_BADGE_HTML : ""}`;
|
||||||
}
|
}
|
||||||
@ -4056,8 +4097,10 @@ async function loadTodoScores(items) {
|
|||||||
document.querySelectorAll("#better-todo-main .better-todo-score").forEach(el => {
|
document.querySelectorAll("#better-todo-main .better-todo-score").forEach(el => {
|
||||||
const cached = todoScoreCache.get(`${el.dataset.todoCourse}:${el.dataset.todoPlannable}`);
|
const cached = todoScoreCache.get(`${el.dataset.todoCourse}:${el.dataset.todoPlannable}`);
|
||||||
if (!cached) return;
|
if (!cached) return;
|
||||||
|
// "Hide personal details": never reveal the real earned score.
|
||||||
|
const shownScore = options.hide_personal_details === true ? null : cached.score;
|
||||||
if (cached.excused) el.textContent = "Excused";
|
if (cached.excused) el.textContent = "Excused";
|
||||||
else el.textContent = `${cached.score != null ? formatTodoScoreNumber(cached.score) : "–"}/${el.dataset.todoPoints} pts`;
|
else el.textContent = `${shownScore != null ? formatTodoScoreNumber(shownScore) : "–"}/${el.dataset.todoPoints} pts`;
|
||||||
if (cached.score != null && !el.nextElementSibling?.classList.contains("better-todo-graded")) {
|
if (cached.score != null && !el.nextElementSibling?.classList.contains("better-todo-graded")) {
|
||||||
el.insertAdjacentHTML("afterend", TODO_GRADED_BADGE_HTML);
|
el.insertAdjacentHTML("afterend", TODO_GRADED_BADGE_HTML);
|
||||||
}
|
}
|
||||||
@ -4190,8 +4233,8 @@ function populateAssignments(iscompleted = false) {
|
|||||||
</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;position:relative;">
|
<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;">
|
<div style="display:flex;flex-direction:column;gap:3px;">
|
||||||
<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>
|
<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;">${anonCourseNameForItem(item)}</span>
|
||||||
<a href="${taskHref}" style="color:inherit;text-decoration:none;font-weight:bold;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-sizing:border-box;padding-right:28px;margin-top:-5px;">${item.plannable.title}</a>
|
<a href="${taskHref}" style="color:inherit;text-decoration:none;font-weight:bold;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-sizing:border-box;padding-right:28px;margin-top:-5px;">${anonTitleForItem(item)}</a>
|
||||||
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}${scoreBadge}</span>
|
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}${scoreBadge}</span>
|
||||||
</div>
|
</div>
|
||||||
${editButtonSvg}
|
${editButtonSvg}
|
||||||
@ -4303,8 +4346,8 @@ function populateAnnouncements() {
|
|||||||
</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;position:relative;">
|
<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;">
|
<div style="display:flex;flex-direction:column;gap:3px;">
|
||||||
<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>
|
<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;">${anonCourseNameForItem(item)}</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>
|
<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;">${anonTitleForItem(item)}</a>
|
||||||
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
|
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
|
||||||
</div>
|
</div>
|
||||||
<svg class="better-todo-announcement-checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:0px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'" title="Mark as seen">
|
<svg class="better-todo-announcement-checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:0px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'" title="Mark as seen">
|
||||||
@ -5113,9 +5156,9 @@ async function loadBetterTodo() {
|
|||||||
if (itemState?.["crs"] === true) {
|
if (itemState?.["crs"] === true) {
|
||||||
listItemContainer.querySelector(".canvasrefined-todo-item").style.textDecoration = "line-through";
|
listItemContainer.querySelector(".canvasrefined-todo-item").style.textDecoration = "line-through";
|
||||||
}
|
}
|
||||||
let title = makeElement("a", listItem.querySelector(".canvasrefined-todo-item-header"), { "className": "canvasrefined-todoitem-title", "textContent": item.plannable.title });
|
let title = makeElement("a", listItem.querySelector(".canvasrefined-todo-item-header"), { "className": "canvasrefined-todoitem-title", "textContent": anonTitleForItem(item) });
|
||||||
if (options.todo_hide_feedback === true) title.style = "color:" + courseColor + "!important;";
|
if (options.todo_hide_feedback === true) title.style = "color:" + courseColor + "!important;";
|
||||||
let course = makeElement("p", listItem, { "className": "canvasrefined-todoitem-course", "textContent": item.context_name });
|
let course = makeElement("p", listItem, { "className": "canvasrefined-todoitem-course", "textContent": anonCourseNameForItem(item) });
|
||||||
course.style.color = courseColor;
|
course.style.color = courseColor;
|
||||||
let format = formatTodoDate(date, item.submissions, hr24);
|
let format = formatTodoDate(date, item.submissions, hr24);
|
||||||
let todoDate = makeElement("p", listItem, { "className": "canvasrefined-todoitem-date", "textContent": format.date });
|
let todoDate = makeElement("p", listItem, { "className": "canvasrefined-todoitem-date", "textContent": format.date });
|
||||||
@ -5360,6 +5403,11 @@ async function changeColorPreset(colors) {
|
|||||||
card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor = colors[cnum];
|
card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor = colors[cnum];
|
||||||
card.el.querySelector(".ic-DashboardCard__header-title span").style.color = colors[cnum];
|
card.el.querySelector(".ic-DashboardCard__header-title span").style.color = colors[cnum];
|
||||||
card.el.querySelector(".ic-DashboardCard__header-button-bg").style.backgroundColor = colors[cnum];
|
card.el.querySelector(".ic-DashboardCard__header-button-bg").style.backgroundColor = colors[cnum];
|
||||||
|
// Recompute this card's gradient from its NEW color right
|
||||||
|
// away — changeGradientCards() outside the queue ran too
|
||||||
|
// early (colors land later, on the 250ms interval), which
|
||||||
|
// left gradients built from the previous course colors.
|
||||||
|
changeGradientCards();
|
||||||
} else {
|
} else {
|
||||||
const coursePrefix = "/courses/" + course_id;
|
const coursePrefix = "/courses/" + course_id;
|
||||||
document.querySelectorAll(".planner-item").forEach(item => {
|
document.querySelectorAll(".planner-item").forEach(item => {
|
||||||
@ -5666,6 +5714,305 @@ function runiframeChecker() {
|
|||||||
iframeObserver.observe(document.documentElement, { childList: true, subtree: true });
|
iframeObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Hide personal details (screenshot privacy mode)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Pregenerated pool of fake courses (15 entries). Every real course is mapped
|
||||||
|
// to a unique entry (stable per course via a hash, collisions probed away),
|
||||||
|
// so the same course always shows the same fake name everywhere — dashboard
|
||||||
|
// cards, the Better Todo List and recent feedback.
|
||||||
|
const FAKE_COURSES = [
|
||||||
|
{ name: "Algebra II", teacher: "Martinez" },
|
||||||
|
{ name: "AP Biology", teacher: "Nguyen" },
|
||||||
|
{ name: "Art History", teacher: "Kowalski" },
|
||||||
|
{ name: "Astronomy", teacher: "Delgado" },
|
||||||
|
{ name: "Chemistry", teacher: "Patel" },
|
||||||
|
{ name: "Computer Science", teacher: "Brooks" },
|
||||||
|
{ name: "Creative Writing", teacher: "Ellis" },
|
||||||
|
{ name: "European History", teacher: "Fischer" },
|
||||||
|
{ name: "Geography", teacher: "Okafor" },
|
||||||
|
{ name: "Marine Biology", teacher: "Silva" },
|
||||||
|
{ name: "Physics", teacher: "Jensen" },
|
||||||
|
{ name: "Psychology", teacher: "Moreau" },
|
||||||
|
{ name: "Spanish III", teacher: "Rivera" },
|
||||||
|
{ name: "Statistics", teacher: "Chen" },
|
||||||
|
{ name: "World History", teacher: "Adler" },
|
||||||
|
];
|
||||||
|
const FAKE_SCHOOL_NAME = "Ridgeview High School";
|
||||||
|
const FAKE_ASSIGNMENT_TITLES = [
|
||||||
|
"Homework 4", "Reading Response", "Lab Report 2", "Practice Problems",
|
||||||
|
"Unit Quiz", "Essay Draft", "Worksheet 7", "Group Project",
|
||||||
|
"Chapter Review", "Study Guide", "Discussion Post", "Exit Ticket",
|
||||||
|
"Vocabulary Quiz", "Lab Worksheet", "Project Proposal", "Test Review",
|
||||||
|
"Notes Check", "Article Summary", "Problem Set 3", "Class Survey",
|
||||||
|
];
|
||||||
|
const FAKE_FEEDBACK_COMMENTS = [
|
||||||
|
"Nice work!", "Good effort.", "Well done!", "Keep it up!", "Solid job!",
|
||||||
|
"Great improvement!", "Strong submission.", "Excellent detail.",
|
||||||
|
];
|
||||||
|
|
||||||
|
// FNV-1a — small, dependency-free, deterministic string hash.
|
||||||
|
function anonHash(str) {
|
||||||
|
let h = 2166136261;
|
||||||
|
const s = String(str);
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
h ^= s.charCodeAt(i);
|
||||||
|
h = Math.imul(h, 16777619);
|
||||||
|
}
|
||||||
|
return h >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// key -> index into FAKE_COURSES. Hash-based so a course keeps its fake name
|
||||||
|
// across reloads, with linear probing so every real course gets a UNIQUE fake
|
||||||
|
// course. Once the pool is exhausted (more than 15 courses), reuse is allowed.
|
||||||
|
const anonCourseMap = new Map();
|
||||||
|
function getAnonCourse(key) {
|
||||||
|
const k = String(key ?? "unknown");
|
||||||
|
if (!anonCourseMap.has(k)) {
|
||||||
|
const used = new Set(anonCourseMap.values());
|
||||||
|
let idx = anonHash(k) % FAKE_COURSES.length;
|
||||||
|
if (used.size < FAKE_COURSES.length) {
|
||||||
|
while (used.has(idx)) idx = (idx + 1) % FAKE_COURSES.length;
|
||||||
|
}
|
||||||
|
anonCourseMap.set(k, idx);
|
||||||
|
}
|
||||||
|
return FAKE_COURSES[anonCourseMap.get(k)];
|
||||||
|
}
|
||||||
|
function fakeCourseLabel(key) {
|
||||||
|
const c = getAnonCourse(key);
|
||||||
|
return `${c.name} - ${c.teacher}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "a:<assignment id>" when the id is known, "t:<title text>" otherwise.
|
||||||
|
function fakeAssignmentTitle(key) {
|
||||||
|
return FAKE_ASSIGNMENT_TITLES[anonHash(String(key)) % FAKE_ASSIGNMENT_TITLES.length];
|
||||||
|
}
|
||||||
|
function fakeFeedbackComment(key) {
|
||||||
|
return FAKE_FEEDBACK_COMMENTS[anonHash(String(key)) % FAKE_FEEDBACK_COMMENTS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic fake "x out of 20" score for recent feedback entries.
|
||||||
|
function fakeFeedbackScore(key) {
|
||||||
|
const earned = 13 + (anonHash(String(key)) % 8);
|
||||||
|
return `${earned} out of 20`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic fake dashboard grade: a plausible percent plus the letter the
|
||||||
|
// user's own GPA cutoffs would assign it (same scale their real grades show).
|
||||||
|
function fakeGradeText(key) {
|
||||||
|
const h = anonHash(String(key));
|
||||||
|
const percent = Math.round((70 + (h % 300) / 10) * 10) / 10;
|
||||||
|
const letter = percentToLetterGrade(percent);
|
||||||
|
return (letter ? `${letter} ` : "") + `${percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Term strings look like "2026/2027 - Campolindo High School - Year": keep the
|
||||||
|
// year and the term type, swap the school name for a fake one.
|
||||||
|
function anonFakeTerm(orig) {
|
||||||
|
const parts = String(orig).split(" - ").map(p => p.trim()).filter(p => p !== "");
|
||||||
|
if (parts.length >= 3) return `${parts[0]} - ${FAKE_SCHOOL_NAME} - ${parts[parts.length - 1]}`;
|
||||||
|
return FAKE_SCHOOL_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render-time helpers so text baked into todo/assignment templates is fake
|
||||||
|
// from the start (a DOM pass alone can't beat async re-renders clobbering it).
|
||||||
|
function anonCourseNameForItem(item) {
|
||||||
|
const real = item?.context_name ?? "";
|
||||||
|
if (options.hide_personal_details !== true) return real;
|
||||||
|
return fakeCourseLabel(item?.course_id ?? item?.context_id ?? "name:" + String(real).trim().toLowerCase());
|
||||||
|
}
|
||||||
|
function anonTitleForItem(item) {
|
||||||
|
const real = item?.plannable?.title ?? "";
|
||||||
|
if (options.hide_personal_details !== true) return real;
|
||||||
|
const id = item?.plannable_id ?? item?.plannable?.id;
|
||||||
|
return fakeAssignmentTitle(id != null ? "a:" + id : "t:" + String(real).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marked-text helpers. Original text/attributes are stashed in dataset entries
|
||||||
|
// so toggling the option off restores the real content in place.
|
||||||
|
function anonText(el, computeFake) {
|
||||||
|
if (!el || el.dataset.crAnonOrig !== undefined) return;
|
||||||
|
const orig = el.textContent;
|
||||||
|
el.dataset.crAnonOrig = orig;
|
||||||
|
el.textContent = computeFake(orig);
|
||||||
|
}
|
||||||
|
function anonAttr(el, attr, computeFake) {
|
||||||
|
if (!el) return;
|
||||||
|
let attrs = {};
|
||||||
|
if (el.dataset.crAnonAttrs !== undefined) {
|
||||||
|
attrs = JSON.parse(el.dataset.crAnonAttrs);
|
||||||
|
if (attrs[attr] !== undefined) return;
|
||||||
|
} else {
|
||||||
|
el.dataset.crAnonAttrs = "{}";
|
||||||
|
}
|
||||||
|
const orig = el.getAttribute(attr);
|
||||||
|
if (orig == null) return;
|
||||||
|
attrs[attr] = orig;
|
||||||
|
el.dataset.crAnonAttrs = JSON.stringify(attrs);
|
||||||
|
el.setAttribute(attr, computeFake(orig));
|
||||||
|
}
|
||||||
|
function restoreAnonymized(root = document) {
|
||||||
|
root.querySelectorAll("[data-cr-anon-orig]").forEach(el => {
|
||||||
|
el.textContent = el.dataset.crAnonOrig;
|
||||||
|
delete el.dataset.crAnonOrig;
|
||||||
|
});
|
||||||
|
root.querySelectorAll("[data-cr-anon-attrs]").forEach(el => {
|
||||||
|
try {
|
||||||
|
const attrs = JSON.parse(el.dataset.crAnonAttrs);
|
||||||
|
for (const [attr, value] of Object.entries(attrs)) el.setAttribute(attr, value);
|
||||||
|
} catch (e) { /* malformed marker: leave as-is */ }
|
||||||
|
delete el.dataset.crAnonAttrs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anonymize one dashboard card's visible + screenreader text.
|
||||||
|
function anonymizeDashboardCard(card) {
|
||||||
|
const link = card.querySelector('.ic-DashboardCard__link[href*="/courses/"]');
|
||||||
|
const idMatch = link ? (link.getAttribute("href") || "").match(/courses\/(\d+)/) : null;
|
||||||
|
const key = idMatch
|
||||||
|
? "id:" + idMatch[1]
|
||||||
|
: "name:" + (card.querySelector(".ic-DashboardCard__header-title")?.textContent || "").trim().toLowerCase();
|
||||||
|
const fakeLabel = fakeCourseLabel(key);
|
||||||
|
|
||||||
|
// Snapshot the real names BEFORE anything is anonymized so substring
|
||||||
|
// replacement in aria-labels/screenreader spans works off originals.
|
||||||
|
// Both forms matter: the h2 text is the course nickname ("Calculus BC -
|
||||||
|
// Schoen") while the title attr/subtitle use the full course name.
|
||||||
|
const titleEl = card.querySelector(".ic-DashboardCard__header-title");
|
||||||
|
const origNames = [];
|
||||||
|
if (titleEl) {
|
||||||
|
origNames.push((titleEl.getAttribute("title") || "").trim());
|
||||||
|
origNames.push(titleEl.textContent.trim());
|
||||||
|
}
|
||||||
|
const subtitleEl = card.querySelector(".ic-DashboardCard__header-subtitle");
|
||||||
|
if (subtitleEl) {
|
||||||
|
origNames.push(subtitleEl.textContent.trim());
|
||||||
|
origNames.push((subtitleEl.getAttribute("title") || "").trim());
|
||||||
|
}
|
||||||
|
const uniqueOrigNames = [...new Set(origNames.filter(n => n && n.length > 1))];
|
||||||
|
const replaceNames = (s) => {
|
||||||
|
let out = String(s);
|
||||||
|
uniqueOrigNames.forEach(n => { out = out.split(n).join(fakeLabel); });
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (titleEl) {
|
||||||
|
const inner = titleEl.querySelector("span") || titleEl;
|
||||||
|
anonText(inner, () => fakeLabel);
|
||||||
|
anonAttr(titleEl, "title", () => fakeLabel);
|
||||||
|
}
|
||||||
|
if (subtitleEl) {
|
||||||
|
anonText(subtitleEl, () => fakeLabel);
|
||||||
|
anonAttr(subtitleEl, "title", () => fakeLabel);
|
||||||
|
}
|
||||||
|
const termEl = card.querySelector(".ic-DashboardCard__header-term");
|
||||||
|
if (termEl) {
|
||||||
|
anonText(termEl, anonFakeTerm);
|
||||||
|
anonAttr(termEl, "title", anonFakeTerm);
|
||||||
|
}
|
||||||
|
// querySelectorAll only matches descendants, so the card's own
|
||||||
|
// aria-label needs handling separately.
|
||||||
|
anonAttr(card, "aria-label", replaceNames);
|
||||||
|
card.querySelectorAll("[aria-label]").forEach(el => anonAttr(el, "aria-label", replaceNames));
|
||||||
|
card.querySelectorAll(".screenreader-only").forEach(el => anonText(el, replaceNames));
|
||||||
|
// The fake grade is normally written directly by insertGrades(); this
|
||||||
|
// covers cards rendered before that ran or with grades unavailable.
|
||||||
|
const gradeEl = card.querySelector(".canvasrefined-card-grade");
|
||||||
|
if (gradeEl && gradeEl.textContent.trim() !== "") anonText(gradeEl, () => fakeGradeText(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anonymize one recent-feedback entry (the classic right-sidebar widget, which
|
||||||
|
// the Better Todo List re-appends as-is).
|
||||||
|
function anonymizeFeedbackLink(a) {
|
||||||
|
const details = a.querySelector(".event-details");
|
||||||
|
if (!details) return;
|
||||||
|
const href = a.getAttribute("href") || "";
|
||||||
|
const cMatch = href.match(/courses\/(\d+)/);
|
||||||
|
const aMatch = href.match(/assignments\/(\d+)/);
|
||||||
|
const contextEl = details.querySelector(".event-details__context");
|
||||||
|
const key = cMatch
|
||||||
|
? "id:" + cMatch[1]
|
||||||
|
: "name:" + (contextEl ? contextEl.textContent : "").trim().toLowerCase();
|
||||||
|
const fakeLabel = fakeCourseLabel(key);
|
||||||
|
|
||||||
|
if (contextEl) anonText(contextEl, () => fakeLabel);
|
||||||
|
const titleEl = details.querySelector(".recent_feedback_title");
|
||||||
|
if (titleEl) anonText(titleEl, () => fakeAssignmentTitle(aMatch ? "a:" + aMatch[1] : "t:" + titleEl.textContent.trim()));
|
||||||
|
const scoreEl = details.querySelector("p strong");
|
||||||
|
if (scoreEl && /out of|\/.|%/.test(scoreEl.textContent)) {
|
||||||
|
anonText(scoreEl, () => fakeFeedbackScore(aMatch ? "a:" + aMatch[1] : "t:" + (titleEl ? titleEl.textContent : "")));
|
||||||
|
}
|
||||||
|
// The comment is the last <p> and is quoted feedback text.
|
||||||
|
const ps = details.querySelectorAll("p");
|
||||||
|
const commentP = ps.length ? ps[ps.length - 1] : null;
|
||||||
|
if (commentP && commentP !== scoreEl?.parentElement && !commentP.querySelector("strong")) {
|
||||||
|
anonText(commentP, (orig) => {
|
||||||
|
const t = orig.trim();
|
||||||
|
return t.startsWith('"') ? `"${fakeFeedbackComment(aMatch ? "a:" + aMatch[1] : "t:" + t)}"` : fakeFeedbackComment(aMatch ? "a:" + aMatch[1] : "t:" + t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One anonymizer sweep over the current DOM.
|
||||||
|
function anonymizePersonalDetails(root = document) {
|
||||||
|
if (options.hide_personal_details !== true) return;
|
||||||
|
try {
|
||||||
|
root.querySelectorAll(".ic-DashboardCard").forEach(anonymizeDashboardCard);
|
||||||
|
root.querySelectorAll(".recent_feedback a[href]").forEach(anonymizeFeedbackLink);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course image removal (small checkbox under the toggle): hides the course
|
||||||
|
// photos on dashboard cards via a style element, so no upload/refresh is
|
||||||
|
// needed and unchecking restores them instantly.
|
||||||
|
function applyCourseImageHiding() {
|
||||||
|
const on = options.hide_course_images === true;
|
||||||
|
let style = document.getElementById("canvasrefined-hide-course-images");
|
||||||
|
if (on && !style) {
|
||||||
|
style = document.createElement("style");
|
||||||
|
style.id = "canvasrefined-hide-course-images";
|
||||||
|
style.textContent = `
|
||||||
|
.ic-DashboardCard__header_image { background-image: none !important; }
|
||||||
|
.ic-DashboardCard__header_image img { display: none !important; }
|
||||||
|
.canvasrefined-link-image { display: none !important; }
|
||||||
|
`;
|
||||||
|
(document.head || document.documentElement).append(style);
|
||||||
|
} else if (!on && style) {
|
||||||
|
style.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let personalDetailsObserver = null;
|
||||||
|
let anonPassScheduled = false;
|
||||||
|
|
||||||
|
// Master switch: starts/stops the DOM watcher and (de)anonymizes in place.
|
||||||
|
function setupPersonalDetailsHiding() {
|
||||||
|
applyCourseImageHiding();
|
||||||
|
if (options.hide_personal_details === true) {
|
||||||
|
if (!personalDetailsObserver) {
|
||||||
|
personalDetailsObserver = new MutationObserver(() => {
|
||||||
|
if (anonPassScheduled) return;
|
||||||
|
anonPassScheduled = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
anonPassScheduled = false;
|
||||||
|
anonymizePersonalDetails();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
personalDetailsObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
anonymizePersonalDetails();
|
||||||
|
} else {
|
||||||
|
if (personalDetailsObserver) {
|
||||||
|
personalDetailsObserver.disconnect();
|
||||||
|
personalDetailsObserver = null;
|
||||||
|
}
|
||||||
|
restoreAnonymized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Dashboard grades
|
Dashboard grades
|
||||||
*/
|
*/
|
||||||
@ -5710,7 +6057,8 @@ function insertGrades() {
|
|||||||
if (letter) percent = `${letter} ${percent}`;
|
if (letter) percent = `${letter} ${percent}`;
|
||||||
}
|
}
|
||||||
let gradeContainer = cards[i].querySelector(".canvasrefined-card-grade") || makeElement("a", cards[i].querySelector(".ic-DashboardCard__header"), { "className": "canvasrefined-card-grade" });
|
let gradeContainer = cards[i].querySelector(".canvasrefined-card-grade") || makeElement("a", cards[i].querySelector(".ic-DashboardCard__header"), { "className": "canvasrefined-card-grade" });
|
||||||
gradeContainer.textContent = percent;
|
// "Hide personal details": show a deterministic fake grade instead.
|
||||||
|
gradeContainer.textContent = options.hide_personal_details === true ? fakeGradeText("id:" + course_id) : percent;
|
||||||
if (options.grade_hover === true) {
|
if (options.grade_hover === true) {
|
||||||
gradeContainer.classList.add("canvasrefined-hover-only");
|
gradeContainer.classList.add("canvasrefined-hover-only");
|
||||||
} else {
|
} else {
|
||||||
@ -5741,7 +6089,7 @@ Card assignments
|
|||||||
function createCardAssignment(assignment) {
|
function createCardAssignment(assignment) {
|
||||||
let assignmentContainer = document.createElement("div");
|
let assignmentContainer = document.createElement("div");
|
||||||
assignmentContainer.className = "canvasrefined-assignment-container";
|
assignmentContainer.className = "canvasrefined-assignment-container";
|
||||||
let assignmentName = makeElement("a", assignmentContainer, { "className": "canvasrefined-assignment-link", "textContent": assignment.plannable.title, "href": assignment.html_url });
|
let assignmentName = makeElement("a", assignmentContainer, { "className": "canvasrefined-assignment-link", "textContent": anonTitleForItem(assignment), "href": assignment.html_url });
|
||||||
let assignmentDueAt = makeElement("span", assignmentContainer, { "className": "canvasrefined-assignment-dueat", "textContent": formatCardDue(new Date(assignment.plannable_date)) });
|
let assignmentDueAt = makeElement("span", assignmentContainer, { "className": "canvasrefined-assignment-dueat", "textContent": formatCardDue(new Date(assignment.plannable_date)) });
|
||||||
if (assignment.overdue === true) assignmentDueAt.classList.add("canvasrefined-assignment-overdue");
|
if (assignment.overdue === true) assignmentDueAt.classList.add("canvasrefined-assignment-overdue");
|
||||||
if (assignment?.submissions?.submitted === true) {
|
if (assignment?.submissions?.submitted === true) {
|
||||||
@ -6748,14 +7096,80 @@ function applyAestheticChanges() {
|
|||||||
const gridRowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
|
const gridRowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
|
||||||
const centerGrid = options.center_cards === true;
|
const centerGrid = options.center_cards === true;
|
||||||
const gpaOn = options.gpa_calc === true;
|
const gpaOn = options.gpa_calc === true;
|
||||||
|
const gpaTop = options.gpa_calc_prepend === true;
|
||||||
|
const flexGrid = options.card_grid_flex === true;
|
||||||
|
if (flexGrid) {
|
||||||
|
// Flexible grid: fixed-width tracks (cards keep their natural
|
||||||
|
// width — no stretching). While the container is wide it lays out
|
||||||
|
// exactly the user's column count; as it narrows, @container
|
||||||
|
// queries — measured against the card container's REAL width, so
|
||||||
|
// there's no window/chrome-width guessing — step the count down
|
||||||
|
// precisely when the next column would no longer fit. Tracks are
|
||||||
|
// always a full card wide, so cards can't overlap; extra cards
|
||||||
|
// wrap into auto-height rows instead of being clipped.
|
||||||
|
const cardW = options.customCardStyles === true && parseInt(options.cardWidth, 10) > 0
|
||||||
|
? parseInt(options.cardWidth, 10)
|
||||||
|
: 262;
|
||||||
|
// Container queries need an ancestor size container; the card
|
||||||
|
// section wrapper is a plain full-width block, so inline-size
|
||||||
|
// containment is safe there.
|
||||||
|
style.textContent += `#DashboardCard_Container{container-type:inline-size}`;
|
||||||
|
// Full fit (m = gridCols columns): applies whenever no narrower
|
||||||
|
// breakpoint matches, i.e. only when all columns truly fit.
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols},${cardW}px)!important;grid-auto-rows:auto!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||||
|
// Grid gaps alone control spacing (beats the customCardStyles
|
||||||
|
// margin rules emitted later via higher specificity).
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-right:0!important;margin-bottom:0!important}`;
|
||||||
|
if (gpaOn) {
|
||||||
|
if (gpaTop) {
|
||||||
|
// Top mode: definite first-row placement, one track wide;
|
||||||
|
// the expanded calculator takes the full second row.
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1!important}`;
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||||
|
} else {
|
||||||
|
// Bottom mode: no fixed spare rows (column count varies
|
||||||
|
// with width). `order` places the GPA card after every
|
||||||
|
// course card in the auto-placement sequence — it flows
|
||||||
|
// right after the last card — and the expanded calculator
|
||||||
|
// follows on a full-width row.
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{order:1!important}`;
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-column:1/-1!important;order:2!important}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Step the column count down: m columns apply once m+1 no longer
|
||||||
|
// fit (full fit for m columns = m*cardW + (m-1)*colGap). Emitted
|
||||||
|
// widest-first so the narrowest matching rule wins the cascade.
|
||||||
|
// The last step (1 column) also lets the lone card shrink with
|
||||||
|
// the window instead of overflowing it.
|
||||||
|
for (let m = gridCols - 1; m >= 1; m--) {
|
||||||
|
const tooWideFor = (m + 1) * cardW + m * gridColGap;
|
||||||
|
if (m === 1) {
|
||||||
|
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:minmax(0,1fr)!important}.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{width:100%!important;min-width:0!important}}`;
|
||||||
|
} else {
|
||||||
|
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:repeat(${m},${cardW}px)!important}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const totalRows = gridRows + (gpaOn ? 2 : 0);
|
const totalRows = gridRows + (gpaOn ? 2 : 0);
|
||||||
style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard{grid-column:span 2!important}`;
|
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard{grid-column:span 2!important}`;
|
||||||
if (gpaOn) {
|
if (gpaOn) {
|
||||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:${gridRows + 1}!important;grid-column:span 2!important}`;
|
// "Move to top" (gpa_calc_prepend) decides where the GPA card sits
|
||||||
|
// in the grid: on -> first row, leftmost slot (the calculator row
|
||||||
|
// goes right below it, and cards auto-place around both); off ->
|
||||||
|
// spare rows past the card rows, anchored to the rightmost slot.
|
||||||
|
// In both cases the card spans one card width (2 sub-columns) and
|
||||||
|
// the expanded calculator spans the full grid width.
|
||||||
|
if (gpaTop) {
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1/span 2!important}`;
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||||
|
} else {
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:${gridRows + 1}!important;grid-column:-3/span 2!important}`;
|
||||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:${gridRows + 2}!important;grid-column:1/-1!important}`;
|
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:${gridRows + 2}!important;grid-column:1/-1!important}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (options.customCardStyles === true) {
|
if (options.customCardStyles === true) {
|
||||||
if (options.imageSize !== undefined && options.imageSize !== 100) style.textContent += `.ic-DashboardCard__header_image {transform: scale(${options.imageSize / 100})!important; }`;
|
if (options.imageSize !== undefined && options.imageSize !== 100) style.textContent += `.ic-DashboardCard__header_image {transform: scale(${options.imageSize / 100})!important; }`;
|
||||||
if (options.cardRoundness !== undefined && options.cardRoundness !== 5) style.textContent += `.ic-DashboardCard {border-radius: ${options.cardRoundness}px!important;}`;
|
if (options.cardRoundness !== undefined && options.cardRoundness !== 5) style.textContent += `.ic-DashboardCard {border-radius: ${options.cardRoundness}px!important;}`;
|
||||||
@ -6835,6 +7249,9 @@ function centerUnevenGridRows() {
|
|||||||
const cards = Array.from(container.children).filter(el => el.classList && el.classList.contains("ic-DashboardCard"));
|
const cards = Array.from(container.children).filter(el => el.classList && el.classList.contains("ic-DashboardCard"));
|
||||||
cards.forEach(card => card.style.removeProperty("grid-column"));
|
cards.forEach(card => card.style.removeProperty("grid-column"));
|
||||||
if (options.card_grid !== true || options.card_grid_center_rows !== true) return;
|
if (options.card_grid !== true || options.card_grid_center_rows !== true) return;
|
||||||
|
// Flexible grid: the live column count changes with the window width, so
|
||||||
|
// fixed sub-column offsets computed here would be wrong — skip centering.
|
||||||
|
if (options.card_grid_flex === true) return;
|
||||||
const cols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
|
const cols = Math.max(1, parseInt(options.card_grid_columns, 10) || 4);
|
||||||
const rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
const rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
||||||
const visible = cards.filter(card => getComputedStyle(card).display !== "none");
|
const visible = cards.filter(card => getComputedStyle(card).display !== "none");
|
||||||
@ -7615,38 +8032,34 @@ function escapeGlobalSearchAttr(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeGradientCards() {
|
function changeGradientCards() {
|
||||||
if (options.gradient_cards === true) {
|
// Apply the gradient INLINE on each hero instead of via index-based CSS
|
||||||
let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero');
|
// rules: the gradient is always computed from that hero's actual current
|
||||||
|
// course color, so it stays in sync even when cards are reordered,
|
||||||
// Create the style once; re-appending triggers the MutationObserver and re-runs this function.
|
// re-rendered, or recolored after this pass (the old #gradientcss
|
||||||
let cardcss = document.querySelector("#gradientcss");
|
// "card N" selectors silently drifted onto the wrong cards).
|
||||||
if (!cardcss) {
|
// Only background-image is set inline — background-color (the course
|
||||||
cardcss = document.createElement('style');
|
// color) is left untouched so it can still be read/recolord later.
|
||||||
cardcss.id = "gradientcss";
|
const heroes = document.querySelectorAll('.ic-DashboardCard__header_hero');
|
||||||
document.documentElement.appendChild(cardcss);
|
heroes.forEach(hero => {
|
||||||
|
if (options.gradient_cards !== true) {
|
||||||
|
hero.style.removeProperty("background-image");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const rgb = hero.style.backgroundColor;
|
||||||
// Build CSS into a string and only touch the DOM if it changed.
|
const parts = rgb.match(/\d+(\.\d+)?/g);
|
||||||
let css = "";
|
// No inline course color (e.g. hero not painted yet): leave untouched
|
||||||
for (let i = 0; i < cardheads.length; i++) {
|
// rather than guessing NaN colors.
|
||||||
let colorone = cardheads[i].style.backgroundColor.split(',');
|
if (!parts || parts.length < 3) return;
|
||||||
let [r, g, b] = [parseInt(colorone[0].split('(')[1]), parseInt(colorone[1]), parseInt(colorone[2])];
|
const [r, g, b] = [parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2])];
|
||||||
let [h, s, l] = [rgbToHsl(r, g, b)[0], rgbToHsl(r, g, b)[1], rgbToHsl(r, g, b)[2]];
|
let [h, s, l] = rgbToHsl(r, g, b);
|
||||||
let degree = ((h % 60) / 60) >= .66 ? 30 : ((h % 60) / 60) <= .33 ? -30 : 15;
|
let degree = ((h % 60) / 60) >= .66 ? 30 : ((h % 60) / 60) <= .33 ? -30 : 15;
|
||||||
let newh = h > 300 ? (360 - (h + 65)) + (65 + degree) : h + 65 + degree;
|
let newh = h > 300 ? (360 - (h + 65)) + (65 + degree) : h + 65 + degree;
|
||||||
css += ".ic-DashboardCard:nth-of-type(" + (i + 1) + ") .ic-DashboardCard__header_hero{background: linear-gradient(115deg, hsl(" + h + "deg," + s + "%," + l + "%) 5%, hsl(" + newh + "deg," + s + "%," + l + "%) 100%)!important}";
|
hero.style.backgroundImage = `linear-gradient(115deg, hsl(${h}deg,${s}%,${l}%) 5%, hsl(${newh}deg,${s}%,${l}%) 100%)`;
|
||||||
}
|
});
|
||||||
|
|
||||||
if (cardcss.textContent !== css) {
|
// Clean up the legacy index-based stylesheet from older versions.
|
||||||
cardcss.textContent = css;
|
const legacy = document.querySelector("#gradientcss");
|
||||||
}
|
if (legacy) legacy.remove();
|
||||||
|
|
||||||
} else {
|
|
||||||
let cardcss = document.querySelector("#gradientcss");
|
|
||||||
if (cardcss && cardcss.textContent !== "") {
|
|
||||||
cardcss.textContent = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showUpdateMsg() {
|
function showUpdateMsg() {
|
||||||
|
|||||||
68
js/popup.js
68
js/popup.js
@ -1,6 +1,7 @@
|
|||||||
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', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics'];
|
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', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics', 'hide_personal_details'];
|
||||||
const syncedSubOptions = [
|
const syncedSubOptions = [
|
||||||
"grade_analytics_zones",
|
"grade_analytics_zones",
|
||||||
|
"hide_course_images",
|
||||||
"todo_hide_feedback",
|
"todo_hide_feedback",
|
||||||
"todo_hide_read",
|
"todo_hide_read",
|
||||||
"todo_full_height",
|
"todo_full_height",
|
||||||
@ -50,6 +51,7 @@ const syncedSubOptions = [
|
|||||||
"card_grid_column_gap",
|
"card_grid_column_gap",
|
||||||
"card_grid_row_gap",
|
"card_grid_row_gap",
|
||||||
"card_grid_center_rows",
|
"card_grid_center_rows",
|
||||||
|
"card_grid_flex",
|
||||||
"customBackgroundLink",
|
"customBackgroundLink",
|
||||||
"customBackgroundScale",
|
"customBackgroundScale",
|
||||||
"customBackgroundDaily",
|
"customBackgroundDaily",
|
||||||
@ -68,7 +70,7 @@ const localSwitches = [];
|
|||||||
// Theme export only carries visual settings, never personal productivity data.
|
// Theme export only carries visual settings, never personal productivity data.
|
||||||
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
|
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
|
||||||
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
|
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
|
||||||
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding", "card_grid", "card_grid_columns", "card_grid_rows", "card_grid_column_gap", "card_grid_row_gap", "card_grid_center_rows"];
|
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding", "card_grid", "card_grid_columns", "card_grid_rows", "card_grid_column_gap", "card_grid_row_gap", "card_grid_center_rows", "card_grid_flex"];
|
||||||
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "tab_icons"];
|
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "tab_icons"];
|
||||||
const exportSidebar = ["better_sidebar", "sidebar_scale"];
|
const exportSidebar = ["better_sidebar", "sidebar_scale"];
|
||||||
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "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", "todo_show_scores", "hover_preview"];
|
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "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", "todo_show_scores", "hover_preview"];
|
||||||
@ -103,6 +105,8 @@ const defaultOptions = {
|
|||||||
},
|
},
|
||||||
"new_install": true,
|
"new_install": true,
|
||||||
"assignments_due": true,
|
"assignments_due": true,
|
||||||
|
"hide_personal_details": false,
|
||||||
|
"hide_course_images": false,
|
||||||
"gpa_calc": false,
|
"gpa_calc": false,
|
||||||
"dark_mode": true,
|
"dark_mode": true,
|
||||||
"gradent_cards": false,
|
"gradent_cards": false,
|
||||||
@ -204,6 +208,7 @@ const defaultOptions = {
|
|||||||
"card_grid_column_gap": 12,
|
"card_grid_column_gap": 12,
|
||||||
"card_grid_row_gap": 12,
|
"card_grid_row_gap": 12,
|
||||||
"card_grid_center_rows": false,
|
"card_grid_center_rows": false,
|
||||||
|
"card_grid_flex": false,
|
||||||
"customCardStyles": false,
|
"customCardStyles": false,
|
||||||
"customBackgroundLink": "",
|
"customBackgroundLink": "",
|
||||||
"customBackgroundScale": 100,
|
"customBackgroundScale": 100,
|
||||||
@ -225,9 +230,23 @@ chrome.storage.onChanged.addListener((changes) => {
|
|||||||
// Hidden-card edits from the card menu change how many cells the
|
// Hidden-card edits from the card menu change how many cells the
|
||||||
// grid preview should mark as filled.
|
// grid preview should mark as filled.
|
||||||
updateCardGridPreview();
|
updateCardGridPreview();
|
||||||
|
scheduleGridPreviewFollowUp();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The live card count is read from the dashboard tab's DOM, which applies
|
||||||
|
// display:none for a newly hidden card in its own storage listener — a race
|
||||||
|
// against the popup's. One debounced follow-up render re-syncs the preview
|
||||||
|
// (and the cut-off warning) right after a card edit settles.
|
||||||
|
let gridPreviewFollowUpTimer = null;
|
||||||
|
function scheduleGridPreviewFollowUp() {
|
||||||
|
if (gridPreviewFollowUpTimer) clearTimeout(gridPreviewFollowUpTimer);
|
||||||
|
gridPreviewFollowUpTimer = setTimeout(() => {
|
||||||
|
gridPreviewFollowUpTimer = null;
|
||||||
|
updateCardGridPreview();
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
function displayErrors() {
|
function displayErrors() {
|
||||||
chrome.storage.local.get("errors", storage => {
|
chrome.storage.local.get("errors", storage => {
|
||||||
storage["errors"].forEach(e => {
|
storage["errors"].forEach(e => {
|
||||||
@ -492,6 +511,11 @@ function setupCardGrid(initial) {
|
|||||||
document.getElementById("card_grid_center_rows").addEventListener("change", () => {
|
document.getElementById("card_grid_center_rows").addEventListener("change", () => {
|
||||||
updateCardGridPreview();
|
updateCardGridPreview();
|
||||||
});
|
});
|
||||||
|
// Flexible grid changes what the warning means (cards wrap instead of
|
||||||
|
// being cut off), so re-render it on toggle.
|
||||||
|
document.getElementById("card_grid_flex")?.addEventListener("change", () => {
|
||||||
|
updateCardGridPreview();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid count/spacing inputs for the card grid. Update the preview
|
// Grid count/spacing inputs for the card grid. Update the preview
|
||||||
@ -509,11 +533,13 @@ function setupCardGridCountInput(id, initial, fallback = 1) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number of dashboard cards that will occupy grid cells: every stored card
|
// Fallback count of dashboard cards that will occupy grid cells, used only
|
||||||
// that isn't hidden via the card menu. Hidden cards get display:none in the
|
// when no dashboard tab answered the live "getGridCardCount" query (e.g. the
|
||||||
// dashboard, so they don't take up a grid slot.
|
// options page was opened outside a Canvas window). Every stored card that
|
||||||
|
// isn't hidden via the card menu would be shown, so it doesn't take up a
|
||||||
|
// grid slot. Treated as truthy so legacy string values ("true") count too.
|
||||||
function countActiveDashboardCards(cards) {
|
function countActiveDashboardCards(cards) {
|
||||||
return Object.values(cards || {}).filter(c => c && typeof c === "object" && c.hidden !== true && c.hide !== true).length;
|
return Object.values(cards || {}).filter(c => c && typeof c === "object" && !c.hidden && !c.hide).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Squares preview: one square per grid cell. Filled squares = cells covered
|
// Squares preview: one square per grid cell. Filled squares = cells covered
|
||||||
@ -534,10 +560,20 @@ function updateCardGridPreview() {
|
|||||||
preview.style.columnGap = `${colGap}px`;
|
preview.style.columnGap = `${colGap}px`;
|
||||||
preview.style.rowGap = "4px";
|
preview.style.rowGap = "4px";
|
||||||
preview.innerHTML = "";
|
preview.innerHTML = "";
|
||||||
chrome.storage.sync.get("custom_cards", s => {
|
chrome.storage.sync.get(["custom_cards", "gpa_calc"], async s => {
|
||||||
// Grid may have been toggled off while the storage read was pending.
|
// Grid may have been toggled off while the storage read was pending.
|
||||||
if (document.getElementById("card-grid-options").style.display === "none") return;
|
if (document.getElementById("card-grid-options").style.display === "none") return;
|
||||||
const active = countActiveDashboardCards(s.custom_cards);
|
// Prefer the live count from an open dashboard tab: the page itself
|
||||||
|
// knows exactly which cards are rendered and not display:none (hidden
|
||||||
|
// via the card menu), so the preview/warning can't drift out of sync
|
||||||
|
// with stored data (stale entries, other-domain cards, legacy values).
|
||||||
|
// Fall back to the storage-derived count when no dashboard answers.
|
||||||
|
const live = await sendFromPopup("getGridCardCount");
|
||||||
|
let active = (live && typeof live.count === "number" && live.count >= 0)
|
||||||
|
? live.count
|
||||||
|
: countActiveDashboardCards(s.custom_cards);
|
||||||
|
// The GPA calculator card occupies one card slot in the grid too.
|
||||||
|
if (s.gpa_calc === true) active += 1;
|
||||||
// Uneven-row centering: the real grid shifts the last partial row in
|
// Uneven-row centering: the real grid shifts the last partial row in
|
||||||
// half-card steps (double sub-columns — see centerUnevenGridRows).
|
// half-card steps (double sub-columns — see centerUnevenGridRows).
|
||||||
// In the preview that's a margin on every square of that row; since
|
// In the preview that's a margin on every square of that row; since
|
||||||
@ -564,8 +600,14 @@ function updateCardGridPreview() {
|
|||||||
}
|
}
|
||||||
const warning = document.getElementById("card-grid-warning");
|
const warning = document.getElementById("card-grid-warning");
|
||||||
if (warning) {
|
if (warning) {
|
||||||
if (active > total) {
|
if (s.card_grid_flex === true) {
|
||||||
warning.textContent = `\u26A0 Your grid holds ${total} card${total === 1 ? "" : "s"} but you have ${active} active card${active === 1 ? "" : "s"} - some cards may be cut off.`;
|
// Flexible grid never clips: cards wrap into new rows as the
|
||||||
|
// window narrows, so the cut-off warning doesn't apply.
|
||||||
|
warning.textContent = "Flexible grid is on - cards wrap to fit narrow windows instead of being cut off.";
|
||||||
|
warning.style.display = "block";
|
||||||
|
} else if (active > total) {
|
||||||
|
const gpaNote = s.gpa_calc === true ? " (including the GPA calculator)" : "";
|
||||||
|
warning.textContent = `\u26A0 Your grid holds ${total} card${total === 1 ? "" : "s"} but you have ${active} card${active === 1 ? "" : "s"}${gpaNote} - some cards may be cut off.`;
|
||||||
warning.style.display = "block";
|
warning.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
warning.style.display = "none";
|
warning.style.display = "none";
|
||||||
@ -716,7 +758,7 @@ function toggleAlternateColorsVisibility(darkModeOn) {
|
|||||||
|
|
||||||
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
|
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
|
||||||
function toggleSubOptionsVisibility(option, isOn) {
|
function toggleSubOptionsVisibility(option, isOn) {
|
||||||
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics"];
|
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"];
|
||||||
if (!togglesWithSubOptions.includes(option)) return;
|
if (!togglesWithSubOptions.includes(option)) return;
|
||||||
const optionEl = document.getElementById(option);
|
const optionEl = document.getElementById(option);
|
||||||
if (!optionEl) return;
|
if (!optionEl) return;
|
||||||
@ -1098,6 +1140,7 @@ function setup() {
|
|||||||
"grade_hover",
|
"grade_hover",
|
||||||
"card_letter",
|
"card_letter",
|
||||||
"auto_detect_disabled",
|
"auto_detect_disabled",
|
||||||
|
"hide_course_images",
|
||||||
// "hide_completed",
|
// "hide_completed",
|
||||||
"hover_preview",
|
"hover_preview",
|
||||||
"customBackgroundDaily",
|
"customBackgroundDaily",
|
||||||
@ -1107,6 +1150,7 @@ function setup() {
|
|||||||
"customCardStyles",
|
"customCardStyles",
|
||||||
"card_grid",
|
"card_grid",
|
||||||
"card_grid_center_rows",
|
"card_grid_center_rows",
|
||||||
|
"card_grid_flex",
|
||||||
"grade_analytics_zones",
|
"grade_analytics_zones",
|
||||||
],
|
],
|
||||||
tabs: {
|
tabs: {
|
||||||
@ -1289,7 +1333,7 @@ function setup() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
toggleBetterSidebarSubOptions(sync["better_sidebar"] === true);
|
toggleBetterSidebarSubOptions(sync["better_sidebar"] === true);
|
||||||
["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics"].forEach(opt => {
|
["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => {
|
||||||
toggleSubOptionsVisibility(opt, sync[opt] === true);
|
toggleSubOptionsVisibility(opt, sync[opt] === true);
|
||||||
});
|
});
|
||||||
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user