Compare commits

...

4 Commits

Author SHA1 Message Date
Guy Sandler
1a484e726b card opacity + padding 2026-08-25 16:48:15 -07:00
Guy Sandler
6c1bc9dc98 bug fixes 2026-08-25 16:03:37 -07:00
Guy Sandler
fcdfe41e84 timeframe for better todo 2026-08-25 15:38:00 -07:00
Guy Sandler
cedb83d3d3 weird bug fix 2026-08-25 14:56:34 -07:00
6 changed files with 284 additions and 53691 deletions

View File

@ -60,13 +60,16 @@
"message": "Quiz Safe Mode"
},
"quiz_safe_mode_desc": {
"message": "Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience."
"message": "Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience"
},
"opacity_options": {
"message": "Opacity & Blur"
},
"opacity_only_background": {
"message": "Only available with a custom background. Lower opacity to let the background show through; add blur for a glass effect."
"message": "Only available with a custom background"
},
"card_transparency": {
"message": "Card transparency"
},
"background_opacity": {
"message": "Background opacity"
@ -80,6 +83,12 @@
"sidebar_blur": {
"message": "Sidebar blur"
},
"card_opacity": {
"message": "Card opacity"
},
"card_blur": {
"message": "Card blur"
},
"sidebar_opacity_auto": {
"message": "Applies to the Better Sidebar when it's on, otherwise the normal sidebar."
},

View File

@ -21,8 +21,8 @@ h2 {font-size:24px;}
.header {font-size: 16px;display: flex;align-items: center;gap:12px; margin-bottom:14px;}
.header h1 { font-weight: 600; font-size:24px; }
#bclogo {height: 30px;width:30px;filter: none}
#numAssignmentsSlider, #numTodoItemsSlider, #sidebarScaleSlider, #customBackgroundScale, #bgOpacitySlider, #sidebarOpacitySlider, #bgBlurSlider, #sidebarBlurSlider {display: block;margin-top: 14px;width: 100%;-webkit-appearance: none; appearance: none;background:var(--inputbg);outline: none;height:6px;border-radius:20px;}
#numAssignmentsSlider::-webkit-slider-thumb, #numTodoItemsSlider::-webkit-slider-thumb, #sidebarScaleSlider::-webkit-slider-thumb, #customBackgroundScale::-webkit-slider-thumb, #bgOpacitySlider::-webkit-slider-thumb, #sidebarOpacitySlider::-webkit-slider-thumb, #bgBlurSlider::-webkit-slider-thumb, #sidebarBlurSlider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none;height: 18px; width: 18px;background: #727272;cursor: pointer;border-radius:30px;}
#numAssignmentsSlider, #numTodoItemsSlider, #sidebarScaleSlider, #customBackgroundScale, #bgOpacitySlider, #sidebarOpacitySlider, #bgBlurSlider, #sidebarBlurSlider, #cardOpacitySlider, #cardBlurSlider {display: block;margin-top: 14px;width: 100%;-webkit-appearance: none; appearance: none;background:var(--inputbg);outline: none;height:6px;border-radius:20px;}
#numAssignmentsSlider::-webkit-slider-thumb, #numTodoItemsSlider::-webkit-slider-thumb, #sidebarScaleSlider::-webkit-slider-thumb, #customBackgroundScale::-webkit-slider-thumb, #bgOpacitySlider::-webkit-slider-thumb, #sidebarOpacitySlider::-webkit-slider-thumb, #bgBlurSlider::-webkit-slider-thumb, #sidebarBlurSlider::-webkit-slider-thumb, #cardOpacitySlider::-webkit-slider-thumb, #cardBlurSlider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none;height: 18px; width: 18px;background: #727272;cursor: pointer;border-radius:30px;}
.option-container {font-size: 14px; margin-top: 8px;background: var(--containerbg);padding: 18px;border-radius: 14px;box-sizing:border-box}
.options .option-container {padding: 14px;}
a.option-container {display: block;color:#5ca5f6;font-size: 14px;}
@ -69,6 +69,10 @@ h2 {margin-top: 20px;font-weight:600}
#todo_progress_rings option {background:#1e1e1e;color:#e2e2e2;}
#todo_progress_rings option:checked,
#todo_progress_rings option:hover {background:#2e2e2e;color:#fff;}
#todo_timeframe {font-size: 12px;background: var(--inputbg);color:#e2e2e2;border:none;border-radius:7px;padding:6px 8px;font-family:inherit;width:100%;box-sizing:border-box;margin-top:2px;color-scheme:dark;}
#todo_timeframe option {background:#1e1e1e;color:#e2e2e2;}
#todo_timeframe option:checked,
#todo_timeframe option:hover {background:#2e2e2e;color:#fff;}
#advanced-settings, #gpa-bounds-btn, #custom-font-btn, #customize-dark-btn, #card-colors-btn {width: 100%;}
.back-btn {width: 120px;height:100%;margin-top:0}
.big-button {border: none;border-radius: 7px;background: var(--containerbg);color: #fff;font-family: inherit;padding: 12px 10px;font-weight:600}

View File

@ -259,6 +259,16 @@
<option value="line">One line</option>
</select>
</div>
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
<label for="todo_timeframe" class="sub-text">Time frame</label>
<select id="todo_timeframe" name="todo_timeframe">
<option value="all">All time</option>
<option value="1week">1 week</option>
<option value="2week">2 weeks</option>
<option value="month">Month</option>
<option value="2month">2 months</option>
</select>
</div>
<div class="sub-option">
<input type="checkbox" id="todo_confetti" name="todo_confetti">
<label for="todo_confetti" class="sub-text">Show confetti on complete</label>
@ -582,7 +592,7 @@
</div>
<div class="sub-option">
<input type="checkbox" id="export-background" />
<span>Background image</span>
<span>Background &amp; opacity</span>
</div>
</div>
<div style="flex: 1">
@ -841,6 +851,13 @@
<span class="sub-text">px</span>
</div>
</div>
<div class="sub-option">
<div style="margin-top: 5px;display: flex;gap: 8px;align-items: center">
<span class="sub-text" style="width:75px;">Card Padding</span>
<input type="number" id="cardPadding" class="card-input" min="0" max="100" value="0" style="width: 80px;">
<span class="sub-text">px</span>
</div>
</div>
</div>
</div>
@ -1041,6 +1058,10 @@
<input type="checkbox" id="fitImageToScreen" name="fitImageToScreen">
<label for="fitImageToScreen" class="sub-text">Fit image to screen</label>
</div>
<div class="sub-option" style="margin-top:8px;">
<input type="checkbox" id="card_transparency" name="card_transparency">
<label for="card_transparency" class="sub-text" data-i18n="card_transparency">Card transparency</label>
</div>
</div>
<div class="option-container" id="opacity-options" style="display:none;">
<h3 class="header-small" data-i18n="opacity_options">Opacity &amp; Blur</h3>
@ -1085,6 +1106,26 @@
</div>
<input type="range" min="0" max="30" step="1" id="sidebarBlurSlider">
</div>
<div class="opacity-slider-row" id="card-transparency-options" style="display:none;">
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center;margin-top:10px;">
<span class="sub-text" style="font-weight:600;color:#e2e2e2;" data-i18n="card_opacity">Card opacity</span>
<div style="display:flex;align-items:center;gap:6px;">
<button class="opacity-reset-btn" id="cardOpacityReset" type="button" title="Reset to default" data-i18n-title="reset_default"><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>
<span class="sub-text" id="cardOpacityValue">80%</span>
</div>
</div>
<input type="range" min="0" max="100" step="1" id="cardOpacitySlider">
</div>
<div class="opacity-slider-row" id="card-transparency-blur-row" style="display:none;">
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center;margin-top:10px;">
<span class="sub-text" style="font-weight:600;color:#e2e2e2;" data-i18n="card_blur">Card blur</span>
<div style="display:flex;align-items:center;gap:6px;">
<button class="opacity-reset-btn" id="cardBlurReset" type="button" title="Reset to default" data-i18n-title="reset_default"><svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>
<span class="sub-text" id="cardBlurValue">8px</span>
</div>
</div>
<input type="range" min="0" max="30" step="1" id="cardBlurSlider">
</div>
</div>
</div>

View File

@ -407,6 +407,14 @@ let reminderCheck = null;
let betterSidebarLoading = false;
let dashboardReadyTimer = null;
let sidebarReadyTimer = null;
// Signature of the dashboard card set last time we ran the full setup pass.
// The MutationObserver in checkDashboardReady fires on every childList change
// in the document — including the ones our own setup pass (loadCardAssignments,
// customizeCards, etc.) causes. Without a guard, that re-triggers the observer
// and re-runs the whole pass every animation frame (an infinite reflow loop).
// We only need to re-run when Canvas actually changes the dashboard cards, so
// we skip the heavy pass whenever the card set is unchanged.
let lastDashboardCardSignature = null;
let sidebarBadgeObserver = null;
let sidebarBadgeSyncTimer = null;
let sidebarBadgeWatchRetries = 0;
@ -663,6 +671,10 @@ function applyOptionsChanges(changes) {
)
setupCardAssignments();
loadCardAssignments();
// The card overflow fix in applyAestheticChanges() depends on
// assignments_due, so re-run it when that option toggles to keep
// the overflow rule in sync without a page reload.
applyAestheticChanges();
break;
case "custom_assignments":
case "assignment_date_format":
@ -684,6 +696,7 @@ function applyOptionsChanges(changes) {
case "todo_separate_scrollbar":
case "num_todo_items":
case "hover_preview":
case "todo_timeframe":
// case "todo_overdues":
case "todo_hide_feedback":
case "todo_full_height":
@ -745,8 +758,14 @@ function applyOptionsChanges(changes) {
case "cardSpacing":
case "cardWidth":
case "cardHeight":
case "cardPadding":
case "customCardStyles":
applyAestheticChanges();
// Coalesce rapid card-style edits (e.g. holding the arrow keys on a
// number input) into a single applyAestheticChanges() call. Each
// storage onChanged event would otherwise re-run the dashboard style
// pass immediately; a ~150ms cooldown is barely noticeable but keeps
// the page from thrashing while the user is still adjusting values.
debouncedApplyAestheticChanges();
break;
case "customBackgroundLink":
applyCustomBackground();
@ -761,6 +780,11 @@ function applyOptionsChanges(changes) {
break;
case "sidebar_opacity":
case "sidebar_blur":
applyCustomBackground();
break;
case "card_transparency":
case "card_opacity":
case "card_blur":
applyCustomBackground();
break;
case "better_todo":
@ -886,6 +910,14 @@ async function applyCustomBackground() {
// the previous dashboard-header glass look; sidebar defaults to none.
const bgBlur = Math.max(0, Math.min(30, Number(options.bg_blur ?? 8)));
const sidebarBlur = Math.max(0, Math.min(30, Number(options.sidebar_blur ?? 0)));
// Card transparency mirrors the content-panel glass effect (bg_opacity/
// bg_blur) but applies it to dashboard course cards (.ic-DashboardCard).
// Only active when the user explicitly enables it, since transparent cards
// over a busy background can hurt legibility.
const cardTransparency = options.card_transparency === true;
const cardOpacity = Math.max(0, Math.min(100, Number(options.card_opacity ?? 80)));
const cardBlur = Math.max(0, Math.min(30, Number(options.card_blur ?? 8)));
const cardTransparent = 100 - cardOpacity;
style.textContent = `
#wrapper {
background-image: url(${backgroundUrl}) !important;
@ -1134,7 +1166,11 @@ async function applyCustomBackground() {
.canvasrefined-gpa-card,
.canvasrefined-gpa,
.ic-DashboardCard {
background: var(--bcbackground-0) !important;
${cardTransparency
? `background: color-mix(in srgb, var(--bcbackground-0), transparent ${cardTransparent}%) !important;
backdrop-filter: blur(${cardBlur}px) saturate(120%) !important;
-webkit-backdrop-filter: blur(${cardBlur}px) saturate(120%) !important;`
: `background: var(--bcbackground-0) !important;`}
}
tr.student_assignment.assignment_graded.editable > * {
border:none!important
@ -1195,15 +1231,46 @@ function checkDashboardReady() {
const c = document.querySelector("#DashboardCard_Container");
if (c) {
let cards = document.querySelectorAll(".ic-DashboardCard");
changeGradientCards();
setupCardAssignments();
loadCardAssignments();
customizeCards(cards);
insertGrades();
loadDashboardNotes();
setupGPACalc();
showUpdateMsg();
createNasaInfoOverlay();
// Build a cheap signature of the current card set. The setup
// pass below mutates the cards' internals (assignment rows,
// grades, etc.) but never adds/removes the .ic-DashboardCard
// elements themselves, so the signature stays stable across
// our own mutations. It only changes when Canvas re-renders the
// dashboard (cards added/removed/reordered/replaced). Skipping
// when it's unchanged breaks the self-retriggering reflow loop.
let signature = cards.length + "";
for (let i = 0; i < cards.length; i++) {
const link = cards[i].querySelector(".ic-DashboardCard__link");
signature += "|" + (link ? link.getAttribute("href") : "");
}
// Canvas often re-renders the dashboard on a hard reload and
// replaces the .ic-DashboardCard nodes with fresh ones that have
// the same courses/links (so the signature is unchanged) but no
// longer carry our .canvasrefined-card-assignment marker. The
// signature guard alone would skip re-setup in that case, leaving
// card assignments empty until a popup toggle forces a reload.
// Re-run whenever any card is missing its marker too. This is safe
// from the self-retriggering reflow loop: after the pass every
// card has the marker, so our own subsequent mutation bursts skip.
let missingMarker = false;
for (let i = 0; i < cards.length; i++) {
if (!cards[i].querySelector(".canvasrefined-card-assignment")) {
missingMarker = true;
break;
}
}
if (signature !== lastDashboardCardSignature || missingMarker) {
lastDashboardCardSignature = signature;
changeGradientCards();
setupCardAssignments();
loadCardAssignments();
customizeCards(cards);
insertGrades();
loadDashboardNotes();
setupGPACalc();
showUpdateMsg();
createNasaInfoOverlay();
}
}
const rightSide = document.querySelector("#right-side");
@ -1501,6 +1568,17 @@ function updateIndicator(element) {
}
// better todo html
betterTodoFilter = "tasks";
// Timeframe filter for the upcoming Tasks tab. "all" = no limit; otherwise
// items are limited to those due on/before now+range (which also keeps
// overdue items). Only affects the Tasks (upcoming) tab; announcements and
// completed are unaffected because their dates are in the past.
let betterTodoTimeframe = "all";
const BETTER_TODO_TIMEFRAME_DAYS = {
"1week": 7,
"2week": 14,
"month": 30,
"2month": 60,
};
// null = show every class; a string courseId = only that class's tasks.
let betterTodoProgressFilter = null;
let domContainers = {};
@ -2633,6 +2711,15 @@ async function createTodoSections(location) {
announcements = displayData.filter(item => item.plannable_type == "announcement");
assignmentsDue = displayData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note") && !item.submissions?.submitted && !item.planner_override?.marked_complete);
completed = displayData.filter(item => (item.plannable_type == "assignment" || item.plannable_type == "planner_note") && (item.submissions?.submitted || item.planner_override?.marked_complete));
// The timeframe is a persisted Better Todo List sub-option set in the
// popup. Read the current value each render so popup changes apply on
// the next render. Keeps items due on/before now+range (overdue items
// are before now, so they are kept too). Only the Tasks tab is affected.
betterTodoTimeframe = (options.todo_timeframe && Object.prototype.hasOwnProperty.call(BETTER_TODO_TIMEFRAME_DAYS, options.todo_timeframe)) ? options.todo_timeframe : "all";
if (betterTodoTimeframe !== "all") {
const cutoff = Date.now() + (BETTER_TODO_TIMEFRAME_DAYS[betterTodoTimeframe] * 24 * 60 * 60 * 1000);
assignmentsDue = assignmentsDue.filter(item => new Date(item.plannable_date).getTime() <= cutoff);
}
// console.log("assignments", assignmentsDue);
// console.log("announcements", announcements);
// console.log("completed", completed);
@ -3013,7 +3100,7 @@ function populateAssignments(iscompleted = false) {
</div>
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;position:relative;">
<div style="display:flex;flex-direction:column;gap:3px;">
<span style="color:${courseColor};font-size:12px;margin-top:-2px;">${item.context_name}</span>
<span style="color:${courseColor};font-size:12px;margin-top:-2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;box-sizing:border-box;padding-right:22px;">${item.context_name}</span>
<a href="${taskHref}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
</div>
@ -3113,7 +3200,7 @@ function populateAnnouncements() {
</div>
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;">
<div style="display:flex;flex-direction:column;gap:3px;">
<span style="color:${courseColor};font-size:12px;margin-top:-2px;">${item.context_name}</span>
<span style="color:${courseColor};font-size:12px;margin-top:-2px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;box-sizing:border-box;padding-right:22px;">${item.context_name}</span>
<a href="${domain + item.html_url}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
</div>
@ -5141,6 +5228,18 @@ function loadCustomFont() {
Smaller features
*/
// Debounced wrapper around applyAestheticChanges for card-style options that
// can fire many storage onChanged events in quick succession (number inputs).
// See the "cardPadding"/"imageSize"/etc. cases in applyOptionsChanges.
let aestheticDebounceTimer = null;
function debouncedApplyAestheticChanges(delay = 150) {
if (aestheticDebounceTimer) clearTimeout(aestheticDebounceTimer);
aestheticDebounceTimer = setTimeout(() => {
aestheticDebounceTimer = null;
applyAestheticChanges();
}, delay);
}
function applyAestheticChanges() {
// Quiz safe mode: don't inject custom layout/aesthetic CSS on quiz pages.
if (quizSafeModeActive()) return;
@ -5158,7 +5257,30 @@ function applyAestheticChanges() {
if (options.cardRoundness !== undefined && options.cardRoundness !== 5) style.textContent += `.ic-DashboardCard {border-radius: ${options.cardRoundness}px!important;}`;
if (options.cardSpacing !== undefined && options.cardSpacing !== 0) style.textContent += `.ic-DashboardCard {margin-right: ${options.cardSpacing / 2}px!important; margin-bottom: ${options.cardSpacing / 2}px!important;}`;
if (options.cardWidth !== undefined && options.cardWidth !== 262) style.textContent += `.ic-DashboardCard {width: ${options.cardWidth}px!important;}`;
if (options.cardHeight !== undefined && options.cardHeight !== 250) style.textContent += `.ic-DashboardCard {height: ${options.cardHeight}px!important;}`;
// Always emit a fixed card height when custom card styles are on. The old
// `!== 250` guard silently dropped the height rule when cardHeight matched
// the default, which is exactly what happens after importing a theme that
// carries the default cardHeight (250) — leaving cards content-sized.
// Content-sized cards plus the dashboard reflow loop trigger Firefox scroll
// anchoring to yank the viewport back up while scrolling. A fixed height
// (even the default 250px) keeps layout stable.
if (options.cardHeight !== undefined && options.cardHeight !== null && options.cardHeight !== "") {
style.textContent += `.ic-DashboardCard {height: ${options.cardHeight}px!important;}`;
// Canvas sets overflow:hidden on .ic-DashboardCard. With a fixed
// height that clips the appended .canvasrefined-card-assignment area
// (the assignment rows live at the bottom of the card), making the
// .canvasrefined-assignment-link anchors unclickable for users with
// custom card styles enabled. Allow overflow so those rows stay
// visible and interactive when card assignments are shown.
if (options.assignments_due === true) style.textContent += `.ic-DashboardCard {overflow: visible!important;}`;
}
// Inner card padding. Applied to the whole .ic-DashboardCard box (the
// element that holds the hero header, title, and action buttons) so the
// colored hero header and its content all get consistent breathing room
// from the card's edges. Guarded by !== 0 (default).
if (options.cardPadding !== undefined && Number(options.cardPadding) > 0) {
style.textContent += `.ic-DashboardCard {padding: ${options.cardPadding}px!important;}`;
}
}
style.textContent += ".ic-app-nav-toggle-and-crumbs{display:none!important}";

View File

@ -4,6 +4,7 @@ const syncedSubOptions = [
"todo_full_height",
"todo_confetti",
"todo_progress_rings",
"todo_timeframe",
"device_dark",
"relative_dues",
"card_overdues",
@ -30,6 +31,7 @@ const syncedSubOptions = [
"cardSpacing",
"cardWidth",
"cardHeight",
"cardPadding",
"customBackgroundLink",
"customBackgroundScale",
"customBackgroundDaily",
@ -40,18 +42,20 @@ const syncedSubOptions = [
"sidebar_opacity",
"bg_blur",
"sidebar_blur",
"card_opacity",
"card_blur",
];
const localSwitches = [];
// Theme export only carries visual settings, never personal productivity data.
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding"];
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_feedback", "tab_icons"];
const exportSidebar = ["better_sidebar", "sidebar_scale"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "hover_preview"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_timeframe", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "hover_preview"];
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted"];
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur"];
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,
// no personal productivity features).
const exportToggles = ["dark_mode", "quiz_safe_mode"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
@ -102,6 +106,9 @@ const defaultOptions = {
"sidebar_opacity": 100,
"bg_blur": 8,
"sidebar_blur": 0,
"card_transparency": false,
"card_opacity": 80,
"card_blur": 8,
"todo_hr24": false,
"todo_separate_scrollbar": false,
"todo_alternate_colors": false,
@ -147,6 +154,7 @@ const defaultOptions = {
"todo_hide_feedback": false,
"todo_full_height": false,
"todo_progress_rings": "rings",
"todo_timeframe": "all",
"todo_confetti": true,
"device_dark": false,
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
@ -159,6 +167,7 @@ const defaultOptions = {
"cardSpacing": 0,
"cardWidth": 262,
"cardHeight": 250,
"cardPadding": 0,
"customCardStyles": false,
"customBackgroundLink": "",
"customBackgroundScale": 100,
@ -308,6 +317,19 @@ function setupProgressRingsSelect(initial) {
});
}
// Timeframe dropdown for the upcoming Tasks tab. Persisted so the Better Todo
// List remembers the selected range across sessions.
function setupTimeframeSelect(initial) {
const el = document.querySelector("#todo_timeframe");
if (!el) return;
const allowed = ["all", "1week", "2week", "month", "2month"];
let value = allowed.includes(initial) ? initial : "all";
el.value = value;
el.addEventListener("change", function () {
chrome.storage.sync.set({ "todo_timeframe": this.value });
});
}
function setupAutoDarkInput(initial, time) {
let el = document.querySelector('#' + time);
el.value = initial.hour + ":" + initial.minute;
@ -339,11 +361,26 @@ function setupDashboardMethod(initial) {
});
}
// Custom card style inputs (image size, roundness, spacing, width, height,
// padding) fire `chrome.storage.sync.set` on every keystroke/arrow press.
// chrome.storage.sync caps writes at MAX_WRITE_OPERATIONS_PER_MINUTE (120/min),
// so rapidly adjusting these number inputs used to hit the quota and surface
// a "quota exceeded" error. Coalesce rapid edits into a single write with a
// short (200ms) debounce — barely noticeable to the user, but stays well
// under the write quota even when dragging arrows or typing fast.
const cardStyleSetTimers = {};
function debouncedCardStyleSet(key, value, delay = 200) {
if (cardStyleSetTimers[key]) clearTimeout(cardStyleSetTimers[key]);
cardStyleSetTimers[key] = setTimeout(() => {
chrome.storage.sync.set({ [key]: value });
}, delay);
}
function setupImageSizeInput(initial) {
let el = document.querySelector("#imageSize");
el.value = initial;
el.addEventListener("input", (e) => {
chrome.storage.sync.set({ "imageSize": e.target.value });
debouncedCardStyleSet("imageSize", e.target.value);
});
}
@ -351,7 +388,7 @@ function setupCardRoundnessInput(initial) {
let el = document.querySelector("#cardRoundness");
el.value = initial;
el.addEventListener("input", (e) => {
chrome.storage.sync.set({ "cardRoundness": e.target.value });
debouncedCardStyleSet("cardRoundness", e.target.value);
});
}
@ -359,7 +396,7 @@ function setupCardSpacingInput(initial) {
let el = document.querySelector("#cardSpacing");
el.value = initial;
el.addEventListener("input", (e) => {
chrome.storage.sync.set({ "cardSpacing": e.target.value });
debouncedCardStyleSet("cardSpacing", e.target.value);
});
}
@ -367,7 +404,7 @@ function setupCardWidthInput(initial) {
let el = document.querySelector("#cardWidth");
el.value = initial;
el.addEventListener("input", (e) => {
chrome.storage.sync.set({ "cardWidth": e.target.value });
debouncedCardStyleSet("cardWidth", e.target.value);
});
}
@ -375,10 +412,18 @@ function setupCardHeightInput(initial) {
let el = document.querySelector("#cardHeight");
el.value = initial;
el.addEventListener("input", (e) => {
chrome.storage.sync.set({ "cardHeight": e.target.value });
debouncedCardStyleSet("cardHeight", e.target.value);
});
}
function setupCardPaddingInput(initial) {
let el = document.querySelector("#cardPadding");
el.value = initial;
el.addEventListener("input", (e) => {
debouncedCardStyleSet("cardPadding", e.target.value);
});
}
function setupCustomBackgroundLink(initial) {
let el = document.querySelector("#customBackgroundLink");
el.value = initial || "";
@ -499,6 +544,17 @@ function customBackgroundActive() {
function toggleOpacityOptions() {
const el = document.getElementById("opacity-options");
if (el) el.style.display = customBackgroundActive() ? "" : "none";
toggleCardTransparencyOptions();
}
// Card opacity/blur sliders only show when both a custom background is active
// (so #opacity-options is visible) and the "Card transparency" checkbox is on.
function toggleCardTransparencyOptions() {
const on = document.getElementById("card_transparency")?.checked === true;
const opacityRow = document.getElementById("card-transparency-options");
const blurRow = document.getElementById("card-transparency-blur-row");
if (opacityRow) opacityRow.style.display = on ? "" : "none";
if (blurRow) blurRow.style.display = on ? "" : "none";
}
// Hide the standalone feedback toggle when Better Todo's own sub-option replaces it.
@ -850,6 +906,7 @@ function setup() {
"customBackgroundDaily",
"customBackgroundNasaDaily",
"fitImageToScreen",
"card_transparency",
"customCardStyles",
],
tabs: {
@ -918,6 +975,14 @@ function setup() {
{
identifier: "sidebar_blur",
setup: (initial) => setupRangeSlider("sidebar_blur", "sidebarBlurSlider", "sidebarBlurValue", "sidebarBlurReset", 0, 30, "px", initial),
},
{
identifier: "card_opacity",
setup: (initial) => setupRangeSlider("card_opacity", "cardOpacitySlider", "cardOpacityValue", "cardOpacityReset", 80, 100, "%", initial),
},
{
identifier: "card_blur",
setup: (initial) => setupRangeSlider("card_blur", "cardBlurSlider", "cardBlurValue", "cardBlurReset", 8, 30, "px", initial),
},
{
identifier: "card_limit",
@ -951,6 +1016,10 @@ function setup() {
identifier: "cardHeight",
setup: (initial) => setupCardHeightInput(initial),
},
{
identifier: "cardPadding",
setup: (initial) => setupCardPaddingInput(initial),
},
{
identifier: "customBackgroundLink",
setup: (initial) => setupCustomBackgroundLink(initial),
@ -963,6 +1032,10 @@ function setup() {
identifier: "todo_progress_rings",
setup: (initial) => setupProgressRingsSelect(initial),
},
{
identifier: "todo_timeframe",
setup: (initial) => setupTimeframeSelect(initial),
},
],
};
@ -1426,32 +1499,13 @@ function setup() {
"F": { "cutoff": 0, "gpa": 0 }
});
});
document.getElementById("imageSize").addEventListener("input", (e) => {
const value = e.target.value;
chrome.storage.sync.set({ "imageSize": value });
document.querySelector("#imageSizeValue").textContent = value + "%";
})
document.getElementById("cardRoundness").addEventListener("input", (e) => {
const value = e.target.value;
chrome.storage.sync.set({ "cardRoundness": value });
document.querySelector("#cardRoundnessValue").textContent = value + "px";
})
document.getElementById("cardSpacing").addEventListener("input", (e) => {
const value = e.target.value;
chrome.storage.sync.set({ "cardSpacing": value });
document.querySelector("#cardSpacingValue").textContent = value + "px";
})
document.getElementById("cardWidth").addEventListener("input", (e) => {
const value = e.target.value;
chrome.storage.sync.set({ "cardWidth": value });
document.querySelector("#cardWidthValue").textContent = value + "%";
});
document.getElementById("cardHeight").addEventListener("input", (e) => {
const value = e.target.value;
chrome.storage.sync.set({ "cardHeight": value });
document.querySelector("#cardHeightValue").textContent = value + "%";
});
// Note: the card-style number inputs (imageSize, cardRoundness, cardSpacing,
// cardWidth, cardHeight, cardPadding) are wired via their setup*Input
// handlers in menu.special above, which debounce the storage writes. The
// duplicate listeners that used to live here referenced nonexistent
// #*Value spans (threw on every input) and double-wrote to storage, which
// is what blew the sync write quota when adjusting card styles quickly.
document.getElementById("clearCustomBackground").addEventListener("click", () => {
chrome.storage.sync.set({ "customBackgroundLink": "", "customBackgroundScale": 100 });
@ -1658,6 +1712,7 @@ function saveCurrentTheme() {
"todo_full_height": current["todo_full_height"],
"todo_confetti": current["todo_confetti"],
"todo_progress_rings": current["todo_progress_rings"],
"todo_timeframe": current["todo_timeframe"],
"todo_hr24": current["todo_hr24"],
"todo_separate_scrollbar": current["todo_separate_scrollbar"],
"better_sidebar": current["better_sidebar"],

File diff suppressed because one or more lines are too long