some updates to hide personal info

This commit is contained in:
Guy Sandler 2026-10-09 19:15:56 -07:00
parent 1b29e80f19
commit befce19c90
4 changed files with 437 additions and 24 deletions

View File

@ -76,10 +76,12 @@ h2 {margin-top: 20px;font-weight:600}
.custom-card-hide {display: flex; align-items: center;}
.option-name {font-weight: 600}
.sub-text {font-size: 12px;color: #c7c7c7}
#todo_progress_rings {font-size: 12px;background: var(--inputbg);color:#e2e2e2;border:none;border-radius:7px;padding:6px 8px;font-family:inherit;width:100%;box-sizing:border-box;margin-top:2px;color-scheme:dark;}
#todo_progress_rings option {background:#1e1e1e;color:#e2e2e2;}
#todo_progress_rings, #anon_grade_preset {font-size: 12px;background: var(--inputbg);color:#e2e2e2;border:none;border-radius:7px;padding:6px 8px;font-family:inherit;width:100%;box-sizing:border-box;margin-top:2px;color-scheme:dark;}
#todo_progress_rings option, #anon_grade_preset option {background:#1e1e1e;color:#e2e2e2;}
#todo_progress_rings option:checked,
#todo_progress_rings option:hover {background:#2e2e2e;color:#fff;}
#todo_progress_rings option:hover,
#anon_grade_preset option:checked,
#anon_grade_preset option:hover {background:#2e2e2e;color:#fff;}
#todo_timeframe {font-size: 12px;background: var(--inputbg);color:#e2e2e2;border:none;border-radius:7px;padding:6px 8px;font-family:inherit;width:100%;box-sizing:border-box;margin-top:2px;color-scheme:dark;}
#todo_timeframe option {background:#1e1e1e;color:#e2e2e2;}
#todo_timeframe option:checked,

View File

@ -1263,12 +1263,40 @@
<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 class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
<label for="anon_grade_preset" class="sub-text">Fake grade level</label>
<select id="anon_grade_preset" name="anon_grade_preset">
<option value="high">High</option>
<option value="medhigh">Med-High</option>
<option value="med">Medium</option>
<option value="medlow">Med-Low</option>
</select>
</div>
<div class="sub-option">
<input type="checkbox" id="anon_fake_todo" name="anon_fake_todo">
<label for="anon_fake_todo" class="sub-text">Fill the todo list with fake courses</label>
</div>
<div class="sub-option" id="anon_fake_completed_wrap" style="display:none;align-items:center;">
<span class="sub-text">Fake tasks completed:</span>
<input type="number" id="anon_fake_completed" class="card-input" min="0" max="7" value="2" style="width:52px;margin-top:0;">
<span class="sub-text">/ 7</span>
</div>
<div class="sub-option" style="align-items:center;flex-wrap:wrap;">
<input type="checkbox" id="anon_progress_enabled" name="anon_progress_enabled">
<label for="anon_progress_enabled" class="sub-text">Fake progress display:</label>
<input type="number" id="anon_progress_done" class="card-input" min="0" max="999" value="12" style="width:52px;margin-top:0;">
<span class="sub-text">/</span>
<input type="number" id="anon_progress_total" class="card-input" min="1" max="999" value="18" style="width:52px;margin-top:0;">
<span class="sub-text">complete</span>
</div>
<div class="sub-option">
<button id="anon-reroll-btn" class="customization-button" style="width:100%;">Reroll classes</button>
</div>
</div>
</div>
<p id="dev-alert"></p>

View File

@ -1098,6 +1098,15 @@ function applyOptionsChanges(changes) {
}
break;
case "hide_personal_details":
case "anon_grade_level":
case "anon_grade_seed":
// "Reroll classes": the fake-course mapping is memoized, so a new
// seed has to reset it before anything re-renders.
if (key === "anon_grade_seed") anonCourseMap.clear();
// The GPA calculator caches its rendered state; drop the marker
// so setupGPACalc below rebuilds it with fake/real values instead
// of leaving the previous inputs in place.
document.querySelectorAll(".canvasrefined-gpa-card, .canvasrefined-gpa").forEach(el => el.removeAttribute("data-canvasrefined-gpa-rendered"));
// Starts/stops the DOM watcher; with it off, restores the real
// text/attributes stashed in dataset markers.
setupPersonalDetailsHiding();
@ -1114,6 +1123,20 @@ function applyOptionsChanges(changes) {
cardAssignments = preloadAssignmentEls();
loadCardAssignments();
break;
case "anon_fake_todo":
case "anon_fake_completed":
case "anon_progress_enabled":
case "anon_progress_done":
case "anon_progress_total":
// Adds/removes the fake todo items (or changes how many are done,
// or rewrites the progress readout) without touching anything else.
if (options.better_todo && document.getElementById("better-todo-main")) {
moreAnnouncementCount = 0;
moreAssignmentCount = 0;
clearTodoList();
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
}
break;
case "hide_course_images":
applyCourseImageHiding();
break;
@ -1254,6 +1277,10 @@ function applyOptionsChanges(changes) {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = getTodoScopedData(data, courseId);
// Rings must match the list when fake items are shown.
if (options.hide_personal_details === true && options.anon_fake_todo === true) {
scopedData.push(...buildFakeTodoItems());
}
renderProgressRings(placeholder, scopedData);
});
}
@ -2909,6 +2936,54 @@ function renderProgressOneLine(wrapper, shown, totalAll, completedAll, percent)
});
}
// "Fake progress display": rewrite the per-course completion entries so the
// progress readout shows exactly `anon_progress_done` out of
// `anon_progress_total`. Course totals are re-distributed proportionally to
// their real share, completion ratios get a small per-course offset (seeded,
// so rerolling varies it) so the rings look organic, and rounding drift is
// corrected so the displayed sums add up to exactly the chosen numbers.
function applyAnonProgressOverride(entries) {
const rawDone = parseInt(options.anon_progress_done);
const rawTotal = parseInt(options.anon_progress_total);
const total = isNaN(rawTotal) ? 18 : Math.max(1, Math.min(999, rawTotal));
const done = isNaN(rawDone) ? 12 : Math.max(0, Math.min(total, rawDone));
// Spread the faked total across courses in proportion to their real
// share of the workload (minimum 1 each so every ring stays visible).
const realTotal = entries.reduce((s, e) => s + e.total, 0) || 1;
let assignedTotal = 0;
entries.forEach((e, i) => {
if (i === entries.length - 1) {
e.total = Math.max(1, total - assignedTotal);
} else {
e.total = Math.max(1, Math.round((total * e.total) / realTotal));
}
assignedTotal += e.total;
});
// Completed counts: target ratio plus a seeded per-course offset so the
// rings don't all show the same fill.
const ratio = done / total;
let assignedDone = 0;
entries.forEach(e => {
const variation = ((anonSeededHash("ring:" + e.courseId) % 100) / 100 - 0.5) * 0.3;
e.completed = Math.max(0, Math.min(e.total, Math.round(e.total * (ratio + variation))));
assignedDone += e.completed;
});
// Correct rounding drift so the sums match the chosen x/y exactly.
let drift = done - assignedDone;
while (drift !== 0) {
let changed = false;
for (const e of entries) {
if (drift > 0 && e.completed < e.total) { e.completed++; drift--; changed = true; }
else if (drift < 0 && e.completed > 0) { e.completed--; drift++; changed = true; }
if (drift === 0) break;
}
if (!changed) break;
}
}
function renderProgressRings(container, scopedData) {
const mode = getProgressRingMode();
if (mode === "none") { container.innerHTML = ""; return; }
@ -2974,6 +3049,15 @@ function renderProgressRings(container, scopedData) {
});
const shown = entries.slice(0, 6);
// "Fake progress display" (Hide personal details): when enabled, the
// readout reports the user's chosen completed/total instead of the real
// counts. Per-course rings are re-distributed around that ratio (with a
// little per-course variation, so every ring isn't filled identically)
// and the sums are corrected to add up to exactly x/y.
if (options.hide_personal_details === true && options.anon_progress_enabled === true && shown.length > 0) {
applyAnonProgressOverride(shown);
}
const totalAll = shown.reduce((s, e) => s + e.total, 0);
const completedAll = shown.reduce((s, e) => s + e.completed, 0);
const percent = totalAll === 0 ? 0 : Math.round((completedAll / totalAll) * 100);
@ -3650,6 +3734,12 @@ async function createTodoSections(location) {
if (renderGen !== todoRenderGen) return;
const courseId = getCurrentCourseId();
const scopedData = getTodoScopedData(data, courseId);
// "Fill the todo list with fake courses": fake items join the same
// scoped pipeline as real ones, so the Tasks list AND the progress
// rings count them identically (rings read from scopedData too).
if (options.hide_personal_details === true && options.anon_fake_todo === true) {
scopedData.push(...buildFakeTodoItems());
}
// Clicking a color in the progress display filters the list to that
// one class. The filter only makes sense where multiple classes show
@ -3956,6 +4046,9 @@ async function getTodoPreviewText(item) {
const id = item.plannable_id;
const key = type + ":" + id;
if (todoPreviewCache.has(key)) return todoPreviewCache.get(key);
// Fake todo items ("Fill the todo list with fake courses") have no real
// Canvas page behind them, so never hit the API for them.
if (item.anonFake === true) return "No details given";
// Custom task (planner note): the description is already on the planner
// item as item.plannable.details, so no API call is needed.
if (type === "planner_note" || (item.planner_override && item.planner_override.custom === true)) {
@ -4215,10 +4308,12 @@ function populateAssignments(iscompleted = false) {
}
const courseColor =
options.custom_cards_3?.[String(item.course_id)]?.color ??
item.anonFake === true
? item.anonColor // fake items carry their (real) course color
: (options.custom_cards_3?.[String(item.course_id)]?.color ??
options.custom_cards_3?.[item.course_id]?.color ??
options.custom_cards_3?.[item.plannable.course_id]?.color ??
"#cccccc";
"#cccccc");
// "Ignore card colors" (Better Todo List): when on, the class name is
// rendered black in light mode or the theme text color in dark mode
@ -4235,7 +4330,7 @@ function populateAssignments(iscompleted = false) {
// list shows it regardless of the checkmark state, so the toggle is
// disabled for it (see the checkmark listener below).
const wasSubmitted = item.submissions?.submitted === true;
const taskHref = isCustomTask ? customTaskHref(item) : (domain + item.html_url);
const taskHref = isCustomTask ? customTaskHref(item) : (item.anonFake === true ? "#" : domain + item.html_url);
const editButtonSvg = isCustomTask
? `<svg class="better-todo-assignment-edit" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:18px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'" title="Edit this custom task"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" stroke="var(--bctext-0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`
: "";
@ -4288,6 +4383,7 @@ function populateAssignments(iscompleted = false) {
// again (for genuinely submitted items this sets a local pin, since
// Canvas has no way to un-submit).
assignment.querySelector(".better-todo-assignment-checkmark").addEventListener("click", () => {
if (item.anonFake === true) return; // fake items can't be completed in Canvas
console.log("marking ", item.plannable.title, iscompleted ? "as incomplete" : "as complete");
markAs(item, assignment.firstElementChild, !iscompleted);
});
@ -4594,6 +4690,10 @@ function markAs(item, element, makeComplete) {
break;
}
}
// Keep the rings consistent with the list when fake items are shown.
if (options.hide_personal_details === true && options.anon_fake_todo === true) {
scopedData.push(...buildFakeTodoItems());
}
renderProgressRings(progressPlaceholder, scopedData);
});
@ -5280,6 +5380,13 @@ async function loadBetterTodo() {
chrome.storage.sync.get(options.custom_assignments_overflow, storage => {
//assignmentData = assignmentData === null ? data : assignmentData;
let items = combineAssignments(data);
// Same "fill the todo list with fake courses" behavior as the
// modern renderer (see createTodoSections). Re-sorted so the
// fake items slot into chronological order like real ones.
if (options.hide_personal_details === true && options.anon_fake_todo === true) {
items = items.concat(buildFakeTodoItems())
.sort((a, b) => new Date(a.plannable_date) - new Date(b.plannable_date));
}
items.forEach((item, index) => {
let date = new Date(item.plannable_date);
let itemState = options.assignment_states[item.plannable_id];
@ -5314,10 +5421,12 @@ async function loadBetterTodo() {
let listItem = listItemContainer.querySelector(".canvasrefined-todo-item");
const courseColor =
options.custom_cards_3?.[String(item.course_id)]?.color ??
item.anonFake === true
? item.anonColor // fake items carry their (real) course color
: (options.custom_cards_3?.[String(item.course_id)]?.color ??
options.custom_cards_3?.[item.course_id]?.color ??
options.custom_cards_3?.[item.plannable?.course_id]?.color ??
"#cccccc";
"#cccccc");
if (itemState?.["lbl"] && itemState["lbl"] !== "") {
makeElement("span", listItem.querySelector(".canvasrefined-todo-item-header"), { "className": "canvasrefined-todo-label", "textContent": itemState["lbl"] });
}
@ -5343,6 +5452,12 @@ async function loadBetterTodo() {
delay = setTimeout(async () => {
if (listItem.classList.contains("canvasrefined-todo-hover")) {
previewTitle.textContent = item.plannable.title;
// Fake todo items have no Canvas page behind them.
if (item.anonFake === true) {
previewText.textContent = "No details given";
preview.style.display = "block";
return;
}
// custom assignment (planner note): preview its description/details
if (customItem) {
const details = item.plannable && item.plannable.details ? item.plannable.details : "";
@ -5931,6 +6046,12 @@ function anonHash(str) {
return h >>> 0;
}
// Same hash, but mixed with the "Reroll classes" seed so the popup button can
// reshuffle every fake course/assignment/grade at once without a page refresh.
function anonSeededHash(key) {
return anonHash(String(key) + "/" + String(options.anon_grade_seed ?? 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.
@ -5939,7 +6060,7 @@ 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;
let idx = anonSeededHash(k) % FAKE_COURSES.length;
if (used.size < FAKE_COURSES.length) {
while (used.has(idx)) idx = (idx + 1) % FAKE_COURSES.length;
}
@ -5954,27 +6075,80 @@ function fakeCourseLabel(key) {
// "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];
return FAKE_ASSIGNMENT_TITLES[anonSeededHash(key) % FAKE_ASSIGNMENT_TITLES.length];
}
function fakeFeedbackComment(key) {
return FAKE_FEEDBACK_COMMENTS[anonHash(String(key)) % FAKE_FEEDBACK_COMMENTS.length];
return FAKE_FEEDBACK_COMMENTS[anonSeededHash(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);
const earned = 13 + (anonSeededHash(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).
// Letter-grade mixes for the "Fake grade level" dropdown. Each preset is a
// general blend of letter grades centered on that level — real gradebooks
// have spread, so a screenshot shows a believable mix (mostly that level,
// with a tail of grades above and below) instead of a wall of identical
// letters. Weights are percentages and sum to 100.
const ANON_GRADE_MIXES = {
high: [["A+", 16], ["A", 30], ["A-", 22], ["B+", 14], ["B", 10], ["B-", 8]],
medhigh: [["A", 5], ["A-", 13], ["B+", 22], ["B", 24], ["B-", 18], ["C+", 12], ["C", 6]],
med: [["B", 5], ["B-", 12], ["C+", 21], ["C", 24], ["C-", 19], ["D+", 12], ["D", 7]],
medlow: [["C", 5], ["C-", 12], ["D+", 20], ["D", 23], ["D-", 20], ["F", 20]],
};
// Letters ordered highest -> lowest, used to derive each letter's percent band
// from the user's own GPA cutoffs (a "B+" lives at [B+.cutoff, A-.cutoff)).
const ANON_LETTER_ORDER = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F"];
// Deterministic fake percent for a key: pick a letter from the selected
// preset's mix, then a plausible percent inside that letter's band on the
// user's own GPA scale, so the number and letter always agree (the GPA
// calculator re-derives the letter from the percent).
function fakeGradePercent(key) {
const mix = ANON_GRADE_MIXES[options.anon_grade_level] || ANON_GRADE_MIXES.high;
const bounds = options.gpa_calc_bounds;
// Letter pick and percent pick use different hash channels so two
// courses with the same letter still get different percentages.
let pick = anonSeededHash(key) % 100;
let letter = mix[mix.length - 1][0];
for (const [l, weight] of mix) {
if (pick < weight) { letter = l; break; }
pick -= weight;
}
// Small outlier chance: ~1 in 9 courses jumps two letters up or down
// from its drawn grade, like the one surprise class every real transcript
// seems to have. Outliers never leave the A..F range.
if (anonSeededHash(key + ":out") % 9 === 0) {
const dir = anonSeededHash(key + ":dir") % 2 === 0 ? -2 : 2;
const idx = ANON_LETTER_ORDER.indexOf(letter);
letter = ANON_LETTER_ORDER[Math.min(ANON_LETTER_ORDER.length - 1, Math.max(0, idx + dir))];
}
const idx = ANON_LETTER_ORDER.indexOf(letter);
const min = bounds && bounds[letter] ? Number(bounds[letter].cutoff) : 70;
const upper = bounds && idx > 0 && bounds[ANON_LETTER_ORDER[idx - 1]] ? Number(bounds[ANON_LETTER_ORDER[idx - 1]].cutoff) : 100;
const max = Math.max(min + 0.5, upper - 0.1); // never reach the next letter's cutoff
let percent = Math.round((min + (anonSeededHash(key + ":pct") % 997) / 1000 * (max - min)) * 10) / 10;
if (percent > max) percent = max;
return percent;
}
function fakeGradeText(key) {
const h = anonHash(String(key));
const percent = Math.round((70 + (h % 300) / 10) * 10) / 10;
const percent = fakeGradePercent(key);
const letter = percentToLetterGrade(percent);
return (letter ? `${letter} ` : "") + `${percent}%`;
}
// Fake GPA (out of 4) for the calculator's cumulative row: take the letter of
// the fake grade and look up what the user's own GPA scale awards for it.
function fakeCumulativeGpa() {
const letter = percentToLetterGrade(fakeGradePercent("cumulative"));
const bounds = options.gpa_calc_bounds;
return bounds && bounds[letter] ? bounds[letter].gpa : 3.5;
}
// 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) {
@ -5988,7 +6162,14 @@ function anonFakeTerm(orig) {
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());
const cid = item?.course_id ?? item?.context_id;
// Numeric course ids hash under the same "id:<n>" key the dashboard cards
// use, so a course shows the SAME fake name everywhere (card, todo list,
// fake items impersonating it).
const key = (cid != null && /^\d+$/.test(String(cid)))
? "id:" + cid
: (cid != null ? String(cid) : "name:" + String(real).trim().toLowerCase());
return fakeCourseLabel(key);
}
function anonTitleForItem(item) {
const real = item?.plannable?.title ?? "";
@ -5997,6 +6178,91 @@ function anonTitleForItem(item) {
return fakeAssignmentTitle(id != null ? "a:" + id : "t:" + String(real).trim());
}
// Fallback palette for fake todo items when the dashboard has no colored
// cards to impersonate (fresh install / no card data).
const ANON_FAKE_COLORS = ["#E24C4B", "#E28227", "#7A9E29", "#0B99AF", "#6A68D9", "#9B59D0", "#C24A9F", "#357FA3"];
// Pool of real dashboard courses fake todo items impersonate. Using the
// user's actual courses means fake items inherit their real card colors and
// the same fake name their dashboard card shows, so the todo list matches
// the dashboard in a screenshot. Courses hidden from the dashboard or from
// the todo list are excluded so a fake item never appears for a course whose
// real items wouldn't.
function getAnonFakeCoursePool() {
const cards = options.custom_cards_3 || {};
const pool = [];
Object.keys(cards).forEach(id => {
const card = cards[id];
if (!card || typeof card !== "object") return;
if (!/^\d+$/.test(String(id))) return; // only real course ids
if (isCourseHidden(id) || isCourseTodoHidden(id)) return;
pool.push({ id: String(id), color: card.color || null });
});
return pool;
}
// "Fill the todo list with fake courses" (Hide personal details sub-option):
// builds extra planner-style items impersonating the user's real dashboard
// courses (same color, same fake name the card shows), so a screenshot of
// the Better Todo List doesn't reveal how many real courses or assignments
// exist. Items are deterministic per reroll seed and marked with anonFake so
// click/hover handlers never hit the Canvas API with fake ids.
function buildFakeTodoItems() {
const types = ["assignment", "quiz", "assignment", "discussion_topic", "assignment", "quiz", "assignment"];
const dayOffsets = [0, 1, 2, 3, 5, 8, 12];
const dueHours = [23, 23, 20, 23, 18, 23, 22];
const pool = getAnonFakeCoursePool();
const now = Date.now();
// "Fake tasks completed" (popup spinner): how many of the fake items show
// as done. The progress rings count them, so this sets the x/y in the
// rings' "x/y done" readout (and the Completed tab, if the user opens it).
const rawDone = parseInt(options.anon_fake_completed);
const doneCount = isNaN(rawDone) ? 2 : Math.max(0, Math.min(types.length, rawDone));
// Which items are done is a seeded shuffle so rerolling changes it too.
const doneSet = new Set(
types.map((_, i) => i)
.sort((a, b) => anonSeededHash("done:" + b) - anonSeededHash("done:" + a))
.slice(0, doneCount)
);
const items = [];
for (let i = 0; i < types.length; i++) {
let courseId, color;
if (pool.length > 0) {
// Each fake item is attributed to one of the user's real courses
// (picked per seed), so it inherits that course's dashboard color
// and its anonymized name.
const pick = pool[anonSeededHash("pick:" + i) % pool.length];
courseId = pick.id;
color = pick.color || ANON_FAKE_COLORS[anonSeededHash(String(courseId)) % ANON_FAKE_COLORS.length];
} else {
// No colored dashboard cards available — fall back to anonymous
// courses with a neutral palette.
courseId = "anonfake-" + i;
color = ANON_FAKE_COLORS[anonSeededHash(String(courseId)) % ANON_FAKE_COLORS.length];
}
const id = -(anonSeededHash("todoId:" + i) % 900000) - 1;
const due = new Date(now + dayOffsets[i] * 86400000);
due.setHours(dueHours[i], 59, 0, 0);
const isDone = doneSet.has(i);
items.push({
anonFake: true,
anonColor: color,
plannable_id: id,
plannable_type: types[i],
plannable_date: due.toISOString(),
plannable: { id: id, title: fakeAssignmentTitle("a:" + id) },
course_id: courseId,
context_id: courseId,
context_name: fakeCourseLabel(courseId),
html_url: "#",
// Done fake items carry a submitted+graded submission so both the
// rings and the Completed tab count them; unfinished ones don't.
submissions: isDone ? { submitted: true, graded: true } : { submitted: false },
});
}
return items;
}
// 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) {
@ -6665,7 +6931,14 @@ function createGPACalcCourse(location, course) {
let percent = makeElement("span", changerContainer, { "className": "canvasrefined-course-percent-sign", "textContent": course.id === "cumulative" ? "/4" : "%" });
let courseGrade = course?.enrollments[0].has_grading_periods === true ? course.enrollments[0].current_period_computed_current_score : course.enrollments[0].computed_current_score;
if (customs["gr"] !== null) {
// "Hide personal details": the calculator gets the same fake grade the
// dashboard card shows for this course (same "id:<id>" key, so rerolling
// keeps them consistent). The cumulative row gets a fake GPA derived from
// the fake grade's letter on the user's own scale.
const anonGpa = options.hide_personal_details === true;
if (anonGpa) {
changer.value = course.id === "cumulative" ? fakeCumulativeGpa() : fakeGradePercent("id:" + course.id);
} else if (customs["gr"] !== null) {
changer.value = customs["gr"];
} else if (courseGrade) {
changer.value = courseGrade;
@ -6673,11 +6946,18 @@ function createGPACalcCourse(location, course) {
changer.value = "--";
}
// Same fake label the dashboard card uses for this course.
if (anonGpa && course.id !== "cumulative") {
courseName.textContent = fakeCourseLabel("id:" + course.id);
}
if (course.id !== "cumulative") {
let weightSelections = makeElement("form", courseContainer, { "className": "canvasrefined-course-weights" });
weightSelections.innerHTML = '<select name="weight-selection" class="canvasrefined-course-weight"><option value="dnc">Do not count</option><option value="regular">Regular/College</option><option value="honors">Honors</option><option value="ap">AP/IB</option></select>';
let weightChanger = weightSelections.querySelector(".canvasrefined-course-weight");
weightChanger.value = changer.value === "--" ? "dnc" : customs.weight;
// Count the course only when it has a real grade (or a saved custom
// one) — a fake grade must not resurrect courses the user left blank.
weightChanger.value = (courseGrade || customs["gr"] !== null) ? customs.weight : "dnc";
weightChanger.addEventListener('change', () => changeGPASettings(course.id, { "weight": weightSelections.querySelector(".canvasrefined-course-weight").value }));
let useCustomGr = makeElement("input", courseContainer, { "className": "canvasrefined-course-customgr", "type": "checkbox", "checked": customs.gr !== null ? true : false });
@ -6691,10 +6971,19 @@ function createGPACalcCourse(location, course) {
changeGPASettings(course.id, { "gr": changer.value });
}
}
// While personal details are hidden the input holds a fake grade;
// put it back so a stray toggle can't reveal (or save) the real one.
if (options.hide_personal_details === true) changer.value = fakeGradePercent("id:" + course.id);
});
}
changer.addEventListener('input', (e) => {
// While personal details are hidden the input shows a fake grade —
// recalculate the display but never persist fake values to storage.
if (options.hide_personal_details === true) {
calculateGPA2();
return;
}
if (course.id === "cumulative" || (options["custom_cards"][course.id]["gr"] !== undefined && options["custom_cards"][course.id]["gr"] !== null)) {
changeGPASettings(course.id, { "gr": e.target.value });
} else {

View File

@ -136,6 +136,13 @@ const defaultOptions = {
"card_opacity": 80,
"card_blur": 8,
"todo_hr24": false,
"anon_grade_level": "high",
"anon_grade_seed": 0,
"anon_fake_todo": false,
"anon_fake_completed": 2,
"anon_progress_enabled": false,
"anon_progress_done": 12,
"anon_progress_total": 18,
"todo_separate_scrollbar": false,
"todo_alternate_colors": false,
"todo_ignore_card_colors": false,
@ -416,6 +423,63 @@ function setupTimeframeSelect(initial) {
});
}
// "Fake grade level" dropdown (Hide personal details). Controls the band the
// generated fake grades fall into, so a screenshot can show all As, all Bs, etc.
function setupAnonGradeSelect(initial) {
const el = document.querySelector("#anon_grade_preset");
if (!el) return;
const allowed = ["high", "medhigh", "med", "medlow"];
el.value = allowed.includes(initial) ? initial : "high";
el.addEventListener("change", function () {
chrome.storage.sync.set({ "anon_grade_level": this.value });
});
}
// "Fake tasks completed" spinner (Hide personal details → fake todo list).
// Sets how many of the 7 fake tasks show as done, which drives the x/y in
// the progress rings' "x/y done" readout.
function setupAnonFakeCompleted(initial) {
const el = document.querySelector("#anon_fake_completed");
if (!el) return;
const clamp = (v) => Math.max(0, Math.min(7, v));
const parsed = parseInt(initial);
el.value = isNaN(parsed) ? 2 : clamp(parsed);
el.addEventListener("change", function () {
const v = clamp(parseInt(this.value));
this.value = v;
chrome.storage.sync.set({ "anon_fake_completed": v });
});
}
// The spinner only means something while "Fill the todo list with fake
// courses" is on, so show/hide it with that checkbox.
function toggleAnonFakeCompletedVisibility(show) {
const wrap = document.getElementById("anon_fake_completed_wrap");
if (wrap) wrap.style.display = show ? "flex" : "none";
}
// "Fake progress display" x/y inputs (Hide personal details). While enabled,
// the todo list's progress readout reports exactly this many completed out
// of this many total, regardless of the real counts.
function setupAnonProgressInput(key, initial, isDone) {
const el = document.querySelector("#" + key);
if (!el) return;
const clamp = (v) => {
if (isDone) {
const total = parseInt(document.querySelector("#anon_progress_total")?.value) || 18;
return Math.max(0, Math.min(999, Math.min(total, v)));
}
return Math.max(1, Math.min(999, v));
};
const parsed = parseInt(initial);
el.value = isNaN(parsed) ? (isDone ? 12 : 18) : clamp(parsed);
el.addEventListener("change", function () {
const v = clamp(parseInt(this.value));
this.value = v;
chrome.storage.sync.set({ [key]: v });
});
}
function setupAutoDarkInput(initial, time) {
let el = document.querySelector('#' + time);
el.value = initial.hour + ":" + initial.minute;
@ -1176,6 +1240,8 @@ function setup() {
"card_letter",
"auto_detect_disabled",
"hide_course_images",
"anon_fake_todo",
"anon_progress_enabled",
// "hide_completed",
"hover_preview",
"customBackgroundDaily",
@ -1339,6 +1405,22 @@ function setup() {
identifier: "todo_timeframe",
setup: (initial) => setupTimeframeSelect(initial),
},
{
identifier: "anon_grade_level",
setup: (initial) => setupAnonGradeSelect(initial),
},
{
identifier: "anon_fake_completed",
setup: (initial) => setupAnonFakeCompleted(initial),
},
{
identifier: "anon_progress_done",
setup: (initial) => setupAnonProgressInput("anon_progress_done", initial, true),
},
{
identifier: "anon_progress_total",
setup: (initial) => setupAnonProgressInput("anon_progress_total", initial, false),
},
{
identifier: "todo_more_expanded",
setup: (initial) => setupTodoMoreOptions(initial),
@ -1402,6 +1484,9 @@ function setup() {
} else {
chrome.storage.sync.set(JSON.parse(`{"${option}": ${status}}`));
}
if (option === "anon_fake_todo") {
toggleAnonFakeCompletedVisibility(status);
}
syncCustomBackgroundDailyState(document.querySelector("#customBackgroundDaily")?.checked === true || document.querySelector("#customBackgroundNasaDaily")?.checked === true);
toggleOpacityOptions();
});
@ -1409,6 +1494,7 @@ function setup() {
document.querySelector("#" + option).checked = value;
});
syncCustomBackgroundDailyState(sync.customBackgroundDaily === true || sync.customBackgroundNasaDaily === true);
toggleAnonFakeCompletedVisibility(sync["anon_fake_todo"] === true);
toggleDarkModeDisable(sync.auto_dark);
displayBackgroundPresets();
toggleOpacityOptions();
@ -1502,6 +1588,14 @@ function setup() {
updateStorageUsage();
});
// "Reroll classes" (Report issue tab → Privacy). Bumps the fake-data seed;
// content scripts watch the key, so every open Canvas tab re-shuffles its
// fake courses/assignments/grades without a refresh.
document.querySelector("#anon-reroll-btn").addEventListener("click", () => {
chrome.storage.sync.set({ "anon_grade_seed": Math.floor(Math.random() * 1000000) });
displayAlert(false, "Fake classes rerolled.");
});
// activate planner cache clear button (Report issue tab). Clears the
// cached planner items locally, then tells any open Canvas tabs to
// re-fetch fresh data so the clear takes effect without a refresh.