mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 13:57:17 +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;}
|
||||
h2 {font-size:24px;}
|
||||
.option input{display:none;}
|
||||
.options input:nth-of-type(1).checked ~ .slider .sliderknob{background-color:#e1e1e12e;}
|
||||
.options input:nth-of-type(2).checked ~ .slider .sliderknob{transform:translate(18px);background-color:#8dd28d;}
|
||||
/* Knob state for every big toggle (main options AND tab pages like Report issue) */
|
||||
.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 span{margin-left:8px;}
|
||||
.main{position: relative;padding: 20px}
|
||||
|
||||
@ -880,6 +880,10 @@
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -1236,6 +1240,24 @@
|
||||
<button class="big-button back-btn" data-i18n="back">Back</button>
|
||||
</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>
|
||||
<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>
|
||||
<div class="option-container">
|
||||
<h3 class="header-small">Storage reset</h3>
|
||||
|
||||
509
js/content.js
509
js/content.js
@ -941,6 +941,7 @@ function startExtension() {
|
||||
watchSequenceFooter();
|
||||
watchProfileLogoutPageButton();
|
||||
watchGradeAnalytics();
|
||||
setupPersonalDetailsHiding();
|
||||
|
||||
setupQuizSafeModeBanner();
|
||||
|
||||
@ -1087,6 +1088,26 @@ function applyOptionsChanges(changes) {
|
||||
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
||||
}
|
||||
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_prepend":
|
||||
case "gpa_calc_weighted":
|
||||
@ -1174,6 +1195,7 @@ function applyOptionsChanges(changes) {
|
||||
case "card_grid_column_gap":
|
||||
case "card_grid_row_gap":
|
||||
case "card_grid_center_rows":
|
||||
case "card_grid_flex":
|
||||
case "customCardStyles":
|
||||
// Coalesce rapid card-style edits (e.g. holding the arrow keys on a
|
||||
// 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
|
||||
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 ("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 ("fixdm"): sendResponse(runDarkModeFixer(true)); break;
|
||||
case ("updateBackground"): applyCustomBackground(); sendResponse(true); break;
|
||||
@ -3928,14 +3966,15 @@ async function showTodoPreview(anchor, item) {
|
||||
const el = getTodoPreviewEl();
|
||||
const title = el.querySelector(".canvasrefined-preview-title");
|
||||
const text = el.querySelector(".canvasrefined-preview-text");
|
||||
title.textContent = item.plannable && item.plannable.title ? item.plannable.title : "";
|
||||
title.textContent = anonTitleForItem(item);
|
||||
text.textContent = "Loading…";
|
||||
el.style.display = "block";
|
||||
positionTodoPreview(el, anchor);
|
||||
const content = await getTodoPreviewText(item);
|
||||
if (token !== todoPreviewToken) return; // a newer hover (or hide) superseded this one
|
||||
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
|
||||
}
|
||||
|
||||
@ -3995,8 +4034,10 @@ function todoScoreBadgeHtml(item) {
|
||||
// hasn't run yet (a cached null score means the fetch confirmed no grade).
|
||||
const graded = cached ? cached.score != null : item.submissions?.graded === true;
|
||||
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";
|
||||
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}"`;
|
||||
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 => {
|
||||
const cached = todoScoreCache.get(`${el.dataset.todoCourse}:${el.dataset.todoPlannable}`);
|
||||
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";
|
||||
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")) {
|
||||
el.insertAdjacentHTML("afterend", TODO_GRADED_BADGE_HTML);
|
||||
}
|
||||
@ -4190,8 +4233,8 @@ 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:${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;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>
|
||||
<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;">${anonTitleForItem(item)}</a>
|
||||
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}${scoreBadge}</span>
|
||||
</div>
|
||||
${editButtonSvg}
|
||||
@ -4303,8 +4346,8 @@ function populateAnnouncements() {
|
||||
</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:${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:${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;">${anonTitleForItem(item)}</a>
|
||||
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
|
||||
</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">
|
||||
@ -5113,9 +5156,9 @@ async function loadBetterTodo() {
|
||||
if (itemState?.["crs"] === true) {
|
||||
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;";
|
||||
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;
|
||||
let format = formatTodoDate(date, item.submissions, hr24);
|
||||
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-title span").style.color = 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 {
|
||||
const coursePrefix = "/courses/" + course_id;
|
||||
document.querySelectorAll(".planner-item").forEach(item => {
|
||||
@ -5666,6 +5714,305 @@ function runiframeChecker() {
|
||||
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
|
||||
*/
|
||||
@ -5710,7 +6057,8 @@ function insertGrades() {
|
||||
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" });
|
||||
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) {
|
||||
gradeContainer.classList.add("canvasrefined-hover-only");
|
||||
} else {
|
||||
@ -5741,7 +6089,7 @@ Card assignments
|
||||
function createCardAssignment(assignment) {
|
||||
let assignmentContainer = document.createElement("div");
|
||||
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)) });
|
||||
if (assignment.overdue === true) assignmentDueAt.classList.add("canvasrefined-assignment-overdue");
|
||||
if (assignment?.submissions?.submitted === true) {
|
||||
@ -6748,12 +7096,78 @@ function applyAestheticChanges() {
|
||||
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 gpaOn = options.gpa_calc === true;
|
||||
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 > .ic-DashboardCard{grid-column:span 2!important}`;
|
||||
if (gpaOn) {
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:${gridRows + 1}!important;grid-column:span 2!important}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:${gridRows + 2}!important;grid-column:1/-1!important}`;
|
||||
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);
|
||||
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}`;
|
||||
if (gpaOn) {
|
||||
// "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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.customCardStyles === true) {
|
||||
@ -6835,6 +7249,9 @@ function centerUnevenGridRows() {
|
||||
const cards = Array.from(container.children).filter(el => el.classList && el.classList.contains("ic-DashboardCard"));
|
||||
cards.forEach(card => card.style.removeProperty("grid-column"));
|
||||
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 rows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
||||
const visible = cards.filter(card => getComputedStyle(card).display !== "none");
|
||||
@ -7615,38 +8032,34 @@ function escapeGlobalSearchAttr(s) {
|
||||
}
|
||||
|
||||
function changeGradientCards() {
|
||||
if (options.gradient_cards === true) {
|
||||
let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero');
|
||||
|
||||
// Create the style once; re-appending triggers the MutationObserver and re-runs this function.
|
||||
let cardcss = document.querySelector("#gradientcss");
|
||||
if (!cardcss) {
|
||||
cardcss = document.createElement('style');
|
||||
cardcss.id = "gradientcss";
|
||||
document.documentElement.appendChild(cardcss);
|
||||
// Apply the gradient INLINE on each hero instead of via index-based CSS
|
||||
// rules: the gradient is always computed from that hero's actual current
|
||||
// course color, so it stays in sync even when cards are reordered,
|
||||
// re-rendered, or recolored after this pass (the old #gradientcss
|
||||
// "card N" selectors silently drifted onto the wrong cards).
|
||||
// Only background-image is set inline — background-color (the course
|
||||
// color) is left untouched so it can still be read/recolord later.
|
||||
const heroes = document.querySelectorAll('.ic-DashboardCard__header_hero');
|
||||
heroes.forEach(hero => {
|
||||
if (options.gradient_cards !== true) {
|
||||
hero.style.removeProperty("background-image");
|
||||
return;
|
||||
}
|
||||
const rgb = hero.style.backgroundColor;
|
||||
const parts = rgb.match(/\d+(\.\d+)?/g);
|
||||
// No inline course color (e.g. hero not painted yet): leave untouched
|
||||
// rather than guessing NaN colors.
|
||||
if (!parts || parts.length < 3) return;
|
||||
const [r, g, b] = [parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2])];
|
||||
let [h, s, l] = rgbToHsl(r, g, b);
|
||||
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;
|
||||
hero.style.backgroundImage = `linear-gradient(115deg, hsl(${h}deg,${s}%,${l}%) 5%, hsl(${newh}deg,${s}%,${l}%) 100%)`;
|
||||
});
|
||||
|
||||
// Build CSS into a string and only touch the DOM if it changed.
|
||||
let css = "";
|
||||
for (let i = 0; i < cardheads.length; i++) {
|
||||
let colorone = cardheads[i].style.backgroundColor.split(',');
|
||||
let [r, g, b] = [parseInt(colorone[0].split('(')[1]), parseInt(colorone[1]), parseInt(colorone[2])];
|
||||
let [h, s, l] = [rgbToHsl(r, g, b)[0], rgbToHsl(r, g, b)[1], rgbToHsl(r, g, b)[2]];
|
||||
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;
|
||||
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}";
|
||||
}
|
||||
|
||||
if (cardcss.textContent !== css) {
|
||||
cardcss.textContent = css;
|
||||
}
|
||||
|
||||
} else {
|
||||
let cardcss = document.querySelector("#gradientcss");
|
||||
if (cardcss && cardcss.textContent !== "") {
|
||||
cardcss.textContent = "";
|
||||
}
|
||||
}
|
||||
// Clean up the legacy index-based stylesheet from older versions.
|
||||
const legacy = document.querySelector("#gradientcss");
|
||||
if (legacy) legacy.remove();
|
||||
}
|
||||
|
||||
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 = [
|
||||
"grade_analytics_zones",
|
||||
"hide_course_images",
|
||||
"todo_hide_feedback",
|
||||
"todo_hide_read",
|
||||
"todo_full_height",
|
||||
@ -50,6 +51,7 @@ const syncedSubOptions = [
|
||||
"card_grid_column_gap",
|
||||
"card_grid_row_gap",
|
||||
"card_grid_center_rows",
|
||||
"card_grid_flex",
|
||||
"customBackgroundLink",
|
||||
"customBackgroundScale",
|
||||
"customBackgroundDaily",
|
||||
@ -68,7 +70,7 @@ const localSwitches = [];
|
||||
// Theme export only carries visual settings, never personal productivity data.
|
||||
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
|
||||
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 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"];
|
||||
@ -103,6 +105,8 @@ const defaultOptions = {
|
||||
},
|
||||
"new_install": true,
|
||||
"assignments_due": true,
|
||||
"hide_personal_details": false,
|
||||
"hide_course_images": false,
|
||||
"gpa_calc": false,
|
||||
"dark_mode": true,
|
||||
"gradent_cards": false,
|
||||
@ -204,6 +208,7 @@ const defaultOptions = {
|
||||
"card_grid_column_gap": 12,
|
||||
"card_grid_row_gap": 12,
|
||||
"card_grid_center_rows": false,
|
||||
"card_grid_flex": false,
|
||||
"customCardStyles": false,
|
||||
"customBackgroundLink": "",
|
||||
"customBackgroundScale": 100,
|
||||
@ -225,9 +230,23 @@ chrome.storage.onChanged.addListener((changes) => {
|
||||
// Hidden-card edits from the card menu change how many cells the
|
||||
// grid preview should mark as filled.
|
||||
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() {
|
||||
chrome.storage.local.get("errors", storage => {
|
||||
storage["errors"].forEach(e => {
|
||||
@ -492,6 +511,11 @@ function setupCardGrid(initial) {
|
||||
document.getElementById("card_grid_center_rows").addEventListener("change", () => {
|
||||
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
|
||||
@ -509,11 +533,13 @@ function setupCardGridCountInput(id, initial, fallback = 1) {
|
||||
});
|
||||
}
|
||||
|
||||
// Number of dashboard cards that will occupy grid cells: every stored card
|
||||
// that isn't hidden via the card menu. Hidden cards get display:none in the
|
||||
// dashboard, so they don't take up a grid slot.
|
||||
// Fallback count of dashboard cards that will occupy grid cells, used only
|
||||
// when no dashboard tab answered the live "getGridCardCount" query (e.g. the
|
||||
// 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) {
|
||||
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
|
||||
@ -534,10 +560,20 @@ function updateCardGridPreview() {
|
||||
preview.style.columnGap = `${colGap}px`;
|
||||
preview.style.rowGap = "4px";
|
||||
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.
|
||||
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
|
||||
// half-card steps (double sub-columns — see centerUnevenGridRows).
|
||||
// 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");
|
||||
if (warning) {
|
||||
if (active > total) {
|
||||
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.`;
|
||||
if (s.card_grid_flex === true) {
|
||||
// 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";
|
||||
} else {
|
||||
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.
|
||||
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;
|
||||
const optionEl = document.getElementById(option);
|
||||
if (!optionEl) return;
|
||||
@ -1098,6 +1140,7 @@ function setup() {
|
||||
"grade_hover",
|
||||
"card_letter",
|
||||
"auto_detect_disabled",
|
||||
"hide_course_images",
|
||||
// "hide_completed",
|
||||
"hover_preview",
|
||||
"customBackgroundDaily",
|
||||
@ -1107,6 +1150,7 @@ function setup() {
|
||||
"customCardStyles",
|
||||
"card_grid",
|
||||
"card_grid_center_rows",
|
||||
"card_grid_flex",
|
||||
"grade_analytics_zones",
|
||||
],
|
||||
tabs: {
|
||||
@ -1289,7 +1333,7 @@ function setup() {
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user