diff --git a/html/popup.html b/html/popup.html index 05293cd..aeba99a 100644 --- a/html/popup.html +++ b/html/popup.html @@ -327,6 +327,10 @@ +
+ + +
Max items to show: diff --git a/js/background.js b/js/background.js index 816fe2b..d144bba 100644 --- a/js/background.js +++ b/js/background.js @@ -86,6 +86,7 @@ chrome.runtime.onInstalled.addListener(function () { "todo_full_height": true, "todo_progress_rings": "rings", "todo_confetti": true, + "todo_show_scores": false, "device_dark": false, "cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 }, // "show_updates": false, diff --git a/js/content.js b/js/content.js index f40ab20..f8078e1 100644 --- a/js/content.js +++ b/js/content.js @@ -955,6 +955,7 @@ function applyOptionsChanges(changes) { case "todo_full_height": case "todo_ignore_card_colors": case "todo_remove_icons": + case "todo_show_scores": case "custom_cards_3": if (options.better_todo && document.getElementById("better-todo-main")) { moreAnnouncementCount = 0; @@ -3754,6 +3755,110 @@ function attachTodoHoverPreview(anchor, item) { const TODO_QUIZ_ICON_SVG = ''; const TODO_DISCUSSION_ICON_SVG = ''; +// --- Assignment scores on the Better Todo List ("Show assignment scores") --- +// Canvas's planner items API omits the actual submission score (its +// `submissions` object only carries booleans like graded/late), so points +// possible come straight from the plannable and the earned score is lazily +// fetched per course (students/submissions?student_ids[]=self) and cached for +// the session. Badges always render as "{score}/{points} pts" — an en dash +// stands in for the earned score until the fetch lands (or while the item is +// ungraded/unsubmitted), and "Excused" replaces the whole badge for excused +// submissions. +const todoScoreCache = new Map(); // `${courseId}:${plannableId}` -> {score, excused} +let todoScoreLoadToken = 0; + +function formatTodoScoreNumber(n) { + return String(Math.round(n * 100) / 100); +} + +// Tiny green check shown right after the score on graded assignments. Kept +// as a constant string so both the initial render and the async patch can +// insert the exact same markup. +const TODO_GRADED_BADGE_HTML = ``; + +// Small inline badge shown next to the due date. "" when the item has no +// meaningful points (custom tasks, wiki pages, etc.). +function todoScoreBadgeHtml(item) { + if (item.plannable_type == "planner_note" || item.planner_override?.custom === true) return ""; + const points = item.plannable?.points_possible; + if (points == null) return ""; + const pts = formatTodoScoreNumber(points); + const key = `${item.course_id}:${item.plannable_id}`; + const cached = todoScoreCache.get(key); + // Graded state: the live fetched score is the strongest signal, otherwise + // fall back to the planner API's boolean — but only while the score fetch + // hasn't run yet (a cached null score means the fetch confirmed no grade). + const graded = cached ? cached.score != null : item.submissions?.graded === true; + let text; + if (cached?.excused) text = "Excused"; + else text = `${cached?.score != null ? formatTodoScoreNumber(cached.score) : "–"}/${pts} pts`; + const attrs = `data-todo-course="${item.course_id ?? ""}" data-todo-plannable="${item.plannable_id}" data-todo-points="${pts}"`; + return `${text}${graded ? TODO_GRADED_BADGE_HTML : ""}`; +} + +// Fetches the current user's submission scores for every course with visible +// todo items still missing a score, then patches the badges in place (a full +// re-render would reset the "View More" expansion state). The token guards +// against overlapping loads from rapid option/tab switches: an outdated run +// stops touching the DOM, and any newer run re-collects the still-missing +// items anyway. +async function loadTodoScores(items) { + if (options.todo_show_scores !== true) return; + const byCourse = new Map(); + for (const item of items) { + if (item.plannable_type == "planner_note" || item.planner_override?.custom === true) continue; + if (item.plannable?.points_possible == null) continue; + const cid = item.course_id ?? item.context_id; + if (cid == null) continue; + const key = `${cid}:${item.plannable_id}`; + if (todoScoreCache.has(key)) continue; + if (item.submissions?.excused === true) { todoScoreCache.set(key, { score: null, excused: true }); continue; } + let list = byCourse.get(String(cid)); + if (!list) { list = []; byCourse.set(String(cid), list); } + list.push(item); + } + if (byCourse.size === 0) return; + const token = ++todoScoreLoadToken; + for (const [cid] of byCourse) { + try { + let url = `${domain}/api/v1/courses/${cid}/students/submissions?student_ids[]=self&per_page=100`; + // 10 pages * 100 submissions is a safety net against a malformed + // next link, same as fetchActiveCourseIds. + for (let page = 0; page < 10 && url; page++) { + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json", "Accept": "application/json" } + }); + if (!response.ok) throw new Error(`Canvas API request failed (${response.status})`); + const data = await response.json(); + if (token !== todoScoreLoadToken) return; + if (Array.isArray(data)) { + for (const s of data) { + if (s && s.assignment_id != null) { + todoScoreCache.set(`${cid}:${s.assignment_id}`, { score: s.score, excused: s.excused === true }); + } + } + } + url = getNextPageUrl(response.headers.get("Link")); + } + } catch (e) { + // Leave these items uncached so a later render retries the fetch + // (e.g. teacher accounts, which the submissions endpoint rejects). + } + } + if (token !== todoScoreLoadToken) return; + // Patch every badge that now has data, on either tab. + document.querySelectorAll("#better-todo-main .better-todo-score").forEach(el => { + const cached = todoScoreCache.get(`${el.dataset.todoCourse}:${el.dataset.todoPlannable}`); + if (!cached) return; + if (cached.excused) el.textContent = "Excused"; + else el.textContent = `${cached.score != null ? formatTodoScoreNumber(cached.score) : "–"}/${el.dataset.todoPoints} pts`; + if (cached.score != null && !el.nextElementSibling?.classList.contains("better-todo-graded")) { + el.insertAdjacentHTML("afterend", TODO_GRADED_BADGE_HTML); + } + }); +} + function populateAssignments(iscompleted = false) { const today = new Date(); today.setHours(0,0,0,0); @@ -3858,6 +3963,7 @@ function populateAssignments(iscompleted = false) { // thin "motion lines" occupy the left edge), so it needs less of a // nudge — otherwise it reads as off-center next to the others. const iconLeftOffset = isCustomTask ? 2 : item.plannable_type == "quiz" ? 2 : 5; + const scoreBadge = options.todo_show_scores === true ? todoScoreBadgeHtml(item) : ""; const taskIcon = removeIcons ? "" : isCustomTask ? ` @@ -3881,7 +3987,7 @@ function populateAssignments(iscompleted = false) {
${item.context_name} ${item.plannable.title} - ${convertToDueDate(item.plannable_date)} + ${convertToDueDate(item.plannable_date)}${scoreBadge}
${editButtonSvg} @@ -3913,6 +4019,8 @@ function populateAssignments(iscompleted = false) { attachTodoHoverPreview(assignment, item); }); + if (options.todo_show_scores === true) loadTodoScores(iscompleted ? completed : assignmentsDue); + if (document.getElementById("better-todo-see-more")) { document.getElementById("better-todo-see-more").remove(); } diff --git a/js/popup.js b/js/popup.js index 0472aef..91126a4 100644 --- a/js/popup.js +++ b/js/popup.js @@ -25,6 +25,7 @@ const syncedSubOptions = [ "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", + "todo_show_scores", "grade_hover", "card_letter", // "hide_completed", @@ -59,7 +60,7 @@ const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"]; const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding"]; const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "tab_icons"]; const exportSidebar = ["better_sidebar", "sidebar_scale"]; -const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_timeframe", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "hover_preview"]; +const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_timeframe", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview"]; 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"]; // Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule, @@ -121,6 +122,7 @@ const defaultOptions = { "todo_alternate_colors": false, "todo_ignore_card_colors": false, "todo_remove_icons": false, + "todo_show_scores": false, "todo_hide_read": true, "condensed_cards": false, "center_cards": false, @@ -965,6 +967,7 @@ function setup() { "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", + "todo_show_scores", "grade_hover", "card_letter", // "hide_completed", @@ -1868,6 +1871,7 @@ function saveCurrentTheme() { "todo_alternate_colors": current["todo_alternate_colors"], "todo_ignore_card_colors": current["todo_ignore_card_colors"], "todo_remove_icons": current["todo_remove_icons"], + "todo_show_scores": current["todo_show_scores"], "better_sidebar": current["better_sidebar"], "sidebar_scale": current["sidebar_scale"], "imageSize": current["imageSize"], diff --git a/manifest.json b/manifest.json index dd38f50..76df536 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Canvas Refined", "description": "Even More Feature packed extension for Canvas.", - "version": "7.0.4", + "version": "7.0.5", "icons": { "16": "icon/icon-16.png", "32": "icon/icon-32.png",