diff --git a/css/content.css b/css/content.css
index 60e35a6..5145a70 100644
--- a/css/content.css
+++ b/css/content.css
@@ -48,7 +48,7 @@
}
.canvasrefined-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;}
.canvasrefined-custom-open {max-height: 250px}
-#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 420px;}
+#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 520px;}
.canvasrefined-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important}
#better-todo-new-task-date,
#better-todo-new-task-time {
@@ -58,10 +58,9 @@
font-size: 12px;
padding-inline: 6px;
}
-#better-todo-new-task-date::-webkit-calendar-picker-indicator {
- filter: invert(1) brightness(2);
- opacity: .95;
-}
+/* The native date/time picker icons are themed in dark mode only via
+ color-scheme (see DARKMODE_CSS in css/darkmodecss.js); light mode keeps the
+ default dark glyphs on the light surface. */
.canvasrefined-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;}
/* Better sidebar buttons sit on the --bcsidebar surface and use --bcsidebar-text
for labels/icons, so drop the light button chrome and use a subtle hover tint. */
@@ -204,7 +203,7 @@
.canvasrefined-notes-rendered ul,.canvasrefined-notes-rendered ol {margin:8px 0;padding-left:24px;}
.canvasrefined-notes-rendered li {margin:3px 0;}
.canvasrefined-notes-rendered li.cr-task {list-style:none;margin-left:-18px;display:flex;align-items:flex-start;gap:6px;}
-.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;}
+.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;cursor:pointer;}
.canvasrefined-notes-rendered blockquote {margin:8px 0;padding:4px 12px;border-left:3px solid var(--bcborders,#c7cdd1);color:var(--bctext-1,#5b6770);background:var(--bcbackground-2,rgba(0,0,0,.03));}
.canvasrefined-notes-rendered code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bcbackground-2,#f1f3f5);padding:1px 5px;border-radius:3px;}
.canvasrefined-notes-rendered pre {margin:10px 0;padding:10px 12px;background:var(--bcbackground-2,#f1f3f5);border:1px solid var(--bcborders,#e2e6e8);border-radius:4px;overflow:auto;}
diff --git a/css/darkmodecss.js b/css/darkmodecss.js
index cb0bd08..9a7baee 100644
--- a/css/darkmodecss.js
+++ b/css/darkmodecss.js
@@ -1193,4 +1193,18 @@ hr {
.event-details strong {
color: var(--bctext-0) !important;
}
+/* Theme the native date/time picker icons so they stay visible on the dark
+ sidebar surface. Applied in dark mode only — light mode keeps the default
+ dark glyphs (see css/content.css).
+ color-scheme: dark is the cross-browser way to flip the native picker icon
+ to a light glyph. Firefox 109+ ignores ::-webkit-calendar-picker-indicator
+ (the icon lives in a closed Shadow DOM), so a filter alone does nothing
+ there; color-scheme fixes both Firefox and modern Chrome. Do NOT also apply
+ an invert filter — color-scheme already makes the glyph light, and inverting
+ would flip it back to dark in Chrome. */
+#better-todo-new-task-date,
+#better-todo-new-task-time {
+ color-scheme: dark !important;
+ color: var(--bctext-0) !important;
+}
`;
\ No newline at end of file
diff --git a/html/popup.html b/html/popup.html
index 9883a90..0526500 100644
--- a/html/popup.html
+++ b/html/popup.html
@@ -500,7 +500,13 @@
-
Export Settings
+
+ Export Settings
+
+
+
+
+
diff --git a/js/background.js b/js/background.js
index 895780d..f8bebbf 100644
--- a/js/background.js
+++ b/js/background.js
@@ -81,6 +81,8 @@ chrome.runtime.onInstalled.addListener(function () {
"tab_icons": false,
"todo_hide_feedback": false,
"todo_full_height": true,
+ "todo_progress_rings": true,
+ "todo_confetti": true,
"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 fd8fa6e..524af50 100644
--- a/js/content.js
+++ b/js/content.js
@@ -284,9 +284,7 @@ function createNasaInfoOverlay() {
const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel");
const populatePanel = async () => {
- // The NASA worker (background.js getNasaBackground) falls back up to 7 days
- // when today's APOD isn't available yet, so search backward for the date
- // that was actually cached and displayed instead of assuming today.
+ // Search backward up to 7 days for the APOD actually cached, since today's may not be ready.
const date = new Date();
for (let i = 0; i < 7; i++) {
const dateStr = date.toISOString().slice(0, 10);
@@ -620,8 +618,7 @@ function applyOptionsChanges(changes) {
loadCardAssignments();
break;
case "equal_height_cards":
- // No need to rebuild the assignment rows — just stretch (or reset)
- // the card heights in place for a snappy toggle.
+ // Stretch or reset card heights in place instead of rebuilding rows.
equalizeCardHeights();
break;
case "custom_cards":
@@ -833,7 +830,7 @@ async function applyCustomBackground() {
-webkit-backdrop-filter: blur(8px) saturate(120%) !important;
}
#right-side-wrapper {
- // backdrop-filter: blur(10px) !important;
+ /* backdrop-filter: blur(10px) !important; */
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%);
border-radius: 5px;
}
@@ -1553,6 +1550,7 @@ function buildPlannerNotePayload(form) {
const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value;
const dateValue = form.querySelector("#better-todo-new-task-date")?.value;
const timeValue = form.querySelector("#better-todo-new-task-time")?.value;
+ const link = form.querySelector("#better-todo-new-task-link")?.value?.trim() || "";
if (!title) {
throw new Error("Task title is required.");
@@ -1570,6 +1568,7 @@ function buildPlannerNotePayload(form) {
return {
title,
details,
+ link,
courseId: courseIdRaw ? parseInt(courseIdRaw) : null,
// Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes.
todoDate: `${dateValue}T${timeValue}:00`,
@@ -1646,6 +1645,167 @@ async function createCanvasPlannerNote(payload) {
throw new Error(lastError || "Canvas rejected task creation.");
}
+/* Custom task links: Canvas planner notes have no link field, so store a
+ user link in sync storage keyed by the note id. */
+function getCustomTaskLinks() {
+ return (options && options.custom_task_links) || {};
+}
+
+function getCustomTaskLinkId(item) {
+ return item?.plannable_id ?? item?.plannable?.id ?? null;
+}
+
+function normalizeTaskLink(link) {
+ if (!link) return "";
+ link = String(link).trim();
+ if (!link) return "";
+ if (/^https?:\/\//i.test(link)) return link;
+ if (link.startsWith("//")) return "https:" + link;
+ return domain + (link.startsWith("/") ? link : "/" + link);
+}
+
+function customTaskHref(item) {
+ const id = getCustomTaskLinkId(item);
+ const links = getCustomTaskLinks();
+ if (id != null && links[String(id)]) {
+ return normalizeTaskLink(links[String(id)]);
+ }
+ const courseId = item?.course_id || item?.plannable?.course_id || item?.context_id;
+ if (courseId) return `${domain}/courses/${courseId}`;
+ return `${domain}/`;
+}
+
+function saveCustomTaskLink(noteId, link) {
+ if (noteId == null) return;
+ const links = { ...getCustomTaskLinks() };
+ const key = String(noteId);
+ if (link && String(link).trim()) {
+ links[key] = String(link).trim();
+ } else {
+ delete links[key];
+ }
+ options = { ...options, custom_task_links: links };
+ chrome.storage.sync.set({ custom_task_links: links });
+}
+
+function deleteCustomTaskLink(noteId) {
+ if (noteId == null) return;
+ const links = { ...getCustomTaskLinks() };
+ delete links[String(noteId)];
+ options = { ...options, custom_task_links: links };
+ chrome.storage.sync.set({ custom_task_links: links });
+}
+
+async function updateCanvasPlannerNote(id, payload) {
+ if (!id) throw new Error("Missing task id.");
+ const csrfToken = CSRFtoken();
+ const plannerNote = {
+ title: payload.title,
+ todo_date: payload.todoDate,
+ };
+ if (payload.details) plannerNote.details = payload.details;
+ // Sending course_id as empty string disassociates the note from its course.
+ plannerNote.course_id = payload.courseId || "";
+
+ const attempts = [
+ {
+ headers: {
+ "content-type": "application/json",
+ "accept": "application/json",
+ "X-CSRF-Token": csrfToken,
+ },
+ body: JSON.stringify({ planner_note: plannerNote }),
+ },
+ {
+ headers: {
+ "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
+ "accept": "application/json",
+ "X-CSRF-Token": csrfToken,
+ },
+ body: (() => {
+ const formBody = new URLSearchParams();
+ formBody.set("planner_note[title]", plannerNote.title);
+ formBody.set("planner_note[todo_date]", plannerNote.todo_date);
+ if (plannerNote.details) formBody.set("planner_note[details]", plannerNote.details);
+ formBody.set("planner_note[course_id]", plannerNote.course_id);
+ return formBody.toString();
+ })(),
+ },
+ ];
+
+ let lastError = "Canvas rejected task update.";
+ for (const attempt of attempts) {
+ const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
+ method: "PUT",
+ headers: attempt.headers,
+ body: attempt.body,
+ });
+
+ if (response.status === 200 || response.status === 201) {
+ return response.json();
+ }
+
+ try {
+ const errData = await response.json();
+ if (errData?.errors?.length) {
+ lastError = errData.errors.join(" ");
+ } else if (errData?.message) {
+ lastError = errData.message;
+ }
+ } catch (_) {
+ // Keep prior error text when body is not JSON.
+ }
+ }
+
+ throw new Error(lastError || "Canvas rejected task update.");
+}
+
+async function deleteCanvasPlannerNote(id) {
+ if (!id) throw new Error("Missing task id.");
+ const csrfToken = CSRFtoken();
+ const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
+ method: "DELETE",
+ headers: {
+ "content-type": "application/json",
+ "accept": "application/json",
+ "X-CSRF-Token": csrfToken,
+ },
+ });
+
+ if (response.status === 200 || response.status === 201 || response.status === 204) {
+ return true;
+ }
+
+ let lastError = "Canvas rejected task deletion.";
+ try {
+ const errData = await response.json();
+ if (errData?.errors?.length) lastError = errData.errors.join(" ");
+ else if (errData?.message) lastError = errData.message;
+ } catch (_) { /* ignore */ }
+ throw new Error(lastError);
+}
+
+/* Scroll a task form field into view, leaving room below for the native date/time picker. */
+function scrollTodoIntoView(el, smooth = true) {
+ if (!el) return;
+ const sidebar = document.getElementById("right-side-wrapper");
+ const style = sidebar ? getComputedStyle(sidebar) : null;
+ const scrollsSidebar = sidebar &&
+ (style.overflowY === "auto" || style.overflowY === "scroll") &&
+ sidebar.scrollHeight > sidebar.clientHeight;
+ const behavior = smooth ? "smooth" : "auto";
+ if (scrollsSidebar) {
+ const rect = el.getBoundingClientRect();
+ const sRect = sidebar.getBoundingClientRect();
+ const elTop = rect.top - sRect.top + sidebar.scrollTop;
+ // Keep the element near the top so the picker below it stays on screen.
+ const target = elTop - (sidebar.clientHeight - rect.height) * 0.3;
+ sidebar.scrollTo({ top: Math.max(0, target), behavior });
+ } else {
+ el.scrollIntoView({ block: "center", behavior });
+ }
+}
+
function fillTaskCourseOptions(courseSelect) {
const cards = options.custom_cards || {};
const courseColors = options.custom_cards_3 || {};
@@ -1705,7 +1865,8 @@ function ensureTodoTaskMenu(location, feedbackElement) {
});
menu.innerHTML = `
-
+
+
@@ -1713,9 +1874,13 @@ function ensureTodoTaskMenu(location, feedbackElement) {