custom timeframe FOR FREE

This commit is contained in:
Guy Sandler 2026-10-11 16:52:59 -07:00
parent befce19c90
commit 6850b3cb4f
5 changed files with 158 additions and 87 deletions

View File

@ -76,16 +76,22 @@ h2 {margin-top: 20px;font-weight:600}
.custom-card-hide {display: flex; align-items: center;} .custom-card-hide {display: flex; align-items: center;}
.option-name {font-weight: 600} .option-name {font-weight: 600}
.sub-text {font-size: 12px;color: #c7c7c7} .sub-text {font-size: 12px;color: #c7c7c7}
#todo_progress_rings, #anon_grade_preset {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_progress_rings, #anon_grade_preset {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;margin-bottom:6px;color-scheme:dark;}
#todo_progress_rings option, #anon_grade_preset option {background:#1e1e1e;color:#e2e2e2;} #todo_progress_rings option, #anon_grade_preset option {background:#1e1e1e;color:#e2e2e2;}
#todo_progress_rings option:checked, #todo_progress_rings option:checked,
#todo_progress_rings option:hover, #todo_progress_rings option:hover,
#anon_grade_preset option:checked, #anon_grade_preset option:checked,
#anon_grade_preset option:hover {background:#2e2e2e;color:#fff;} #anon_grade_preset 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 {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;margin-bottom:6px;color-scheme:dark;}
#todo_timeframe option {background:#1e1e1e;color:#e2e2e2;} #todo_timeframe option {background:#1e1e1e;color:#e2e2e2;}
#todo_timeframe option:checked, #todo_timeframe option:checked,
#todo_timeframe option:hover {background:#2e2e2e;color:#fff;} #todo_timeframe option:hover {background:#2e2e2e;color:#fff;}
/* Custom timeframe inputs, shown under the Time frame dropdown when "Custom" is selected */
.custom-tf-wrap {display:none;flex-direction:column;gap:6px;}
.custom-tf-wrap.visible {display:flex;}
.custom-tf-row {display:flex;align-items:center;gap:6px;}
.custom-tf-row input {flex:none;width:58px;font-size:12px;background:var(--inputbg);border:none;padding:6px;color:#e2e2e2;border-radius:6px;font-family:inherit;text-align:right;box-sizing:border-box;color-scheme:dark;}
.custom-tf-row input::-webkit-outer-spin-button, .custom-tf-row input::-webkit-inner-spin-button {opacity:1;}
/* Collapsible "More options" section for the Better Todo List sub-options */ /* Collapsible "More options" section for the Better Todo List sub-options */
.todo-more-options {margin-top: 7px;} .todo-more-options {margin-top: 7px;}
.todo-more-toggle {display: flex;align-items:center;gap:5px;font-size:12px;color:#a7a7a7;cursor:pointer;user-select:none;padding:3px 0;border-radius:5px;transition:color .1s;} .todo-more-toggle {display: flex;align-items:center;gap:5px;font-size:12px;color:#a7a7a7;cursor:pointer;user-select:none;padding:3px 0;border-radius:5px;transition:color .1s;}

View File

@ -291,7 +291,20 @@
<option value="2week">2 weeks</option> <option value="2week">2 weeks</option>
<option value="month">Month</option> <option value="month">Month</option>
<option value="2month">2 months</option> <option value="2month">2 months</option>
<option value="custom">Custom</option>
</select> </select>
<div id="todo_timeframe_custom" class="custom-tf-wrap">
<div class="custom-tf-row">
<span class="sub-text">From</span>
<input type="number" id="todo_timeframe_custom_past" min="0" max="365">
<span class="sub-text">days ago</span>
</div>
<div class="custom-tf-row">
<span class="sub-text">To</span>
<input type="number" id="todo_timeframe_custom_future" min="0" max="365">
<span class="sub-text">days from now</span>
</div>
</div>
</div> </div>
<div class="sub-option"> <div class="sub-option">
<input type="checkbox" id="hover_preview" name="hover_preview"> <input type="checkbox" id="hover_preview" name="hover_preview">

View File

@ -88,6 +88,9 @@ chrome.runtime.onInstalled.addListener(function () {
"todo_hide_feedback": false, "todo_hide_feedback": false,
"todo_full_height": true, "todo_full_height": true,
"todo_progress_rings": "rings", "todo_progress_rings": "rings",
"todo_timeframe": "all",
"todo_timeframe_custom_past": 7,
"todo_timeframe_custom_future": 14,
"todo_confetti": true, "todo_confetti": true,
"todo_show_scores": false, "todo_show_scores": false,
"custom_font_skip_p": false, "custom_font_skip_p": false,

View File

@ -1080,6 +1080,8 @@ function applyOptionsChanges(changes) {
case "num_todo_items": case "num_todo_items":
case "hover_preview": case "hover_preview":
case "todo_timeframe": case "todo_timeframe":
case "todo_timeframe_custom_past":
case "todo_timeframe_custom_future":
// case "todo_overdues": // case "todo_overdues":
case "todo_hide_feedback": case "todo_hide_feedback":
case "todo_hide_read": case "todo_hide_read":
@ -2236,9 +2238,10 @@ function updateIndicator(element) {
// better todo html // better todo html
betterTodoFilter = "tasks"; betterTodoFilter = "tasks";
// Timeframe filter for the upcoming Tasks tab. "all" = no limit; otherwise // Timeframe filter for the upcoming Tasks tab. "all" = no limit; otherwise
// items are limited to those due on/before now+range (which also keeps // items are limited to a window around now: one week of history back through
// overdue items). Only affects the Tasks (upcoming) tab; announcements and // now+range (older overdue backlog is cut). Only affects the Tasks (upcoming)
// completed are unaffected because their dates are in the past. // tab; announcements and completed are unaffected because their dates are in
// the past.
let betterTodoTimeframe = "all"; let betterTodoTimeframe = "all";
const BETTER_TODO_TIMEFRAME_DAYS = { const BETTER_TODO_TIMEFRAME_DAYS = {
"1week": 7, "1week": 7,
@ -2246,6 +2249,10 @@ const BETTER_TODO_TIMEFRAME_DAYS = {
"month": 30, "month": 30,
"2month": 60, "2month": 60,
}; };
// How much history the preset timeframes include: the current window starts
// this many days in the past so recently overdue work stays visible while
// older backlog is cut. Custom timeframes pick their own "days ago" bound.
const TODO_TIMEFRAME_LOOKBACK_DAYS = 4;
// Announcements older than this are never shown in the Better Todo list (and // Announcements older than this are never shown in the Better Todo list (and
// never count toward the unread badge). Unlike assignments, an announcement // never count toward the unread badge). Unlike assignments, an announcement
// stops being an actionable to-do once it's old: the planner cache keeps a // stops being an actionable to-do once it's old: the planner cache keeps a
@ -2255,10 +2262,11 @@ const BETTER_TODO_TIMEFRAME_DAYS = {
// available on the course's own Announcements page. // available on the course's own Announcements page.
const TODO_ANNOUNCEMENT_MAX_AGE_DAYS = 30; const TODO_ANNOUNCEMENT_MAX_AGE_DAYS = 30;
// Look-ahead paging for the timeframe window (arrow buttons under the Tasks // Look-ahead paging for the timeframe window (arrow buttons under the Tasks
// header). 0 = the current window (now through the timeframe cutoff, with // header). 0 = the current window (from the lookback bound — 1 week back for
// overdue items kept). Each press of the right arrow advances one full // presets, "X days ago" for custom — through the future edge). Each press of
// window further out, e.g. a 1-week timeframe on offset 1 shows only items // the right arrow advances one full window further out, e.g. a 1-week
// due in week 2 (the current week is skipped over). // timeframe on offset 1 shows only items due in week 2 (the current window is
// skipped over).
let betterTodoTimeframeOffset = 0; let betterTodoTimeframeOffset = 0;
// null = show every class; a string courseId = only that class's tasks. // null = show every class; a string courseId = only that class's tasks.
let betterTodoProgressFilter = null; let betterTodoProgressFilter = null;
@ -2267,25 +2275,73 @@ let domContainers = {};
// Resolve the persisted todo_timeframe option to a valid key ("all" when // Resolve the persisted todo_timeframe option to a valid key ("all" when
// unset/unknown). // unset/unknown).
function getTodoTimeframeKey() { function getTodoTimeframeKey() {
return (options.todo_timeframe && Object.prototype.hasOwnProperty.call(BETTER_TODO_TIMEFRAME_DAYS, options.todo_timeframe)) ? options.todo_timeframe : "all"; const allowed = ["all", "1week", "2week", "month", "2month", "custom"];
return allowed.includes(options.todo_timeframe) ? options.todo_timeframe : "all";
}
// Custom timeframe bounds ("custom" option): the window runs from X days ago
// to Y days in the future. Clamped to 0-365 days; the defaults match the
// popup's initial inputs (1 week of history, 2 weeks ahead).
function getTodoCustomTimeframeDays() {
const past = parseInt(options.todo_timeframe_custom_past);
const future = parseInt(options.todo_timeframe_custom_future);
return {
past: isNaN(past) ? 7 : Math.max(0, Math.min(365, past)),
future: isNaN(future) ? 14 : Math.max(0, Math.min(365, future)),
};
}
// Total length of the current timeframe window in days ("all" = 0). Presets
// cover their range ahead plus the shared 1-week lookback; custom windows are
// as long as their past+future range. Either way the pager tiles windows back
// to back.
function getTodoTimeframeSpanDays() {
const tf = getTodoTimeframeKey();
if (tf === "all") return 0;
if (tf === "custom") {
const c = getTodoCustomTimeframeDays();
return Math.max(1, c.past + c.future);
}
return BETTER_TODO_TIMEFRAME_DAYS[tf] + TODO_TIMEFRAME_LOOKBACK_DAYS;
}
// Days before "now" where the current window starts. Presets start at the
// shared 1-week lookback (recently overdue work is kept, older backlog is
// cut); custom windows start `past` days in the past per the user's setting.
function getTodoTimeframePastDays() {
const tf = getTodoTimeframeKey();
if (tf === "all") return 0;
if (tf === "custom") return getTodoCustomTimeframeDays().past;
return TODO_TIMEFRAME_LOOKBACK_DAYS;
} }
// Better Todo timeframe filter, shared by the task list and the progress // Better Todo timeframe filter, shared by the task list and the progress
// display so their counts always agree: keeps items due inside the current // display so their counts always agree: keeps items due inside the current
// window. Offset 0 keeps everything on/before now+range (overdue items are // window. Offset 0 runs from the lookback bound (1 week back for presets,
// before now, so they are kept too); offset N>0 shows only the window // "X days ago" for custom) through the future edge, so recently overdue work
// (now+N*range, now+(N+1)*range] so the current timeframe can be paged // stays visible while older backlog is cut; offset N>0 shows only the strict
// window (now+N*span, now+(N+1)*span] so the current timeframe can be paged
// through with the look-ahead arrows. "all" keeps everything. // through with the look-ahead arrows. "all" keeps everything.
function applyTodoTimeframe(items) { // `lenientPast` (Announcements / Completed tabs): on the current window those
// tabs keep everything on/before the cutoff — completed history and unread
// announcements older than the lookback are still meaningful there — while
// paging forward uses the same strict windows as Tasks.
function applyTodoTimeframe(items, lenientPast) {
betterTodoTimeframe = getTodoTimeframeKey(); betterTodoTimeframe = getTodoTimeframeKey();
if (betterTodoTimeframe === "all") return items; if (betterTodoTimeframe === "all") return items;
const days = BETTER_TODO_TIMEFRAME_DAYS[betterTodoTimeframe]; const dayMs = 24 * 60 * 60 * 1000;
const now = Date.now(); const now = Date.now();
const cutoff = now + ((betterTodoTimeframeOffset + 1) * days * 24 * 60 * 60 * 1000); const days = getTodoTimeframeSpanDays();
if (betterTodoTimeframeOffset <= 0) { const pastDays = getTodoTimeframePastDays();
// End of the current window: the future edge (presets end `days` out,
// custom windows end `future` days out; span - past = future in both).
const cutoff = now + ((betterTodoTimeframeOffset + 1) * days - pastDays) * dayMs;
// Start of the current window: the lookback bound (skipped for the
// lenient tabs on the current window).
if (lenientPast === true && betterTodoTimeframeOffset <= 0) {
return items.filter(item => new Date(item.plannable_date).getTime() <= cutoff); return items.filter(item => new Date(item.plannable_date).getTime() <= cutoff);
} }
const windowStart = now + (betterTodoTimeframeOffset * days * 24 * 60 * 60 * 1000); const windowStart = now + (betterTodoTimeframeOffset * days - pastDays) * dayMs;
return items.filter(item => { return items.filter(item => {
const t = new Date(item.plannable_date).getTime(); const t = new Date(item.plannable_date).getTime();
return t > windowStart && t <= cutoff; return t > windowStart && t <= cutoff;
@ -2297,11 +2353,12 @@ function applyTodoTimeframe(items) {
// of items the list is currently filtered to. // of items the list is currently filtered to.
function getTodoTimeframeWindow() { function getTodoTimeframeWindow() {
const tf = getTodoTimeframeKey(); const tf = getTodoTimeframeKey();
const days = tf === "all" ? 0 : BETTER_TODO_TIMEFRAME_DAYS[tf]; const days = getTodoTimeframeSpanDays();
const offset = (tf === "all") ? 0 : betterTodoTimeframeOffset; const offset = (tf === "all") ? 0 : betterTodoTimeframeOffset;
const pastDays = getTodoTimeframePastDays();
const now = Date.now(); const now = Date.now();
const start = new Date(now + (offset * days * 24 * 60 * 60 * 1000)); const start = new Date(now + (offset * days - pastDays) * 24 * 60 * 60 * 1000);
const end = new Date(now + ((offset + 1) * days * 24 * 60 * 60 * 1000)); const end = new Date(start.getTime() + days * 24 * 60 * 60 * 1000);
const fmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); const fmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
return { tf, range: fmt(start) + " \u2013 " + fmt(end) }; return { tf, range: fmt(start) + " \u2013 " + fmt(end) };
} }
@ -2334,9 +2391,11 @@ function updateTodoTimeframeNav() {
function getTodoTimeframeBounds() { function getTodoTimeframeBounds() {
const tf = getTodoTimeframeKey(); const tf = getTodoTimeframeKey();
if (tf === "all") return null; if (tf === "all") return null;
const span = BETTER_TODO_TIMEFRAME_DAYS[tf] * 24 * 60 * 60 * 1000; const spanDays = getTodoTimeframeSpanDays();
const pastDays = getTodoTimeframePastDays();
const now = Date.now(); const now = Date.now();
return { start: now + (betterTodoTimeframeOffset * span), end: now + ((betterTodoTimeframeOffset + 1) * span) }; const start = now + (betterTodoTimeframeOffset * spanDays - pastDays) * 24 * 60 * 60 * 1000;
return { start, end: start + spanDays * 24 * 60 * 60 * 1000 };
} }
// true when `courseId` is the dimmed-out class because another class is selected. // true when `courseId` is the dimmed-out class because another class is selected.
@ -2936,65 +2995,17 @@ function renderProgressOneLine(wrapper, shown, totalAll, completedAll, percent)
}); });
} }
// "Fake progress display": rewrite the per-course completion entries so the
// progress readout shows exactly `anon_progress_done` out of
// `anon_progress_total`. Course totals are re-distributed proportionally to
// their real share, completion ratios get a small per-course offset (seeded,
// so rerolling varies it) so the rings look organic, and rounding drift is
// corrected so the displayed sums add up to exactly the chosen numbers.
function applyAnonProgressOverride(entries) {
const rawDone = parseInt(options.anon_progress_done);
const rawTotal = parseInt(options.anon_progress_total);
const total = isNaN(rawTotal) ? 18 : Math.max(1, Math.min(999, rawTotal));
const done = isNaN(rawDone) ? 12 : Math.max(0, Math.min(total, rawDone));
// Spread the faked total across courses in proportion to their real
// share of the workload (minimum 1 each so every ring stays visible).
const realTotal = entries.reduce((s, e) => s + e.total, 0) || 1;
let assignedTotal = 0;
entries.forEach((e, i) => {
if (i === entries.length - 1) {
e.total = Math.max(1, total - assignedTotal);
} else {
e.total = Math.max(1, Math.round((total * e.total) / realTotal));
}
assignedTotal += e.total;
});
// Completed counts: target ratio plus a seeded per-course offset so the
// rings don't all show the same fill.
const ratio = done / total;
let assignedDone = 0;
entries.forEach(e => {
const variation = ((anonSeededHash("ring:" + e.courseId) % 100) / 100 - 0.5) * 0.3;
e.completed = Math.max(0, Math.min(e.total, Math.round(e.total * (ratio + variation))));
assignedDone += e.completed;
});
// Correct rounding drift so the sums match the chosen x/y exactly.
let drift = done - assignedDone;
while (drift !== 0) {
let changed = false;
for (const e of entries) {
if (drift > 0 && e.completed < e.total) { e.completed++; drift--; changed = true; }
else if (drift < 0 && e.completed > 0) { e.completed--; drift++; changed = true; }
if (drift === 0) break;
}
if (!changed) break;
}
}
function renderProgressRings(container, scopedData) { function renderProgressRings(container, scopedData) {
const mode = getProgressRingMode(); const mode = getProgressRingMode();
if (mode === "none") { container.innerHTML = ""; return; } if (mode === "none") { container.innerHTML = ""; return; }
// Apply the same timeframe window the list pages through so the counts // Apply the same timeframe window the list pages through so the counts
// in the display match what's shown below it. Unlike the Tasks list — // in the display match what's shown below it. Unlike the Tasks tab —
// which keeps every overdue item — finished work from before the window // which keeps recently overdue work — finished work from before the
// is history, not current workload, so it never counts. Unfinished items // window is history, not current workload, so it never counts. The
// from before the window still count on the current window (offset 0) // window is a strict slice (from the lookback bound through the future
// because the Tasks tab shows them as overdue; later windows are strict // edge), so the counts always match the Tasks tab exactly.
// slices, so the backlog only appears on the current one.
const taskItems = scopedData.filter(item => isTodoTaskType(item)); const taskItems = scopedData.filter(item => isTodoTaskType(item));
const bounds = getTodoTimeframeBounds(); const bounds = getTodoTimeframeBounds();
let allAssignments; let allAssignments;
@ -3004,8 +3015,7 @@ function renderProgressRings(container, scopedData) {
const isDone = (item) => (item.submissions?.submitted || item.planner_override?.marked_complete === true) && !isPinnedIncomplete(item); const isDone = (item) => (item.submissions?.submitted || item.planner_override?.marked_complete === true) && !isPinnedIncomplete(item);
allAssignments = taskItems.filter(item => { allAssignments = taskItems.filter(item => {
const t = new Date(item.plannable_date).getTime(); const t = new Date(item.plannable_date).getTime();
if (t > bounds.start && t <= bounds.end) return true; return t > bounds.start && t <= bounds.end;
return betterTodoTimeframeOffset === 0 && t <= bounds.start && !isDone(item);
}); });
} }
@ -3785,15 +3795,16 @@ async function createTodoSections(location) {
// The timeframe is a persisted Better Todo List sub-option set in the // 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 // popup. Read the current value each render so popup changes apply on
// the next render. Every tab is paged through time: Tasks by due // the next render. Every tab is paged through time: Tasks by due
// date, Announcements by posted date, Completed by due date. On the // date, Announcements by posted date, Completed by due date. The
// current window (offset 0) everything on/before the cutoff stays // Tasks tab bounds the past (1 week of history back), while
// visible, so only paging forward narrows these two tabs. // Announcements/Completed keep their older history on the current
// window and only narrow when paged forward.
// Pinned items ("sent back" submissions) bypass the window filter so // Pinned items ("sent back" submissions) bypass the window filter so
// an old pinned item still shows up under Tasks. // an old pinned item still shows up under Tasks.
const pinnedItems = assignmentsDue.filter(item => isPinnedIncomplete(item)); const pinnedItems = assignmentsDue.filter(item => isPinnedIncomplete(item));
assignmentsDue = applyTodoTimeframe(assignmentsDue.filter(item => !isPinnedIncomplete(item))).concat(pinnedItems); assignmentsDue = applyTodoTimeframe(assignmentsDue.filter(item => !isPinnedIncomplete(item)), false).concat(pinnedItems);
announcements = applyTodoTimeframe(announcements); announcements = applyTodoTimeframe(announcements, true);
completed = applyTodoTimeframe(completed); completed = applyTodoTimeframe(completed, true);
// console.log("assignments", assignmentsDue); // console.log("assignments", assignmentsDue);
// console.log("announcements", announcements); // console.log("announcements", announcements);
// console.log("completed", completed); // console.log("completed", completed);

View File

@ -8,6 +8,8 @@ const syncedSubOptions = [
"todo_confetti", "todo_confetti",
"todo_progress_rings", "todo_progress_rings",
"todo_timeframe", "todo_timeframe",
"todo_timeframe_custom_past",
"todo_timeframe_custom_future",
"device_dark", "device_dark",
"relative_dues", "relative_dues",
"card_overdues", "card_overdues",
@ -74,7 +76,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 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", "tab_icons"]; 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 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", "todo_show_scores", "hover_preview"]; const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_confetti", "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 exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted"]; 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"]; 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, // Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,
@ -197,6 +199,8 @@ const defaultOptions = {
"todo_full_height": false, "todo_full_height": false,
"todo_progress_rings": "rings", "todo_progress_rings": "rings",
"todo_timeframe": "all", "todo_timeframe": "all",
"todo_timeframe_custom_past": 7,
"todo_timeframe_custom_future": 14,
"todo_more_expanded": false, "todo_more_expanded": false,
"todo_confetti": true, "todo_confetti": true,
"device_dark": false, "device_dark": false,
@ -411,15 +415,39 @@ function setupTodoMoreOptions(initial) {
} }
// Timeframe dropdown for the upcoming Tasks tab. Persisted so the Better Todo // Timeframe dropdown for the upcoming Tasks tab. Persisted so the Better Todo
// List remembers the selected range across sessions. // List remembers the selected range across sessions. The "custom" option
// reveals two number inputs (days ago / days from now) below the dropdown.
function setTimeframeCustomVisibility(value) {
const wrap = document.getElementById("todo_timeframe_custom");
if (wrap) wrap.classList.toggle("visible", value === "custom");
}
function setupTimeframeSelect(initial) { function setupTimeframeSelect(initial) {
const el = document.querySelector("#todo_timeframe"); const el = document.querySelector("#todo_timeframe");
if (!el) return; if (!el) return;
const allowed = ["all", "1week", "2week", "month", "2month"]; const allowed = ["all", "1week", "2week", "month", "2month", "custom"];
let value = allowed.includes(initial) ? initial : "all"; let value = allowed.includes(initial) ? initial : "all";
el.value = value; el.value = value;
setTimeframeCustomVisibility(value);
el.addEventListener("change", function () { el.addEventListener("change", function () {
chrome.storage.sync.set({ "todo_timeframe": this.value }); chrome.storage.sync.set({ "todo_timeframe": this.value });
setTimeframeCustomVisibility(this.value);
});
}
// Custom timeframe bounds (shown when Time frame is "custom"): the todo list
// window runs from X days ago to Y days in the future. Clamped to 0-365 so a
// typo can't hide every task.
function setupCustomTimeframeInput(key, initial, fallback) {
const el = document.querySelector("#" + key);
if (!el) return;
const clamp = (v) => Math.max(0, Math.min(365, v));
const parsed = parseInt(initial);
el.value = isNaN(parsed) ? fallback : clamp(parsed);
el.addEventListener("change", function () {
const v = clamp(parseInt(this.value));
this.value = v;
chrome.storage.sync.set({ [key]: v });
}); });
} }
@ -1405,6 +1433,14 @@ function setup() {
identifier: "todo_timeframe", identifier: "todo_timeframe",
setup: (initial) => setupTimeframeSelect(initial), setup: (initial) => setupTimeframeSelect(initial),
}, },
{
identifier: "todo_timeframe_custom_past",
setup: (initial) => setupCustomTimeframeInput("todo_timeframe_custom_past", initial, 7),
},
{
identifier: "todo_timeframe_custom_future",
setup: (initial) => setupCustomTimeframeInput("todo_timeframe_custom_future", initial, 14),
},
{ {
identifier: "anon_grade_level", identifier: "anon_grade_level",
setup: (initial) => setupAnonGradeSelect(initial), setup: (initial) => setupAnonGradeSelect(initial),
@ -2291,6 +2327,8 @@ function saveCurrentTheme() {
"todo_confetti": current["todo_confetti"], "todo_confetti": current["todo_confetti"],
"todo_progress_rings": current["todo_progress_rings"], "todo_progress_rings": current["todo_progress_rings"],
"todo_timeframe": current["todo_timeframe"], "todo_timeframe": current["todo_timeframe"],
"todo_timeframe_custom_past": current["todo_timeframe_custom_past"],
"todo_timeframe_custom_future": current["todo_timeframe_custom_future"],
"todo_hr24": current["todo_hr24"], "todo_hr24": current["todo_hr24"],
"todo_separate_scrollbar": current["todo_separate_scrollbar"], "todo_separate_scrollbar": current["todo_separate_scrollbar"],
"todo_alternate_colors": current["todo_alternate_colors"], "todo_alternate_colors": current["todo_alternate_colors"],