diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f346f8..1a1262a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1877,6 +1877,9 @@ "todo_separate_scrollbar": { "message": "Separate Scrollbar" }, + "todo_sections_persist": { + "message": "Remember collapsed sections" + }, "todo_show_scores": { "message": "Show assignment scores" }, diff --git a/css/content.css b/css/content.css index 400204c..7aee149 100644 --- a/css/content.css +++ b/css/content.css @@ -103,6 +103,15 @@ #canvasrefined-todo-list, #canvasrefined-announcement-list {font-size: 12px} .canvasrefined-todo-container, .canvasrefined-todo-container:hover {color:#777} .canvasrefined-todo-container p {margin: 0!important;} +/* Better Todo: collapsible due-date sections (chevron + live item count) */ +.better-todo-section-header {display:flex;align-items:center;gap:6px;font-size:12px;color:var(--bctext-0);cursor:pointer;user-select:none;} +.better-todo-section-header:hover {color:var(--bclinks);} +.better-todo-section-header .better-todo-chevron {flex:none;transform:rotate(90deg);transition:transform .2s ease;} +.better-todo-dueheader.better-todo-collapsed .better-todo-section-header .better-todo-chevron {transform:rotate(0deg);} +.better-todo-dueheader.better-todo-collapsed .todo-group-list {display:none!important;} +/* count badge: theme "Background Alt" surface with the theme's Text Main number; + in light mode (no --bc vars) it falls back to the dark-mode palette pill */ +.better-todo-section-count {flex:none;margin-left:auto;background:var(--bcbackground-1,#161616);color:var(--bctext-0,#f5f5f5);border-radius:8px;padding:1px 7px;font-size:10.5px;font-weight:700;} .canvasrefined-todo-icon svg {height: 18px; width: 18px;margin:0} .canvasrefined-todoitem-course {overflow: hidden; text-overflow: ellipsis;white-space: nowrap;} .canvasrefined-todoitem-title {font-size: 14px;font-weight:bold;display:inline;margin-top:2px;} diff --git a/html/popup.html b/html/popup.html index 50f48c8..5010ec1 100644 --- a/html/popup.html +++ b/html/popup.html @@ -333,6 +333,10 @@ +
+ + +
diff --git a/js/content.js b/js/content.js index d86f89d..2edca3a 100644 --- a/js/content.js +++ b/js/content.js @@ -1288,6 +1288,13 @@ function applyOptionsChanges(changes) { case "custom_cards_3": customizeCards(); break; + case "todo_sections_persist": + // Persistence is opt-in: the moment it turns on, adopt the session's collapses; + // turned off, stored state stays put but is simply not read on the next load. + if (options.todo_sections_persist === true) { + chrome.storage.sync.set({ "todo_collapsed_sections": { ...todoCollapsedSections } }); + } + break; case "todo_hr24": case "todo_separate_scrollbar": case "num_todo_items": @@ -3818,10 +3825,20 @@ async function createTodoSections(location) { else if (key >= 7 && key < 30) label = replaceCountPlaceholder(getMsg("due_weeks", "Due {{COUNT}} weeks"), Math.floor(key / 7)); else if (key == "30") label = getMsg("due_one_month", "Due 1 month"); else label = "" + dueGroupLabel(key) + ""; - makeElement("div", wrapper, { - innerHTML: "" + label + "", - style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);" - }) + // Clickable section header: chevron + label + live item count (user suggestion: + // collapsible due-date sections with a per-section item indicator). + const sectionHeader = makeElement("div", wrapper, { "className": "better-todo-section-header" }); + sectionHeader.setAttribute("role", "button"); + sectionHeader.setAttribute("tabindex", "0"); + sectionHeader.setAttribute("aria-expanded", "true"); + sectionHeader.innerHTML = + '' + + '' + label + '' + + ''; + sectionHeader.addEventListener("click", () => toggleTodoSection(key, wrapper)); + sectionHeader.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleTodoSection(key, wrapper); } + }); let listContainer = makeElement("div", wrapper, { className: "todo-group-list" }); listContainer.style = "display:flex;flex-direction:column;gap:10px;"; @@ -3840,6 +3857,8 @@ async function createTodoSections(location) { populateAssignments(true); } + refreshTodoSections(); + const feedbackElement = location.querySelector(".recent_feedback"); // populate progress rings placeholder (respect user toggle) @@ -3954,6 +3973,61 @@ function clearTodoList() { }); } +/* +Collapsible due-date sections + +The collapse state is session-only by default: it lives in todoCollapsedSections and +dies with the reload. "Remember collapsed sections" (todo_sections_persist, off by +default) seeds the map from chrome.storage once per load and writes every toggle +back, so collapses survive reloads only while that checkbox is on. +*/ +let todoCollapsedSections = {}; +let todoCollapsedSeeded = false; + +function seedTodoCollapsedSections() { + if (todoCollapsedSeeded) return; + todoCollapsedSeeded = true; + if (options.todo_sections_persist === true && options.todo_collapsed_sections && typeof options.todo_collapsed_sections === "object") { + todoCollapsedSections = { ...options.todo_collapsed_sections }; + } +} + +function toggleTodoSection(key, wrapper) { + const collapsed = !wrapper.classList.contains("better-todo-collapsed"); + applyTodoSectionCollapsed(key, wrapper, collapsed); + if (collapsed) todoCollapsedSections[key] = true; + else delete todoCollapsedSections[key]; + if (options.todo_sections_persist === true) { + chrome.storage.sync.set({ "todo_collapsed_sections": { ...todoCollapsedSections } }); + } +} + +function applyTodoSectionCollapsed(key, wrapper, collapsed) { + wrapper.classList.toggle("better-todo-collapsed", collapsed); + const header = wrapper.querySelector(".better-todo-section-header"); + if (header) header.setAttribute("aria-expanded", collapsed ? "false" : "true"); +} + +// Re-apply collapse state and refresh the per-section item counts after each render. +// Headers are rebuilt on every render, so this is what carries the state across the +// clearTodoList/createTodoSections cycle. +function refreshTodoSections() { + seedTodoCollapsedSections(); + Object.keys(domContainers).forEach(key => { + const entry = domContainers[key]; + if (!entry || !entry.wrapper || !entry.wrapper.isConnected) return; + const collapsed = todoCollapsedSections[key] === true; + applyTodoSectionCollapsed(key, entry.wrapper, collapsed); + // Every row the section holds (hidden "view more" rows included). The rows are + // created with a `class:` property assignment that never becomes a real class + // attribute, so query the list's children instead of the row classes; the + // celebration preview's disposable card must not bump the count. + const count = [...entry.listContainer.children].filter(el => el.id !== "canvasrefined-celebration-preview").length; + const countEl = entry.wrapper.querySelector(".better-todo-section-count"); + if (countEl) countEl.textContent = count > 0 ? String(count) : ""; + }); +} + // "Alternate colors" (light mode only): icon fill goes white for lighter course strips. const TODO_ALT_ICON_COLOR = "#ffffff"; let todoAltStyleEl = null; @@ -5026,9 +5100,12 @@ function runCelebrationPreview() { wrapper: todayContainer.wrapper, display: todayContainer.wrapper.style.display, hadVisible: todayContainer.wrapper.hasAttribute("data-has-visible"), + wasCollapsed: todayContainer.wrapper.classList.contains("better-todo-collapsed"), }; todayContainer.wrapper.style.display = "block"; todayContainer.wrapper.setAttribute("data-has-visible", "true"); + // A collapsed section would hide the preview card; expand it for the run. + todayContainer.wrapper.classList.remove("better-todo-collapsed"); } else { card.style.cssText += "position:fixed;top:70px;right:16px;width:300px;z-index:2147483646;box-shadow:0 2px 10px rgba(0,0,0,.25);"; document.body.appendChild(card); @@ -5046,6 +5123,7 @@ function runCelebrationPreview() { if (restoreWrapper) { try { restoreWrapper.wrapper.style.display = restoreWrapper.display; + restoreWrapper.wrapper.classList.toggle("better-todo-collapsed", restoreWrapper.wasCollapsed); if (!restoreWrapper.hadVisible) restoreWrapper.wrapper.removeAttribute("data-has-visible"); } catch (e) { /* wrapper re-rendered away already */ } } diff --git a/js/popup.js b/js/popup.js index e02838c..79db0ae 100644 --- a/js/popup.js +++ b/js/popup.js @@ -55,6 +55,7 @@ const syncedSubOptions = [ "assignment_date_format", "todo_hr24", "todo_separate_scrollbar", + "todo_sections_persist", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", @@ -220,7 +221,7 @@ const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"]; const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding", "card_grid", "card_grid_columns", "card_grid_rows", "card_grid_column_gap", "card_grid_row_gap", "card_grid_center_rows", "card_grid_flex"]; const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "hide_sidebar", "tab_icons"]; const exportSidebar = ["better_sidebar", "sidebar_scale"]; -const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview"]; +const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview", "todo_sections_persist"]; const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", "gpa_calc_show_on_hover", "gpa_calc_align"]; // Widgets tab: every dashboard widget's layout (toggles, positions, heights, row // heights, order, timer presets, quick link sizing). Personal data (quick link items, @@ -335,6 +336,7 @@ const defaultOptions = { "card_opacity": 80, "card_blur": 8, "todo_hr24": false, + "todo_sections_persist": false, "anon_grade_level": "high", "anon_grade_seed": 0, "anon_fake_todo": false, @@ -2055,6 +2057,7 @@ async function setup() { "custom_font_skip_p", "todo_hr24", "todo_separate_scrollbar", + "todo_sections_persist", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons",