From befce19c90f4d04152501a2f27f90f1317470db5 Mon Sep 17 00:00:00 2001 From: Guy Sandler Date: Fri, 9 Oct 2026 19:15:56 -0700 Subject: [PATCH] some updates to hide personal info --- css/popup.css | 8 +- html/popup.html | 30 ++++- js/content.js | 329 +++++++++++++++++++++++++++++++++++++++++++++--- js/popup.js | 94 ++++++++++++++ 4 files changed, 437 insertions(+), 24 deletions(-) diff --git a/css/popup.css b/css/popup.css index 8c47183..220b28e 100644 --- a/css/popup.css +++ b/css/popup.css @@ -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, diff --git a/html/popup.html b/html/popup.html index a10af25..f3af324 100644 --- a/html/popup.html +++ b/html/popup.html @@ -1263,12 +1263,40 @@
Hide personal details -

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).

+
+ + +
+
+ + +
+ +
+ + + + / + + complete +
+
+ +

diff --git a/js/content.js b/js/content.js index bc0cfca..3c0380a 100644 --- a/js/content.js +++ b/js/content.js @@ -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 ?? - options.custom_cards_3?.[item.course_id]?.color ?? - options.custom_cards_3?.[item.plannable.course_id]?.color ?? - "#cccccc"; + 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"); // "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 ? `` : ""; @@ -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 ?? - options.custom_cards_3?.[item.course_id]?.color ?? - options.custom_cards_3?.[item.plannable?.course_id]?.color ?? - "#cccccc"; + 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"); 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:" when the id is known, "t:" 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 { diff --git a/js/popup.js b/js/popup.js index b79ef07..698682e 100644 --- a/js/popup.js +++ b/js/popup.js @@ -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.