mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 22:07:18 +01:00
credits+bump+grid fix+@grades+grade history fix
This commit is contained in:
parent
b8bcf821e0
commit
f30e197339
@ -217,6 +217,9 @@ If nobody is working on the feature, you can clone the dev branch and make your
|
|||||||
|
|
||||||
- [Guy](https://github.com/guysandler)
|
- [Guy](https://github.com/guysandler)
|
||||||
|
|
||||||
|
#### Fork Contributers
|
||||||
|
- [TabulateJarl8](https://github.com/TabulateJarl8)
|
||||||
|
|
||||||
#### Original Owner
|
#### Original Owner
|
||||||
|
|
||||||
- [ksucpea](https://github.com/ksucpea)
|
- [ksucpea](https://github.com/ksucpea)
|
||||||
|
|||||||
@ -370,6 +370,7 @@
|
|||||||
.canvasrefined-gs-type-file { background: #f1f3f5; color: #444; border-color: #dde1e3; }
|
.canvasrefined-gs-type-file { background: #f1f3f5; color: #444; border-color: #dde1e3; }
|
||||||
.canvasrefined-gs-type-link, .canvasrefined-gs-type-external-tool { background: #fff4d6; color: #7a5b00; border-color: #f1dfab; }
|
.canvasrefined-gs-type-link, .canvasrefined-gs-type-external-tool { background: #fff4d6; color: #7a5b00; border-color: #f1dfab; }
|
||||||
.canvasrefined-gs-type-course { background: #dff3f3; color: #0b6e6e; border-color: #b3dede; }
|
.canvasrefined-gs-type-course { background: #dff3f3; color: #0b6e6e; border-color: #b3dede; }
|
||||||
|
.canvasrefined-gs-type-grades { background: #e8eefc; color: #1d4ed8; border-color: #c3d2f5; }
|
||||||
.canvasrefined-gs-footer {
|
.canvasrefined-gs-footer {
|
||||||
display: flex; flex-wrap: wrap; gap: 12px; padding: 8px 14px; font-size: 12px;
|
display: flex; flex-wrap: wrap; gap: 12px; padding: 8px 14px; font-size: 12px;
|
||||||
color: var(--bctext-2, #9aa3ab); border-top: 1px solid var(--bcborders, #e2e6e8);
|
color: var(--bctext-2, #9aa3ab); border-top: 1px solid var(--bcborders, #e2e6e8);
|
||||||
|
|||||||
147
js/content.js
147
js/content.js
@ -16,6 +16,9 @@ function setupNavigationListener() {
|
|||||||
watchSubmissionPageButton();
|
watchSubmissionPageButton();
|
||||||
watchProfileLogoutPageButton();
|
watchProfileLogoutPageButton();
|
||||||
watchGradeAnalytics();
|
watchGradeAnalytics();
|
||||||
|
// Show/hide the Better Todo sidebar for the new page (course home vs
|
||||||
|
// sub-page) without needing a full reload.
|
||||||
|
updateTodoSidebarVisibility();
|
||||||
};
|
};
|
||||||
for (const method of ["pushState", "replaceState"]) {
|
for (const method of ["pushState", "replaceState"]) {
|
||||||
const orig = history[method];
|
const orig = history[method];
|
||||||
@ -65,6 +68,22 @@ function isProfilePage() {
|
|||||||
return /^\/profile(?:\/|$)/.test(current_page);
|
return /^\/profile(?:\/|$)/.test(current_page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Better Todo sidebar belongs only on the main dashboard and each
|
||||||
|
// course's homepage (/courses/<id>, optional trailing slash) — not on
|
||||||
|
// sub-pages like assignments, modules, quizzes or the syllabus.
|
||||||
|
function isTodoAllowedPage() {
|
||||||
|
if (current_page === "/" || current_page === "") return true;
|
||||||
|
return /^\/courses\/\d+\/?$/.test(current_page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide an already-mounted Better Todo sidebar when the user navigates
|
||||||
|
// client-side into or out of an allowed page (Canvas' New UI doesn't reload).
|
||||||
|
function updateTodoSidebarVisibility() {
|
||||||
|
const list = document.getElementById("canvasrefined-todo-list");
|
||||||
|
if (!list) return;
|
||||||
|
list.style.display = isTodoAllowedPage() ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
// Quiz pages: /courses/123/quizzes/456 (pre-take/intro) and
|
// Quiz pages: /courses/123/quizzes/456 (pre-take/intro) and
|
||||||
// /courses/123/quizzes/456/take (the actual quiz).
|
// /courses/123/quizzes/456/take (the actual quiz).
|
||||||
function isQuizPage() {
|
function isQuizPage() {
|
||||||
@ -4659,6 +4678,9 @@ function createTodoViewMore(location, type) {
|
|||||||
function setupBetterTodo() {
|
function setupBetterTodo() {
|
||||||
// Better Todo list is removed from quizzes (it interferes with the quiz page).
|
// Better Todo list is removed from quizzes (it interferes with the quiz page).
|
||||||
if (isQuizPage()) return;
|
if (isQuizPage()) return;
|
||||||
|
// Only the main dashboard and course homepages get the todo sidebar;
|
||||||
|
// sub-pages (assignments, modules, files, ...) keep Canvas's own sidebar.
|
||||||
|
if (!isTodoAllowedPage()) return;
|
||||||
if (options.better_todo !== true || isGradesPage()) return;
|
if (options.better_todo !== true || isGradesPage()) return;
|
||||||
if (document.querySelector('#canvasrefined-todo-list')) return;
|
if (document.querySelector('#canvasrefined-todo-list')) return;
|
||||||
// The dashboard MutationObserver can fire before getApiData() has assigned
|
// The dashboard MutationObserver can fire before getApiData() has assigned
|
||||||
@ -7094,6 +7116,14 @@ function applyAestheticChanges() {
|
|||||||
const gridRows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
const gridRows = Math.max(1, parseInt(options.card_grid_rows, 10) || 3);
|
||||||
const gridColGap = Math.max(0, !isNaN(parseInt(options.card_grid_column_gap, 10)) ? parseInt(options.card_grid_column_gap, 10) : 12);
|
const gridColGap = Math.max(0, !isNaN(parseInt(options.card_grid_column_gap, 10)) ? parseInt(options.card_grid_column_gap, 10) : 12);
|
||||||
const gridRowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
|
const gridRowGap = Math.max(0, !isNaN(parseInt(options.card_grid_row_gap, 10)) ? parseInt(options.card_grid_row_gap, 10) : 12);
|
||||||
|
// Canvas gives dashboard cards their own natural top/left margins
|
||||||
|
// for the default flow layout. The grid positions everything with
|
||||||
|
// its own row/column gaps, so strip those natural margins top and
|
||||||
|
// left — otherwise every card sits visibly lower and further right
|
||||||
|
// than the grid math says it should. (Card Spacing's right/bottom
|
||||||
|
// margins are untouched.)
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container{margin-top:0!important;margin-left:0!important}`;
|
||||||
|
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-top:0!important;margin-left:0!important}`;
|
||||||
const centerGrid = options.center_cards === true;
|
const centerGrid = options.center_cards === true;
|
||||||
const gpaOn = options.gpa_calc === true;
|
const gpaOn = options.gpa_calc === true;
|
||||||
const gpaTop = options.gpa_calc_prepend === true;
|
const gpaTop = options.gpa_calc_prepend === true;
|
||||||
@ -7623,7 +7653,7 @@ function openGlobalSearchModal() {
|
|||||||
// Kick off indexing immediately so the first keystroke is fast.
|
// Kick off indexing immediately so the first keystroke is fast.
|
||||||
ensureGlobalSearchIndex();
|
ensureGlobalSearchIndex();
|
||||||
// Render an initial hint.
|
// Render an initial hint.
|
||||||
resultsEl.innerHTML = `<div class="canvasrefined-gs-hint">Start typing to search your modules and assignments. Use @coursename to search a specific course.</div>`;
|
resultsEl.innerHTML = `<div class="canvasrefined-gs-hint">Start typing to search your modules and assignments. Use @coursename to search a specific course, or @grades to open a course's grades page.</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeGlobalSearchModal() {
|
function closeGlobalSearchModal() {
|
||||||
@ -7961,7 +7991,7 @@ async function runGlobalSearch(query, resultsEl) {
|
|||||||
}
|
}
|
||||||
const index = await ensureGlobalSearchIndex();
|
const index = await ensureGlobalSearchIndex();
|
||||||
if (!query) {
|
if (!query) {
|
||||||
resultsEl.innerHTML = `<div class="canvasrefined-gs-hint">Start typing to search your modules and assignments. Use @coursename to search a specific course.</div>`;
|
resultsEl.innerHTML = `<div class="canvasrefined-gs-hint">Start typing to search your modules and assignments. Use @coursename to search a specific course, or @grades to open a course's grades page.</div>`;
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (!index || !index.length) {
|
if (!index || !index.length) {
|
||||||
@ -7969,6 +7999,32 @@ async function runGlobalSearch(query, resultsEl) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "@grades [course filter]" lists course grade pages instead of content.
|
||||||
|
// A bare "@grades" lists every course; "@grades calc" narrows to courses
|
||||||
|
// matching "calc" (same matching as the @course filter). Checked before
|
||||||
|
// the generic @course parse so "@grades" never gets eaten as a course
|
||||||
|
// name (e.g. a course actually called "Grades").
|
||||||
|
const gradesMatch = query.match(/(?:^|\s)@grades(?:\s+(.+))?$/i);
|
||||||
|
if (gradesMatch) {
|
||||||
|
const courseEntries = index.filter(i => i.type === "Course");
|
||||||
|
const filter = (gradesMatch[1] || "").trim();
|
||||||
|
const matching = courseEntries.filter(c => globalSearchCourseMatches(c, filter));
|
||||||
|
if (!matching.length) {
|
||||||
|
resultsEl.innerHTML = `<div class="canvasrefined-gs-hint">${filter ? `No course matches \u201c${escapeGlobalSearchHtml(filter)}\u201d.` : "No courses found to open grades for."}</div>`;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const gradeRows = matching.map(c => ({
|
||||||
|
type: "Grades",
|
||||||
|
title: c.title,
|
||||||
|
course: c.course,
|
||||||
|
courseId: c.courseId,
|
||||||
|
url: `${domain}/courses/${c.courseId}/grades`,
|
||||||
|
}));
|
||||||
|
resultsEl.innerHTML = gradeRows.map((item, i) => globalSearchRowHtml(item, i)).join("");
|
||||||
|
bindGlobalSearchRows(resultsEl, gradeRows);
|
||||||
|
return gradeRows;
|
||||||
|
}
|
||||||
|
|
||||||
// "@course name" narrows the search to one course. With no other words
|
// "@course name" narrows the search to one course. With no other words
|
||||||
// after the filter (or a bare "@"), list the matching courses' homepages.
|
// after the filter (or a bare "@"), list the matching courses' homepages.
|
||||||
const courseEntries = index.filter(i => i.type === "Course");
|
const courseEntries = index.filter(i => i.type === "Course");
|
||||||
@ -8958,6 +9014,29 @@ async function loadGradeAnalytics() {
|
|||||||
// stashes the original posted score in a hidden "original_score" span (the
|
// stashes the original posted score in a hidden "original_score" span (the
|
||||||
// "original_points" span holds points EARNED, not possible), while points
|
// "original_points" span holds points EARNED, not possible), while points
|
||||||
// possible is only in the "/ 15" span displayed after the grade.
|
// possible is only in the "/ 15" span displayed after the grade.
|
||||||
|
// Month abbreviations for parsing the grades table's due-date strings.
|
||||||
|
const GA_MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
||||||
|
|
||||||
|
// Parse a due-date string from the grades table into sortable parts.
|
||||||
|
// Handles "Sep 12", "Sep 12 at 11:59pm", "Sep 12 by 23:59" and
|
||||||
|
// "Sep 12, 2024 at 11:59pm". Returns { mon, day, minutes } or null.
|
||||||
|
function gaParseDueDate(due) {
|
||||||
|
const m = String(due || "").toLowerCase().match(/([a-z]{3})[a-z]*\.?\s+(\d{1,2})(?:[^\d]+\d{4})?(?:[^\d]{0,8}?(\d{1,2}):(\d{2})\s*(am|pm)?)?/);
|
||||||
|
if (!m) return null;
|
||||||
|
const mon = GA_MONTH_NAMES.indexOf(m[1]);
|
||||||
|
if (mon < 0) return null;
|
||||||
|
const day = parseInt(m[2], 10);
|
||||||
|
if (!isFinite(day) || day < 1 || day > 31) return null;
|
||||||
|
let minutes = 0;
|
||||||
|
if (m[3] !== undefined) {
|
||||||
|
let h = parseInt(m[3], 10);
|
||||||
|
minutes = h * 60 + parseInt(m[4], 10);
|
||||||
|
if (m[5] === "pm" && h < 12) minutes += 720;
|
||||||
|
if (m[5] === "am" && h === 12) minutes = parseInt(m[4], 10);
|
||||||
|
}
|
||||||
|
return { mon, day, minutes };
|
||||||
|
}
|
||||||
|
|
||||||
function gaParseNum(t) {
|
function gaParseNum(t) {
|
||||||
if (!t) return null;
|
if (!t) return null;
|
||||||
let s = String(t).replace(/\s+/g, "");
|
let s = String(t).replace(/\s+/g, "");
|
||||||
@ -8982,6 +9061,9 @@ function gaParseAssignmentRow(tr) {
|
|||||||
status: (q(".submission_status")?.textContent || "").trim(),
|
status: (q(".submission_status")?.textContent || "").trim(),
|
||||||
gid: (q(".assignment_group_id")?.textContent || "").trim(),
|
gid: (q(".assignment_group_id")?.textContent || "").trim(),
|
||||||
due: (q("td.due")?.textContent || "").replace(/\s+/g, " ").trim(),
|
due: (q("td.due")?.textContent || "").replace(/\s+/g, " ").trim(),
|
||||||
|
// Full hover tooltip date when present — a better parse source than
|
||||||
|
// the abbreviated cell text.
|
||||||
|
dueTitle: q("td.due [title]")?.getAttribute("title") || q("td.due")?.getAttribute("title") || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9021,6 +9103,67 @@ function computeGradeAnalyticsFromPage(table) {
|
|||||||
counts[idx >= 0 ? idx : GA_BUCKETS.length - 1]++;
|
counts[idx >= 0 ? idx : GA_BUCKETS.length - 1]++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort graded work chronologically by due date before building the
|
||||||
|
// running-grade timeline. The chart and trend assume row order is
|
||||||
|
// chronological, but the grades table isn't guaranteed to be (it follows
|
||||||
|
// the course's assignment ordering, and users can re-sort it), which left
|
||||||
|
// the grade-history line jumping back and forth in time.
|
||||||
|
//
|
||||||
|
// Due strings have no year, so months are cyclic: a school-year course
|
||||||
|
// (Sep → May) and a calendar-year course (Jan → Dec) both count upward,
|
||||||
|
// just from different "year starts". Each candidate rotation r (month
|
||||||
|
// the year window starts at) maps a due month to (mon - r) mod 12; the
|
||||||
|
// rotation whose keys produce the least total backward drift in the
|
||||||
|
// table's row order wins, with ties going to the first dated row's
|
||||||
|
// month (which reproduces the chart's walking-month behavior on
|
||||||
|
// already-ordered data). This stays correct whether rows come in
|
||||||
|
// due-date order, a different sort order, or shuffled. Day-of-month and
|
||||||
|
// time break ties within a month; undated rows inherit their nearest
|
||||||
|
// dated neighbour's key (forward, then backward fill); equal keys keep
|
||||||
|
// their original relative order (stable sort).
|
||||||
|
const parsed = graded.map(a => gaParseDueDate(a.dueTitle || a.due));
|
||||||
|
// Cost of a rotation = total backward drift it forces on the row order
|
||||||
|
// (sum of positive key drops between consecutive dated rows). Magnitude
|
||||||
|
// beats counting steps: a shuffle costs real months, a year-wrap costs
|
||||||
|
// ~11/12 of a cycle only if it's the wrong rotation.
|
||||||
|
const rotationCost = (r) => {
|
||||||
|
let cost = 0, prev = null;
|
||||||
|
for (const d of parsed) {
|
||||||
|
if (!d) continue;
|
||||||
|
const k = (d.mon - r + 12) % 12;
|
||||||
|
if (prev != null && k < prev) cost += prev - k;
|
||||||
|
prev = k;
|
||||||
|
}
|
||||||
|
return cost;
|
||||||
|
};
|
||||||
|
let bestRotation = 0, bestCost = Infinity;
|
||||||
|
for (let r = 0; r < 12; r++) {
|
||||||
|
const cost = rotationCost(r);
|
||||||
|
if (cost < bestCost) { bestCost = cost; bestRotation = r; }
|
||||||
|
}
|
||||||
|
const firstDated = parsed.find(d => d);
|
||||||
|
if (firstDated) {
|
||||||
|
const pref = firstDated.mon;
|
||||||
|
// Re-scan for the preferred rotation on a cost tie.
|
||||||
|
for (let r = 0; r < 12; r++) {
|
||||||
|
if (rotationCost(r) === bestCost && r === pref) { bestRotation = r; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const keys = parsed.map(d => d ? [(d.mon - bestRotation + 12) % 12, d.day, d.minutes] : null);
|
||||||
|
let lastKey = null;
|
||||||
|
for (let i = 0; i < keys.length; i++) { if (keys[i] == null) keys[i] = lastKey; else lastKey = keys[i]; }
|
||||||
|
let nextKey = null;
|
||||||
|
for (let i = keys.length - 1; i >= 0; i--) { if (keys[i] == null) keys[i] = nextKey; else nextKey = keys[i]; }
|
||||||
|
const order = graded.map((_, i) => i).sort((x, y) => {
|
||||||
|
const kx = keys[x], ky = keys[y];
|
||||||
|
if (!kx || !ky) return x - y;
|
||||||
|
for (let j = 0; j < 3; j++) if (kx[j] !== ky[j]) return kx[j] - ky[j];
|
||||||
|
return x - y;
|
||||||
|
});
|
||||||
|
const sortedGraded = order.map(i => graded[i]);
|
||||||
|
graded.length = 0;
|
||||||
|
graded.push(...sortedGraded);
|
||||||
|
|
||||||
// Running overall grade, in the page's row order, using Canvas's own
|
// Running overall grade, in the page's row order, using Canvas's own
|
||||||
// weighting algorithm (GradeCalculator): sum each group's pct × weight
|
// weighting algorithm (GradeCalculator): sum each group's pct × weight
|
||||||
// over groups that have graded work, then scale up to 100% only when
|
// over groups that have graded work, then scale up to 100% only when
|
||||||
|
|||||||
@ -1329,6 +1329,15 @@ function setup() {
|
|||||||
if (option === "dark_mode") {
|
if (option === "dark_mode") {
|
||||||
toggleAlternateColorsVisibility(status);
|
toggleAlternateColorsVisibility(status);
|
||||||
}
|
}
|
||||||
|
if (option === "hide_personal_details" && status === false) {
|
||||||
|
// "Temporarily remove all course images" is a sub-option of
|
||||||
|
// the privacy mode: switching the mode off restores the
|
||||||
|
// images too (the content script listens for this key and
|
||||||
|
// removes its hiding stylesheet).
|
||||||
|
const imgCheckbox = document.getElementById("hide_course_images");
|
||||||
|
if (imgCheckbox) imgCheckbox.checked = false;
|
||||||
|
chrome.storage.sync.set({ "hide_course_images": false });
|
||||||
|
}
|
||||||
toggleSubOptionsVisibility(option, status);
|
toggleSubOptionsVisibility(option, status);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Canvas Refined",
|
"name": "Canvas Refined",
|
||||||
"description": "Even More Feature packed extension for Canvas.",
|
"description": "Even More Feature packed extension for Canvas.",
|
||||||
"version": "7.0.5",
|
"version": "7.1.0",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icon/icon-16.png",
|
"16": "icon/icon-16.png",
|
||||||
"32": "icon/icon-32.png",
|
"32": "icon/icon-32.png",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user