mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-25 22:35:08 +02:00
Compare commits
3 Commits
462186b1b7
...
47ce60bc0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ce60bc0d | ||
|
|
9ab7d16bd9 | ||
|
|
4184b717e8 |
@ -315,6 +315,9 @@
|
|||||||
"message": "Grade Analytics"
|
"message": "Grade Analytics"
|
||||||
},
|
},
|
||||||
"grade_analytics_desc": {
|
"grade_analytics_desc": {
|
||||||
"message": "Adds an Analytics toggle to the left side of course grade pages with a score-distribution chart and your overall grade over time."
|
"message": "Adds tools to help you analyze your grades."
|
||||||
|
},
|
||||||
|
"grade_analytics_zones": {
|
||||||
|
"message": "Colored grade zones"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -366,3 +366,26 @@
|
|||||||
padding: 0 4px; color: var(--bctext-1, #5b6770);
|
padding: 0 4px; color: var(--bctext-1, #5b6770);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Grades table: scale down with narrow windows =====
|
||||||
|
Canvas lays out #grades_summary with content-driven auto sizing plus hard
|
||||||
|
min-widths (title 150px, details 80px) and nowrap cells (due, score,
|
||||||
|
details), giving it a ~780px floor below which the whole table overflows
|
||||||
|
the page instead of shrinking. Switch it to fixed layout with percentage
|
||||||
|
column widths (set on the header row, which drives fixed layout) and let
|
||||||
|
tight cells wrap, so the header, body and rows all scale with the
|
||||||
|
viewport. #assignments scrolls horizontally as a last resort (e.g. while a
|
||||||
|
what-if score input is open). */
|
||||||
|
#grades_summary { table-layout: fixed; width: 100%; }
|
||||||
|
#grades_summary thead th:nth-child(1) { width: 30%; } /* Name */
|
||||||
|
#grades_summary thead th:nth-child(2) { width: 12%; } /* Due */
|
||||||
|
#grades_summary thead th:nth-child(3) { width: 10%; } /* Submitted */
|
||||||
|
#grades_summary thead th:nth-child(4) { width: 7%; } /* Status */
|
||||||
|
#grades_summary thead th:nth-child(5) { width: 11%; } /* Score */
|
||||||
|
#grades_summary thead th:nth-child(6) { width: 3%; } /* asset processors */
|
||||||
|
#grades_summary thead th:nth-child(7) { width: 15%; } /* Details */
|
||||||
|
#grades_summary thead th:nth-child(8) { width: 3%; } /* progress */
|
||||||
|
#grades_summary td.due,
|
||||||
|
#grades_summary td.assignment_score,
|
||||||
|
#grades_summary td.details { white-space: normal; }
|
||||||
|
#assignments { overflow-x: auto; }
|
||||||
|
|
||||||
|
|||||||
@ -228,9 +228,15 @@
|
|||||||
</div><span class="option-name" data-i18n="grade_analytics">Grade Analytics</span>
|
</div><span class="option-name" data-i18n="grade_analytics">Grade Analytics</span>
|
||||||
<span class="cr-info-bubble" tabindex="0" role="button" aria-label="Grade Analytics info">
|
<span class="cr-info-bubble" tabindex="0" role="button" aria-label="Grade Analytics info">
|
||||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||||
<span class="cr-info-bubble-text" data-i18n="grade_analytics_desc">Adds an Analytics toggle to the left side of course grade pages with a score-distribution chart and your overall grade over time.</span>
|
<span class="cr-info-bubble-text" data-i18n="grade_analytics_desc">Adds tools to help you analyze your grades.</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sub-options">
|
||||||
|
<div class="sub-option" style="margin-top:5px">
|
||||||
|
<input type="checkbox" id="grade_analytics_zones" name="grade_analytics_zones">
|
||||||
|
<label for="grade_analytics_zones" class="sub-text" data-i18n="grade_analytics_zones">Colored grade zones</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="social-buttons">
|
<div class="social-buttons">
|
||||||
<a class="discord-button" href="https://discord.gg/GjFh4JA3wh" target="_blank" rel="noopener noreferrer" title="Join our Discord">
|
<a class="discord-button" href="https://discord.gg/GjFh4JA3wh" target="_blank" rel="noopener noreferrer" title="Join our Discord">
|
||||||
@ -598,14 +604,6 @@
|
|||||||
<input type="checkbox" id="export-toggles" />
|
<input type="checkbox" id="export-toggles" />
|
||||||
<span>On/off toggles</span>
|
<span>On/off toggles</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-option">
|
|
||||||
<input type="checkbox" id="export-layout" />
|
|
||||||
<span>Layout</span>
|
|
||||||
</div>
|
|
||||||
<div class="sub-option">
|
|
||||||
<input type="checkbox" id="export-sidebar" />
|
|
||||||
<span>Sidebar</span>
|
|
||||||
</div>
|
|
||||||
<div class="sub-option">
|
<div class="sub-option">
|
||||||
<input type="checkbox" id="export-todo" />
|
<input type="checkbox" id="export-todo" />
|
||||||
<span>Todo list</span>
|
<span>Todo list</span>
|
||||||
|
|||||||
@ -118,6 +118,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
|||||||
"sidebar_blur": 0,
|
"sidebar_blur": 0,
|
||||||
"global_search": false,
|
"global_search": false,
|
||||||
"grade_analytics": true,
|
"grade_analytics": true,
|
||||||
|
"grade_analytics_zones": false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
374
js/content.js
374
js/content.js
@ -956,6 +956,10 @@ function applyOptionsChanges(changes) {
|
|||||||
case "grade_analytics":
|
case "grade_analytics":
|
||||||
watchGradeAnalytics();
|
watchGradeAnalytics();
|
||||||
break;
|
break;
|
||||||
|
case "grade_analytics_zones":
|
||||||
|
// Colored 10% zones on the line chart — just redraw the charts.
|
||||||
|
if (gradeAnalyticsActive() && gaOpen && gaData) renderGradeAnalytics();
|
||||||
|
break;
|
||||||
case "quiz_safe_mode":
|
case "quiz_safe_mode":
|
||||||
// Toggling safe mode changes which features run on quiz pages; reload
|
// Toggling safe mode changes which features run on quiz pages; reload
|
||||||
// so the gating is applied cleanly.
|
// so the gating is applied cleanly.
|
||||||
@ -1089,6 +1093,18 @@ async function applyCustomBackground() {
|
|||||||
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%);
|
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
/* Native left nav column: #left-side > #sticky-container.ic-sticky-frame
|
||||||
|
(the course/account/group menu links). Tint the whole #left-side column
|
||||||
|
rather than the inner .ic-sticky-frame, whose height only wraps its
|
||||||
|
links — the column spans the full viewport height like the other
|
||||||
|
sidebars, at the same bg_opacity/bg_blur as the Better Todo List
|
||||||
|
panel. Without this, a custom background (most visible in light mode)
|
||||||
|
shows through untinted behind the nav links. */
|
||||||
|
#left-side {
|
||||||
|
backdrop-filter: blur(${bgBlur}px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(${bgBlur}px) !important;
|
||||||
|
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important;
|
||||||
|
}
|
||||||
/* Recent feedback lives in #right-side. The dark-mode CSS
|
/* Recent feedback lives in #right-side. The dark-mode CSS
|
||||||
(darkmodecss.js) recolors its text, but those rules are dark-mode-
|
(darkmodecss.js) recolors its text, but those rules are dark-mode-
|
||||||
only — so in light mode + custom background the sub-text (context,
|
only — so in light mode + custom background the sub-text (context,
|
||||||
@ -1305,6 +1321,20 @@ async function applyCustomBackground() {
|
|||||||
-webkit-backdrop-filter: blur(${cardBlur}px) saturate(120%) !important;`
|
-webkit-backdrop-filter: blur(${cardBlur}px) saturate(120%) !important;`
|
||||||
: `background: var(--bcbackground-0) !important;`}
|
: `background: var(--bcbackground-0) !important;`}
|
||||||
}
|
}
|
||||||
|
/* Card header strip (the course-nickname bar under the hero). Canvas
|
||||||
|
paints it a solid light color ($ic-color-light) and nothing overrides
|
||||||
|
that in light mode, so with card transparency on it reads as a solid
|
||||||
|
band across an otherwise translucent card. Mirror the card surface:
|
||||||
|
transparent cards drop the strip's background so the card's glass
|
||||||
|
(tint + blur already applied to .ic-DashboardCard) shows through;
|
||||||
|
opaque cards paint it the same solid theme color as the card body.
|
||||||
|
Dark mode already flattens this strip via darkmodecss.js, so this
|
||||||
|
is inert there (same value). */
|
||||||
|
.ic-DashboardCard__header_content {
|
||||||
|
${cardTransparency
|
||||||
|
? `background: none !important;`
|
||||||
|
: `background: var(--bcbackground-0) !important;`}
|
||||||
|
}
|
||||||
tr.student_assignment.assignment_graded.editable > * {
|
tr.student_assignment.assignment_graded.editable > * {
|
||||||
border:none!important
|
border:none!important
|
||||||
}`;
|
}`;
|
||||||
@ -1727,6 +1757,17 @@ const BETTER_TODO_TIMEFRAME_DAYS = {
|
|||||||
let betterTodoProgressFilter = null;
|
let betterTodoProgressFilter = null;
|
||||||
let domContainers = {};
|
let domContainers = {};
|
||||||
|
|
||||||
|
// Better Todo timeframe filter, shared by the task list and the progress
|
||||||
|
// display so their counts always agree: keeps items due on/before now+range
|
||||||
|
// (overdue items are before now, so they are kept too). "all" keeps
|
||||||
|
// everything.
|
||||||
|
function applyTodoTimeframe(items) {
|
||||||
|
betterTodoTimeframe = (options.todo_timeframe && Object.prototype.hasOwnProperty.call(BETTER_TODO_TIMEFRAME_DAYS, options.todo_timeframe)) ? options.todo_timeframe : "all";
|
||||||
|
if (betterTodoTimeframe === "all") return items;
|
||||||
|
const cutoff = Date.now() + (BETTER_TODO_TIMEFRAME_DAYS[betterTodoTimeframe] * 24 * 60 * 60 * 1000);
|
||||||
|
return items.filter(item => new Date(item.plannable_date).getTime() <= cutoff);
|
||||||
|
}
|
||||||
|
|
||||||
// true when `courseId` is the dimmed-out class because another class is selected.
|
// true when `courseId` is the dimmed-out class because another class is selected.
|
||||||
function progressFilterDim(courseId) {
|
function progressFilterDim(courseId) {
|
||||||
return betterTodoProgressFilter != null && String(courseId) !== String(betterTodoProgressFilter);
|
return betterTodoProgressFilter != null && String(courseId) !== String(betterTodoProgressFilter);
|
||||||
@ -2233,7 +2274,9 @@ function renderProgressRings(container, scopedData) {
|
|||||||
const mode = getProgressRingMode();
|
const mode = getProgressRingMode();
|
||||||
if (mode === "none") { container.innerHTML = ""; return; }
|
if (mode === "none") { container.innerHTML = ""; return; }
|
||||||
|
|
||||||
const allAssignments = scopedData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note"));
|
// Apply the same timeframe filter the list uses so the counts in the
|
||||||
|
// display match what's shown below it.
|
||||||
|
const allAssignments = applyTodoTimeframe(scopedData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note")));
|
||||||
|
|
||||||
const groups = {};
|
const groups = {};
|
||||||
allAssignments.forEach(item => {
|
allAssignments.forEach(item => {
|
||||||
@ -2901,13 +2944,9 @@ async function createTodoSections(location) {
|
|||||||
completed = displayData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note") && (item.submissions?.submitted || item.planner_override?.marked_complete));
|
completed = displayData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note") && (item.submissions?.submitted || item.planner_override?.marked_complete));
|
||||||
// The timeframe is a persisted Better Todo List sub-option set in the
|
// The timeframe is a persisted Better Todo List sub-option set in the
|
||||||
// popup. Read the current value each render so popup changes apply on
|
// popup. Read the current value each render so popup changes apply on
|
||||||
// the next render. Keeps items due on/before now+range (overdue items
|
// the next render. Only the Tasks tab is affected (announcements and
|
||||||
// are before now, so they are kept too). Only the Tasks tab is affected.
|
// the completed tab always show everything).
|
||||||
betterTodoTimeframe = (options.todo_timeframe && Object.prototype.hasOwnProperty.call(BETTER_TODO_TIMEFRAME_DAYS, options.todo_timeframe)) ? options.todo_timeframe : "all";
|
assignmentsDue = applyTodoTimeframe(assignmentsDue);
|
||||||
if (betterTodoTimeframe !== "all") {
|
|
||||||
const cutoff = Date.now() + (BETTER_TODO_TIMEFRAME_DAYS[betterTodoTimeframe] * 24 * 60 * 60 * 1000);
|
|
||||||
assignmentsDue = assignmentsDue.filter(item => new Date(item.plannable_date).getTime() <= cutoff);
|
|
||||||
}
|
|
||||||
// console.log("assignments", assignmentsDue);
|
// console.log("assignments", assignmentsDue);
|
||||||
// console.log("announcements", announcements);
|
// console.log("announcements", announcements);
|
||||||
// console.log("completed", completed);
|
// console.log("completed", completed);
|
||||||
@ -3740,8 +3779,15 @@ async function setupBetterSidebar(mode = getSidebarLayoutMode()) {
|
|||||||
contentMain?.style.setProperty("backdrop-filter", `blur(${Math.max(0, Math.min(30, Number(options.bg_blur ?? 8)))}px)`, "important");
|
contentMain?.style.setProperty("backdrop-filter", `blur(${Math.max(0, Math.min(30, Number(options.bg_blur ?? 8)))}px)`, "important");
|
||||||
contentMain?.style.setProperty("-webkit-backdrop-filter", `blur(${Math.max(0, Math.min(30, Number(options.bg_blur ?? 8)))}px)`, "important");
|
contentMain?.style.setProperty("-webkit-backdrop-filter", `blur(${Math.max(0, Math.min(30, Number(options.bg_blur ?? 8)))}px)`, "important");
|
||||||
}
|
}
|
||||||
const sidebarParent = layoutMode === "course" && leftSide ? leftSide : mainWrapper;
|
// The rail must always render leftmost. Course-layout pages already
|
||||||
if (layoutMode === "course" && leftSide) {
|
// prepend it into #left-side; dash-layout pages that still have a
|
||||||
|
// native left nav (accounts, groups, etc.) must too — otherwise the
|
||||||
|
// native #left-side column (made position:static above) flows before
|
||||||
|
// #not_right_side and shows up to the LEFT of the Better Sidebar,
|
||||||
|
// looking like a competing sidebar once the custom background tints
|
||||||
|
// it. Prepending keeps the order: [Better Sidebar rail][native nav].
|
||||||
|
const sidebarParent = leftSide ? leftSide : mainWrapper;
|
||||||
|
if (leftSide) {
|
||||||
leftSide.style.display = "flex";
|
leftSide.style.display = "flex";
|
||||||
leftSide.style.flexDirection = "row";
|
leftSide.style.flexDirection = "row";
|
||||||
leftSide.style.alignItems = "stretch";
|
leftSide.style.alignItems = "stretch";
|
||||||
@ -3749,9 +3795,6 @@ async function setupBetterSidebar(mode = getSidebarLayoutMode()) {
|
|||||||
leftSide.style.gap = "0";
|
leftSide.style.gap = "0";
|
||||||
}
|
}
|
||||||
document.querySelector(".ic-app-nav-toggle-and-crumbs")?.style.setProperty("display", "none");
|
document.querySelector(".ic-app-nav-toggle-and-crumbs")?.style.setProperty("display", "none");
|
||||||
if (layoutMode !== "course") {
|
|
||||||
document.getElementById("left-side")?.style.removeProperty("display");
|
|
||||||
}
|
|
||||||
if (layoutMode == "dash") {
|
if (layoutMode == "dash") {
|
||||||
document.getElementById("header")?.style.setProperty("display", "none");
|
document.getElementById("header")?.style.setProperty("display", "none");
|
||||||
}
|
}
|
||||||
@ -6333,6 +6376,22 @@ const GA_BUCKETS = [
|
|||||||
{ label: "0-9", min: 0, max: 10, color: "#7f1d1d" },
|
{ label: "0-9", min: 0, max: 10, color: "#7f1d1d" },
|
||||||
];
|
];
|
||||||
const GA_UNGRADED_COLOR = "#6b7280";
|
const GA_UNGRADED_COLOR = "#6b7280";
|
||||||
|
// 5%-wide zone colors for the line chart background: the doughnut's bucket
|
||||||
|
// colors interpolated at 5% steps (dark red at 0 → green at 100), so every
|
||||||
|
// 5% band gets its own shade.
|
||||||
|
const GA_ZONE_COLORS = (() => {
|
||||||
|
const stops = GA_BUCKETS.slice().reverse(); // 0-9 (dark red) → 90+ (green)
|
||||||
|
const rgb = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)];
|
||||||
|
const lerp = (a, b, t) => Math.round(a + (b - a) * t);
|
||||||
|
return Array.from({ length: 20 }, (_, i) => {
|
||||||
|
const m = i * 5 + 2.5; // band midpoint
|
||||||
|
const k = Math.min(stops.length - 1, Math.floor(m / 10));
|
||||||
|
if (k >= stops.length - 1) return stops[stops.length - 1].color;
|
||||||
|
const t = (m - k * 10) / 10;
|
||||||
|
const c1 = rgb(stops[k].color), c2 = rgb(stops[k + 1].color);
|
||||||
|
return `rgb(${lerp(c1[0], c2[0], t)},${lerp(c1[1], c2[1], t)},${lerp(c1[2], c2[2], t)})`;
|
||||||
|
});
|
||||||
|
})();
|
||||||
const GA_OPEN_KEY = "grade_analytics_open";
|
const GA_OPEN_KEY = "grade_analytics_open";
|
||||||
|
|
||||||
async function getGradeAnalyticsOpenState() {
|
async function getGradeAnalyticsOpenState() {
|
||||||
@ -6366,37 +6425,29 @@ function gradeAnalyticsActive() {
|
|||||||
return options.grade_analytics === true && isGradesPage() && !quizSafeModeActive();
|
return options.grade_analytics === true && isGradesPage() && !quizSafeModeActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paginated Canvas API GET using the user's session. Unwraps Firefox Xray
|
// Grades data is read straight from the #grades_summary table the page
|
||||||
// proxies like getData() does. The first page is fetched serially to learn the
|
// already rendered — no API round trips, so even courses with hundreds of
|
||||||
// total page count from its Link header; every remaining page is then fetched
|
// assignments populate instantly, and the numbers always match what the user
|
||||||
// concurrently, so courses with many assignments don't pay one round trip per
|
// sees (grading periods, unposted grades, etc.). Waits briefly for the table
|
||||||
// page.
|
// to appear on SPA navigations.
|
||||||
async function gaFetchAll(path) {
|
function gaWaitForGradesTable(timeoutMs = 15000) {
|
||||||
const fetchPage = async (url) => {
|
return new Promise((resolve, reject) => {
|
||||||
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
const ready = () => {
|
||||||
if (!res.ok) throw new Error("Canvas API " + res.status + " for " + url.pathname);
|
const table = document.querySelector("#grades_summary");
|
||||||
return { data: JSON.parse(JSON.stringify(await res.json())), link: res.headers.get("Link") || "" };
|
return table && table.querySelector("tr.student_assignment") ? table : null;
|
||||||
};
|
};
|
||||||
const firstUrl = new URL(path, domain);
|
const found = ready();
|
||||||
firstUrl.searchParams.set("per_page", "100");
|
if (found) { resolve(found); return; }
|
||||||
const { data: first, link } = await fetchPage(firstUrl);
|
const started = Date.now();
|
||||||
const out = Array.isArray(first) ? first.slice() : [];
|
const timer = setInterval(() => {
|
||||||
const lastMatch = link.match(/<([^>]+)>;\s*rel="last"/);
|
const table = ready();
|
||||||
if (lastMatch) {
|
if (table) { clearInterval(timer); resolve(table); }
|
||||||
const lastPage = parseInt(new URL(lastMatch[1]).searchParams.get("page") || "1", 10);
|
else if (Date.now() - started > timeoutMs) {
|
||||||
if (lastPage > 1) {
|
clearInterval(timer);
|
||||||
const pageUrls = [];
|
reject(new Error("grades table not found on this page"));
|
||||||
for (let p = 2; p <= lastPage; p++) {
|
|
||||||
const u = new URL(path, domain);
|
|
||||||
u.searchParams.set("per_page", "100");
|
|
||||||
u.searchParams.set("page", String(p));
|
|
||||||
pageUrls.push(u);
|
|
||||||
}
|
}
|
||||||
const rest = await Promise.all(pageUrls.map(u => fetchPage(u).then(r => r.data)));
|
}, 250);
|
||||||
for (const page of rest) if (Array.isArray(page)) out.push(...page);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entry point: called at init, on SPA navigation, and when the option changes.
|
// Entry point: called at init, on SPA navigation, and when the option changes.
|
||||||
@ -6561,18 +6612,18 @@ async function loadGradeAnalytics() {
|
|||||||
if (courseId == null) return;
|
if (courseId == null) return;
|
||||||
gaLoading = true;
|
gaLoading = true;
|
||||||
const status = document.getElementById("canvasrefined-ga-status");
|
const status = document.getElementById("canvasrefined-ga-status");
|
||||||
if (status) status.textContent = "Loading grade data…";
|
if (status) status.textContent = "Reading grade data…";
|
||||||
try {
|
try {
|
||||||
const [assignments, groups] = await Promise.all([
|
// Parse the grades table the page already rendered instead of hitting
|
||||||
gaFetchAll(`/api/v1/courses/${courseId}/assignments?include[]=submission&order_by=due_at`),
|
// the paginated API — instant even for courses with hundreds of
|
||||||
gaFetchAll(`/api/v1/courses/${courseId}/assignment_groups`),
|
// assignments, and always in sync with what the page shows.
|
||||||
]);
|
const table = await gaWaitForGradesTable();
|
||||||
gaCourseId = courseId;
|
gaCourseId = courseId;
|
||||||
gaData = computeGradeAnalytics(assignments, groups);
|
gaData = computeGradeAnalyticsFromPage(table);
|
||||||
gaLoading = false;
|
gaLoading = false;
|
||||||
// renderGradeAnalytics self-guards on panel existence and canvas
|
// renderGradeAnalytics self-guards on panel existence and canvas
|
||||||
// size; if the panel isn't ready yet, the retry below in
|
// size; if the panel isn't ready yet, the retry in
|
||||||
// ensureGradeAnalyticsPanel draws once it is.
|
// ensureGradeAnalyticsPanel / syncGradeAnalyticsUI draws once it is.
|
||||||
renderGradeAnalytics();
|
renderGradeAnalytics();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(err);
|
logError(err);
|
||||||
@ -6583,74 +6634,117 @@ async function loadGradeAnalytics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function gaIsGraded(a) {
|
// Parses one assignment row of #grades_summary into a plain record. The row
|
||||||
return a.published !== false && a.points_possible > 0 &&
|
// stashes the original posted score in a hidden "original_score" span (the
|
||||||
(a.submission_types || []).indexOf("not_graded") === -1 &&
|
// "original_points" span holds points EARNED, not possible), while points
|
||||||
a.submission && a.submission.score != null;
|
// possible is only in the "/ 15" span displayed after the grade.
|
||||||
|
function gaParseNum(t) {
|
||||||
|
if (!t) return null;
|
||||||
|
let s = String(t).replace(/\s+/g, "");
|
||||||
|
if (s === "" || !/\d/.test(s)) return null;
|
||||||
|
// Normalize "1,234.5" (thousands grouping) and "9,5" (comma decimal).
|
||||||
|
if (/^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(s)) s = s.replace(/,/g, "");
|
||||||
|
else if (/^-?\d+,\d+$/.test(s)) s = s.replace(",", ".");
|
||||||
|
const v = parseFloat(s);
|
||||||
|
return isFinite(v) ? v : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gaIsUngraded(a) {
|
function gaParseAssignmentRow(tr) {
|
||||||
// Graded-type assignment with points possible but no score yet.
|
const q = (sel) => tr.querySelector(sel);
|
||||||
if (a.published === false || a.points_possible <= 0) return false;
|
const titleLink = q(".title a");
|
||||||
if ((a.submission_types || []).includes("not_graded")) return false;
|
const possibleText = q(".tooltip .grade + span")?.textContent || "";
|
||||||
return !(a.submission && a.submission.score != null);
|
return {
|
||||||
|
title: (titleLink ? titleLink.textContent : (q("th.title")?.textContent || "")).trim(),
|
||||||
|
score: gaParseNum(q(".original_score")?.textContent),
|
||||||
|
points: gaParseNum(possibleText.replace(/^.*\//, "")),
|
||||||
|
// Rows the page lists as unsubmitted/unposted carry no score; only
|
||||||
|
// "graded" rows have one.
|
||||||
|
status: (q(".submission_status")?.textContent || "").trim(),
|
||||||
|
gid: (q(".assignment_group_id")?.textContent || "").trim(),
|
||||||
|
due: (q("td.due")?.textContent || "").replace(/\s+/g, " ").trim(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeGradeAnalytics(assignments, groups) {
|
function computeGradeAnalyticsFromPage(table) {
|
||||||
|
// Assignment group weights come from the "group total" summary rows
|
||||||
|
// (e.g. "Summative Assessment — 86.67%, weight 85").
|
||||||
|
const groupWeight = {};
|
||||||
|
for (const tr of table.querySelectorAll("tr.group_total")) {
|
||||||
|
const gid = tr.querySelector(".assignment_group_id")?.textContent.trim();
|
||||||
|
const w = parseFloat((tr.querySelector(".group_weight")?.textContent || "").trim());
|
||||||
|
if (gid) groupWeight[gid] = isFinite(w) ? w : 0;
|
||||||
|
}
|
||||||
|
const totalWeight = Object.values(groupWeight).reduce((s, w) => s + w, 0);
|
||||||
|
|
||||||
|
// The page's own computed Total (e.g. "91.1%") — use it directly so the
|
||||||
|
// "Overall grade" stat always matches the page.
|
||||||
|
let pageTotal = null;
|
||||||
|
const totalText = table.querySelector("tr.final_grade .grade")?.textContent || "";
|
||||||
|
const totalMatch = totalText.match(/-?\d+(?:\.\d+)?/);
|
||||||
|
if (totalMatch) pageTotal = parseFloat(totalMatch[0]);
|
||||||
|
|
||||||
|
// Rows are already listed in due-date order; skip the summary rows (they
|
||||||
|
// carry the student_assignment class too).
|
||||||
|
const rows = [...table.querySelectorAll("tr.student_assignment")]
|
||||||
|
.filter(tr => !tr.classList.contains("group_total") && !tr.classList.contains("final_grade"))
|
||||||
|
.map(gaParseAssignmentRow);
|
||||||
|
|
||||||
const counts = GA_BUCKETS.map(() => 0);
|
const counts = GA_BUCKETS.map(() => 0);
|
||||||
let ungraded = 0, graded = 0;
|
let ungraded = 0;
|
||||||
for (const a of assignments) {
|
const graded = [];
|
||||||
if (gaIsUngraded(a)) { ungraded++; continue; }
|
for (const a of rows) {
|
||||||
if (!gaIsGraded(a)) continue;
|
if (a.points == null || a.points <= 0) continue; // no points possible
|
||||||
const pct = (a.submission.score / a.points_possible) * 100;
|
if (a.score == null) { ungraded++; continue; } // unposted / unsubmitted
|
||||||
|
graded.push(a);
|
||||||
|
const pct = (a.score / a.points) * 100;
|
||||||
const idx = GA_BUCKETS.findIndex(b => pct >= b.min && pct < b.max);
|
const idx = GA_BUCKETS.findIndex(b => pct >= b.min && pct < b.max);
|
||||||
counts[idx >= 0 ? idx : GA_BUCKETS.length - 1]++;
|
counts[idx >= 0 ? idx : GA_BUCKETS.length - 1]++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupName = {};
|
// Running overall grade, in the page's row order, using Canvas's own
|
||||||
const weight = {};
|
// weighting algorithm (GradeCalculator): sum each group's pct × weight
|
||||||
for (const g of groups) {
|
// over groups that have graded work, then scale up to 100% only when
|
||||||
groupName[g.id] = g.name;
|
// those weights total less than 100 (weights over 100 are used raw and
|
||||||
weight[g.id] = g.group_weight || 0;
|
// can push the grade past 100). Verified to reproduce the Total shown
|
||||||
}
|
// on the page. Point-based courses (no group weights) use points
|
||||||
const totalWeight = Object.values(weight).reduce((s, w) => s + w, 0);
|
// earned / points possible.
|
||||||
|
|
||||||
// Running weighted grade in due-date order, exactly like the user sees it
|
|
||||||
// accumulate over the term.
|
|
||||||
const timeline = assignments
|
|
||||||
.filter(gaIsGraded)
|
|
||||||
.sort((x, y) => new Date(x.due_at || 0) - new Date(y.due_at || 0));
|
|
||||||
const running = {};
|
const running = {};
|
||||||
const points = timeline.map(a => {
|
const pointsGrade = () => {
|
||||||
const r = (running[a.assignment_group_id] ||= { score: 0, pts: 0 });
|
let s = 0, p = 0;
|
||||||
r.score += a.submission.score;
|
for (const g of Object.values(running)) { s += g.score; p += g.pts; }
|
||||||
r.pts += a.points_possible;
|
return p > 0 ? (s / p) * 100 : null;
|
||||||
let weighted = 0, used = 0;
|
};
|
||||||
|
const points = graded.map(a => {
|
||||||
|
const r = (running[a.gid] ||= { score: 0, pts: 0 });
|
||||||
|
r.score += a.score;
|
||||||
|
r.pts += a.points;
|
||||||
|
let grade = null;
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
let weighted = 0, fullWeight = 0;
|
||||||
for (const gid of Object.keys(running)) {
|
for (const gid of Object.keys(running)) {
|
||||||
const g = running[gid];
|
const g = running[gid];
|
||||||
if (g.pts <= 0) continue;
|
if (g.pts <= 0) continue;
|
||||||
if (totalWeight > 0) {
|
const w = groupWeight[gid] || 0;
|
||||||
weighted += (g.score / g.pts) * (weight[gid] || 0);
|
weighted += (g.score / g.pts) * w;
|
||||||
used += (weight[gid] || 0);
|
fullWeight += w;
|
||||||
|
}
|
||||||
|
// Only zero-weighted groups have graded work — fall back to
|
||||||
|
// points so the chart still has a line.
|
||||||
|
grade = fullWeight > 0 ? (fullWeight < 100 ? (weighted / fullWeight) * 100 : weighted) : pointsGrade();
|
||||||
} else {
|
} else {
|
||||||
weighted += g.score;
|
grade = pointsGrade();
|
||||||
used += g.pts;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const grade = used > 0 ? (weighted / used) * 100 : null;
|
|
||||||
const pct = (a.submission.score / a.points_possible) * 100;
|
|
||||||
return {
|
return {
|
||||||
title: a.name,
|
title: a.title,
|
||||||
score: a.submission.score,
|
score: a.score,
|
||||||
points: a.points_possible,
|
points: a.points,
|
||||||
pct,
|
pct: (a.score / a.points) * 100,
|
||||||
grade,
|
grade,
|
||||||
group: groupName[a.assignment_group_id] || "",
|
due: a.due,
|
||||||
due: a.due_at ? new Date(a.due_at).toLocaleDateString() : "",
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const pcts = timeline.map(a => (a.submission.score / a.points_possible) * 100);
|
const pcts = graded.map(a => (a.score / a.points) * 100);
|
||||||
// Trend: change in the running overall grade over the last 5 graded
|
// Trend: change in the running overall grade over the last 5 graded
|
||||||
// assignments (or since the first, if fewer). Positive = climbing.
|
// assignments (or since the first, if fewer). Positive = climbing.
|
||||||
let trend = null;
|
let trend = null;
|
||||||
@ -6662,9 +6756,9 @@ function computeGradeAnalytics(assignments, groups) {
|
|||||||
return {
|
return {
|
||||||
counts,
|
counts,
|
||||||
ungraded,
|
ungraded,
|
||||||
graded: pcts.length,
|
graded: graded.length,
|
||||||
avg: pcts.length ? pcts.reduce((s, p) => s + p, 0) / pcts.length : null,
|
avg: pcts.length ? pcts.reduce((s, p) => s + p, 0) / pcts.length : null,
|
||||||
current: points.length ? points[points.length - 1].grade : null,
|
current: pageTotal != null ? pageTotal : (points.length ? points[points.length - 1].grade : null),
|
||||||
trend,
|
trend,
|
||||||
points,
|
points,
|
||||||
};
|
};
|
||||||
@ -6691,8 +6785,8 @@ function renderGradeAnalytics() {
|
|||||||
stats.innerHTML =
|
stats.innerHTML =
|
||||||
stat("Overall grade", gaData.current == null ? "-" : gaData.current.toFixed(1) + "%") +
|
stat("Overall grade", gaData.current == null ? "-" : gaData.current.toFixed(1) + "%") +
|
||||||
stat("Grade trend (last 5)", trendVal, trendColor) +
|
stat("Grade trend (last 5)", trendVal, trendColor) +
|
||||||
stat("Graded assignments", gaData.graded) +
|
stat("Graded", gaData.graded) +
|
||||||
stat("Ungraded / no score", gaData.ungraded);
|
stat("Ungraded", gaData.ungraded);
|
||||||
|
|
||||||
const charts = panel.querySelector("#canvasrefined-ga-charts");
|
const charts = panel.querySelector("#canvasrefined-ga-charts");
|
||||||
charts.style.display = "flex";
|
charts.style.display = "flex";
|
||||||
@ -6842,6 +6936,22 @@ function gaDrawLine(canvas, tooltip) {
|
|||||||
// can cheaply redraw with a highlight on the active dot.
|
// can cheaply redraw with a highlight on the active dot.
|
||||||
const draw = (hover) => {
|
const draw = (hover) => {
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
// Optional colored 5% zones behind the plot ("Colored grade zones"
|
||||||
|
// popup option), tinted red→green. With Fit Y axis on, bands are
|
||||||
|
// clipped to the visible range.
|
||||||
|
if (options.grade_analytics_zones) {
|
||||||
|
GA_ZONE_COLORS.forEach((color, i) => {
|
||||||
|
const top = Math.min(yMax, (i + 1) * 5);
|
||||||
|
const bottom = Math.max(yMin, i * 5);
|
||||||
|
if (top <= bottom) return;
|
||||||
|
const y1 = pad.t + (1 - (top - yMin) / (yMax - yMin)) * (h - pad.t - pad.b);
|
||||||
|
const y2 = pad.t + (1 - (bottom - yMin) / (yMax - yMin)) * (h - pad.t - pad.b);
|
||||||
|
ctx.globalAlpha = 0.3;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.fillRect(pad.l, y1, w - pad.l - pad.r, y2 - y1);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
// Y grid: 5 evenly spaced lines across the current range.
|
// Y grid: 5 evenly spaced lines across the current range.
|
||||||
ctx.font = "11px Lato, sans-serif";
|
ctx.font = "11px Lato, sans-serif";
|
||||||
ctx.textAlign = "right"; ctx.textBaseline = "middle";
|
ctx.textAlign = "right"; ctx.textBaseline = "middle";
|
||||||
@ -6866,14 +6976,62 @@ function gaDrawLine(canvas, tooltip) {
|
|||||||
ctx.fillStyle = "#2563eb"; ctx.fill();
|
ctx.fillStyle = "#2563eb"; ctx.fill();
|
||||||
ctx.strokeStyle = "#fff"; ctx.lineWidth = 1; ctx.stroke();
|
ctx.strokeStyle = "#fff"; ctx.lineWidth = 1; ctx.stroke();
|
||||||
});
|
});
|
||||||
// X labels: first, middle, last due dates.
|
// X axis: tick marks plus one label per calendar month. Points are
|
||||||
|
// already in chronological order, so a walking month counter (wrapping
|
||||||
|
// across Dec -> Jan) maps each point to an absolute month; the first
|
||||||
|
// point of each month gets the tick + label.
|
||||||
ctx.textAlign = "center"; ctx.textBaseline = "top";
|
ctx.textAlign = "center"; ctx.textBaseline = "top";
|
||||||
[[0], [Math.floor((pts.length - 1) / 2)], [pts.length - 1]].forEach(([i]) => {
|
ctx.strokeStyle = "rgba(128,128,128,0.55)";
|
||||||
if (pts.length > 1 && pts.length > 2 && i !== 0 && i !== pts.length - 1 && Math.abs(X(i) - X(0)) < 40) return;
|
ctx.lineWidth = 1;
|
||||||
const label = pts[i].due || "";
|
const tick = (x) => {
|
||||||
if (!label) return;
|
ctx.beginPath(); ctx.moveTo(x, h - pad.b); ctx.lineTo(x, h - pad.b + 4); ctx.stroke();
|
||||||
|
};
|
||||||
|
// Subtle tick under every data point on sparse charts.
|
||||||
|
if (pts.length <= 25) pts.forEach((_, i) => tick(X(i)));
|
||||||
|
// Month boundaries: the first point of each calendar month. Points are
|
||||||
|
// already in chronological order, so a walking month counter (wrapping
|
||||||
|
// across Dec → Jan) maps each point to an absolute month.
|
||||||
|
const boundaries = [];
|
||||||
|
let prevM = null, absM = null;
|
||||||
|
pts.forEach((p, i) => {
|
||||||
|
const m = months.indexOf((p.due || "").trim().slice(0, 3));
|
||||||
|
if (m < 0) return; // no due date on this point
|
||||||
|
if (absM == null) absM = m;
|
||||||
|
else if (m >= prevM) absM += m - prevM;
|
||||||
|
else absM += 12 - prevM + m; // wrapped to a new year
|
||||||
|
prevM = m;
|
||||||
|
const last = boundaries[boundaries.length - 1];
|
||||||
|
if (!last || last.absM !== absM) {
|
||||||
|
boundaries.push({ i, absM, label: (p.due || "").trim().slice(0, 3) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
boundaries.forEach(b => tick(X(b.i)));
|
||||||
|
// Collision-aware labels: greedily keep a month label only if it fits
|
||||||
|
// after the previous kept one (labels are centered, so compare against
|
||||||
|
// half-widths plus a 6px gap). The first boundary always gets a label;
|
||||||
|
// so does the last — if it doesn't fit, earlier labels are dropped to
|
||||||
|
// make room, so the axis ends on a real month instead of mid-run.
|
||||||
|
const GAP = 6;
|
||||||
|
const kept = [];
|
||||||
|
boundaries.forEach((b, idx) => {
|
||||||
|
const x = X(b.i);
|
||||||
|
const w = ctx.measureText(b.label).width;
|
||||||
|
if (idx === 0 || x - w / 2 > kept[kept.length - 1].right + GAP) {
|
||||||
|
kept.push({ ...b, x, right: x + w / 2 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lastB = boundaries[boundaries.length - 1];
|
||||||
|
if (lastB && kept[kept.length - 1].i !== lastB.i) {
|
||||||
|
const x = X(lastB.i);
|
||||||
|
const w = ctx.measureText(lastB.label).width;
|
||||||
|
while (kept.length && kept[kept.length - 1].right + GAP > x - w / 2) kept.pop();
|
||||||
|
kept.push({ ...lastB, x, right: x + w / 2 });
|
||||||
|
}
|
||||||
|
kept.forEach((b, k) => {
|
||||||
|
// Anchor the edge labels inward so they don't clip.
|
||||||
|
ctx.textAlign = k === 0 ? "left" : (b.i === pts.length - 1 ? "right" : "center");
|
||||||
ctx.fillStyle = text;
|
ctx.fillStyle = text;
|
||||||
ctx.fillText(label, X(i), h - pad.b + 6);
|
ctx.fillText(b.label, b.x, h - pad.b + 6);
|
||||||
});
|
});
|
||||||
// Hover indicator: dashed vertical guide plus a halo ring around the
|
// Hover indicator: dashed vertical guide plus a halo ring around the
|
||||||
// hovered dot so it's obvious which point the tooltip describes.
|
// hovered dot so it's obvious which point the tooltip describes.
|
||||||
|
|||||||
18
js/popup.js
18
js/popup.js
@ -1,5 +1,6 @@
|
|||||||
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'];
|
||||||
const syncedSubOptions = [
|
const syncedSubOptions = [
|
||||||
|
"grade_analytics_zones",
|
||||||
"todo_hide_feedback",
|
"todo_hide_feedback",
|
||||||
"todo_full_height",
|
"todo_full_height",
|
||||||
"todo_confetti",
|
"todo_confetti",
|
||||||
@ -59,8 +60,9 @@ const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "to
|
|||||||
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted"];
|
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted"];
|
||||||
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "card_transparency", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur", "card_opacity", "card_blur"];
|
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "card_transparency", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur", "card_opacity", "card_blur"];
|
||||||
// Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,
|
// Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,
|
||||||
// no personal productivity features).
|
// no personal productivity features). Includes the former separate Layout and
|
||||||
const exportToggles = ["dark_mode", "quiz_safe_mode"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
|
// Sidebar groups, plus Grade Analytics and its colored-zones sub-toggle.
|
||||||
|
const exportToggles = ["dark_mode", "quiz_safe_mode", "grade_analytics", "grade_analytics_zones"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
|
||||||
const fontsDropdownStateKey = "fonts_dropdown_open";
|
const fontsDropdownStateKey = "fonts_dropdown_open";
|
||||||
|
|
||||||
const apiurl = "none";
|
const apiurl = "none";
|
||||||
@ -151,6 +153,7 @@ const defaultOptions = {
|
|||||||
"equal_height_cards": false,
|
"equal_height_cards": false,
|
||||||
"hide_new_canvas": true,
|
"hide_new_canvas": true,
|
||||||
"hide_sequence_footer": false,
|
"hide_sequence_footer": false,
|
||||||
|
"grade_analytics_zones": false,
|
||||||
"quiz_safe_mode": false,
|
"quiz_safe_mode": false,
|
||||||
"dark_mode_fix": [],
|
"dark_mode_fix": [],
|
||||||
"assignment_states": {},
|
"assignment_states": {},
|
||||||
@ -570,7 +573,7 @@ function toggleAlternateColorsVisibility(darkModeOn) {
|
|||||||
|
|
||||||
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
|
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
|
||||||
function toggleSubOptionsVisibility(option, isOn) {
|
function toggleSubOptionsVisibility(option, isOn) {
|
||||||
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark"];
|
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics"];
|
||||||
if (!togglesWithSubOptions.includes(option)) return;
|
if (!togglesWithSubOptions.includes(option)) return;
|
||||||
const optionEl = document.getElementById(option);
|
const optionEl = document.getElementById(option);
|
||||||
if (!optionEl) return;
|
if (!optionEl) return;
|
||||||
@ -953,6 +956,7 @@ function setup() {
|
|||||||
"fitImageToScreen",
|
"fitImageToScreen",
|
||||||
"card_transparency",
|
"card_transparency",
|
||||||
"customCardStyles",
|
"customCardStyles",
|
||||||
|
"grade_analytics_zones",
|
||||||
],
|
],
|
||||||
tabs: {
|
tabs: {
|
||||||
"advanced-settings": {
|
"advanced-settings": {
|
||||||
@ -1110,7 +1114,7 @@ function setup() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
toggleBetterSidebarSubOptions(sync["better_sidebar"] === true);
|
toggleBetterSidebarSubOptions(sync["better_sidebar"] === true);
|
||||||
["gpa_calc", "assignments_due", "better_todo", "auto_dark"].forEach(opt => {
|
["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics"].forEach(opt => {
|
||||||
toggleSubOptionsVisibility(opt, sync[opt] === true);
|
toggleSubOptionsVisibility(opt, sync[opt] === true);
|
||||||
});
|
});
|
||||||
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
||||||
@ -1312,12 +1316,6 @@ function setup() {
|
|||||||
case "export-background":
|
case "export-background":
|
||||||
final = { ...final, ...(await getExport(storage, exportBackground)) };
|
final = { ...final, ...(await getExport(storage, exportBackground)) };
|
||||||
break;
|
break;
|
||||||
case "export-layout":
|
|
||||||
final = { ...final, ...(await getExport(storage, exportLayout)) };
|
|
||||||
break;
|
|
||||||
case "export-sidebar":
|
|
||||||
final = { ...final, ...(await getExport(storage, exportSidebar)) };
|
|
||||||
break;
|
|
||||||
case "export-todo":
|
case "export-todo":
|
||||||
final = { ...final, ...(await getExport(storage, exportTodo)) };
|
final = { ...final, ...(await getExport(storage, exportTodo)) };
|
||||||
break;
|
break;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user