diff --git a/html/popup.html b/html/popup.html
index 91823e2..0dd12fe 100644
--- a/html/popup.html
+++ b/html/popup.html
@@ -228,6 +228,10 @@
+
+
+
+
Max items to show:
diff --git a/js/content.js b/js/content.js
index aad33c3..83e4d81 100644
--- a/js/content.js
+++ b/js/content.js
@@ -543,6 +543,29 @@ function applyOptionsChanges(changes) {
} else {
window.location.reload();
}
+ break;
+ case "todo_progress_rings": {
+ // toggle progress rings immediately
+ const placeholder = document.getElementById("better-todo-progress-placeholder");
+ if (!placeholder) break;
+ if (changes[key].newValue === true || changes[key].newValue === undefined) {
+ if (typeof assignments?.then === 'function') {
+ assignments.then(data => {
+ const courseId = getCurrentCourseId();
+ const scopedData = courseId
+ ? data.filter(item => {
+ const itemCourseId = parseInt(item.course_id || item.context_id || item?.plannable?.course_id);
+ return itemCourseId === courseId;
+ })
+ : data;
+ renderProgressRings(placeholder, scopedData);
+ });
+ }
+ } else {
+ placeholder.innerHTML = "";
+ }
+ break;
+ }
case "better_sidebar":
if (options.better_sidebar) {
ensureBetterSidebar();
@@ -1783,66 +1806,69 @@ async function createTodoSections(location) {
domContainers = {};
const groupKeys = ["-1", "0", "1", "2", "3", "4", "5", "6", "7", "14", "21", "30", "Later", "New", "Seen", "Ungraded", "Graded"];
- for (const key of groupKeys) {
- let wrapper = makeElement("div", mainSection, {
- style: "display:none;margin-top:10px;",
- className: "better-todo-dueheader",
- });
- let label = "";
- if (key == "Later") label = "Due Later";
- if (key == "-1") label = "Overdue";
- else if (key == "0") label = "Due Today";
- else if (key == "1") label = "Due Tommorow";
- else if (key >= 2 && key < 7) label = "Due " + key + " days";
- else if (key >= 7 && key < 30) label = "Due " + key/7 + " weeks";
- else if (key == "30") label = "Due 1 month";
- else label = "" + key + "";
- makeElement("div", wrapper, {
- innerHTML: "" + label + "",
- style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);" // TODO: might not be theme compatible
- })
+ for (const key of groupKeys) {
+ let wrapper = makeElement("div", mainSection, {
+ style: "display:none;margin-top:10px;",
+ className: "better-todo-dueheader",
+ });
+ let label = "";
+ if (key == "Later") label = "Due Later";
+ if (key == "-1") label = "Overdue";
+ else if (key == "0") label = "Due Today";
+ else if (key == "1") label = "Due Tommorow";
+ else if (key >= 2 && key < 7) label = "Due " + key + " days";
+ else if (key >= 7 && key < 30) label = "Due " + key/7 + " weeks";
+ else if (key == "30") label = "Due 1 month";
+ else label = "" + key + "";
+ makeElement("div", wrapper, {
+ innerHTML: "" + label + "",
+ style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);"
+ })
- let listContainer = makeElement("div", wrapper, { className: "todo-group-list" });
- listContainer.style = "display:flex;flex-direction:column;gap:10px;";
+ let listContainer = makeElement("div", wrapper, { className: "todo-group-list" });
+ listContainer.style = "display:flex;flex-direction:column;gap:10px;";
- domContainers[key] = { wrapper, listContainer };
- }
+ domContainers[key] = { wrapper, listContainer };
+ }
if (betterTodoFilter == "tasks") {
populateAssignments();
}
- if (betterTodoFilter == "announcements") {
- populateAnnouncements();
- }
- if (betterTodoFilter == "completed") {
- populateAssignments(true);
- }
+ if (betterTodoFilter == "announcements") {
+ populateAnnouncements();
+ }
+ if (betterTodoFilter == "completed") {
+ populateAssignments(true);
+ }
const feedbackElement = location.querySelector(".recent_feedback");
- // populate progress rings placeholder
+ // populate progress rings placeholder (respect user toggle)
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
if (progressPlaceholder) {
- renderProgressRings(progressPlaceholder, scopedData);
+ if (options.todo_progress_rings === undefined || options.todo_progress_rings === true) {
+ renderProgressRings(progressPlaceholder, scopedData);
+ } else {
+ progressPlaceholder.innerHTML = "";
+ }
}
// Only show the Add Task control on the Assignments (tasks) tab.
if (betterTodoFilter === "tasks") {
ensureTodoTaskMenu(location, feedbackElement);
} else {
- // remove the actions row if it exists when not on the assignments tab
const existing = location.querySelector("#better-todo-actions-row");
if (existing) existing.remove();
}
if (feedbackElement) {
- if (options.todo_hide_feedback == true) {
- feedbackElement.style.display = "none";
- } else {
- feedbackElement.style.display = "block";
- }
- }
+ if (options.todo_hide_feedback == true) {
+ feedbackElement.style.display = "none";
+ } else {
+ feedbackElement.style.display = "block";
+ }
+ }
const sidebar = document.getElementById("right-side-wrapper");
ensureRightSideWrapperScrollbarHidden();
@@ -2092,6 +2118,119 @@ function populateAnnouncements() {
});
}
+function createConfettiBurst(targetElement, opts = {}) {
+ try {
+ const count = opts.count || 48;
+ const colors = opts.colors || ['#ff4d4f', '#ffc107', '#28a745', '#17a2b8', '#6f42c1', '#ff6b6b', '#ff8a65', '#ffd54f'];
+ const rect = targetElement.getBoundingClientRect();
+ const container = document.createElement('div');
+ container.className = 'bettercanvas-confetti-container';
+ container.style.position = 'fixed';
+ container.style.left = '0';
+ container.style.top = '0';
+ container.style.pointerEvents = 'none';
+ container.style.overflow = 'visible';
+ container.style.zIndex = '2147483647';
+ document.body.appendChild(container);
+
+ const originX = rect.left + rect.width / 2;
+ const originY = rect.top + rect.height * 0.35;
+ const particles = [];
+
+ for (let i = 0; i < count; i++) {
+ const el = document.createElement('div');
+ el.className = 'bettercanvas-confetti';
+ const w = 4 + Math.floor(Math.random() * 7); // smaller pieces
+ const h = Math.max(3, Math.floor(w * (0.4 + Math.random() * 0.8)));
+ el.style.position = 'absolute';
+ el.style.width = w + 'px';
+ el.style.height = h + 'px';
+ el.style.background = colors[Math.floor(Math.random() * colors.length)];
+ el.style.left = (originX - w / 2) + 'px';
+ el.style.top = (originY - h / 2) + 'px';
+ el.style.opacity = '1';
+ el.style.borderRadius = Math.random() > 0.75 ? '50%' : '2px';
+ el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.18)';
+ el.style.transformOrigin = 'center center';
+ el.style.willChange = 'transform, opacity';
+ container.appendChild(el);
+
+ const duration = 850 + Math.floor(Math.random() * 500);
+ const delay = Math.floor(Math.random() * 90);
+ const spread = opts.spread || 110;
+ const horizontalBias = (Math.random() - 0.5) * 2;
+
+ // Arc stays lower and wider than the old cone-shaped burst.
+ const endX = originX + horizontalBias * (spread * (0.7 + Math.random() * 0.6));
+ const endY = originY - (16 + Math.random() * 34);
+ const ctrlX = originX + horizontalBias * (spread * 0.25) + (Math.random() - 0.5) * 14;
+ const ctrlY = originY - (30 + Math.random() * 55);
+
+ particles.push({
+ el,
+ delay,
+ duration,
+ originX,
+ originY,
+ ctrlX,
+ ctrlY,
+ endX,
+ endY,
+ rotate: (Math.random() * 260) - 130,
+ scale: 0.8 + Math.random() * 0.5,
+ });
+ }
+
+ const startTime = performance.now();
+ let rafId = null;
+
+ const animate = now => {
+ let active = false;
+
+ for (let i = particles.length - 1; i >= 0; i--) {
+ const particle = particles[i];
+ const elapsed = now - startTime - particle.delay;
+ if (elapsed < 0) {
+ active = true;
+ continue;
+ }
+
+ const progress = Math.min(1, elapsed / particle.duration);
+ const eased = 1 - Math.pow(1 - progress, 3);
+
+ const x = (1 - eased) * (1 - eased) * particle.originX + 2 * (1 - eased) * eased * particle.ctrlX + eased * eased * particle.endX;
+ const y = (1 - eased) * (1 - eased) * particle.originY + 2 * (1 - eased) * eased * particle.ctrlY + eased * eased * particle.endY;
+
+ particle.el.style.transform = `translate(${Math.round(x - particle.originX)}px, ${Math.round(y - particle.originY)}px) rotate(${particle.rotate * eased}deg) scale(${particle.scale * (1 - eased * 0.15)})`;
+ particle.el.style.opacity = String(1 - progress);
+
+ if (progress < 1) {
+ active = true;
+ } else {
+ particle.el.remove();
+ particles.splice(i, 1);
+ }
+ }
+
+ if (active) {
+ rafId = requestAnimationFrame(animate);
+ } else {
+ try { container.remove(); } catch (e) { /* ignore */ }
+ if (rafId) cancelAnimationFrame(rafId);
+ }
+ };
+
+ rafId = requestAnimationFrame(animate);
+
+ // cleanup container after animations
+ setTimeout(() => {
+ try { container.remove(); } catch (e) { /* ignore */ }
+ }, 2400);
+ } catch (e) {
+ console.error('confetti error', e);
+ }
+}
+
function markAs(item, element) {
const csrfToken = CSRFtoken();
const completeState = item.planner_override ? !item.planner_override.marked_complete : true;
@@ -2117,9 +2256,14 @@ function markAs(item, element) {
element.style.transform = "translate(100%)";
element.style.opacity = "0";
+ // fire confetti only when marking complete (not when unmarking)
+ if (completeState) {
+ try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
+ }
+
// update progress rings immediately so they animate while the item slides/fades
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
- if (progressPlaceholder && typeof assignments?.then === 'function') {
+ if (progressPlaceholder && typeof assignments?.then === 'function' && (options.todo_progress_rings === undefined || options.todo_progress_rings === true)) {
assignments.then(data => {
const courseId = getCurrentCourseId();
const scopedData = courseId
diff --git a/js/popup.js b/js/popup.js
index 94976aa..6106369 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -112,6 +112,7 @@ const defaultOptions = {
"tab_icons": false,
"todo_hide_feedback": false,
"todo_full_height": false,
+ "todo_progress_rings": true,
"device_dark": false,
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
// "show_updates": false,
@@ -302,7 +303,8 @@ function setup() {
"gpa_calc_weighted",
"gpa_calc_cumulative",
// /*'card_method_date',*/ "show_updates",
- "todo_hide_feedback",
+ "todo_hide_feedback",
+ "todo_progress_rings",
"todo_full_height",
"device_dark",
"relative_dues",