From 80662e75e8bbb41bc826ec9011ee0ed08eccf4b1 Mon Sep 17 00:00:00 2001 From: Guy Sandler Date: Sun, 18 Oct 2026 16:57:19 -0700 Subject: [PATCH] finished up widgets --- README.md | 1 + _locales/en/messages.json | 6 - css/content.css | 97 ++----- css/popup.css | 14 +- html/popup.html | 182 ++++++------ js/background.js | 63 ++++- js/content.js | 581 +++++++++++++++++++------------------- js/popup.js | 150 ++++++---- manifest.json | 1 + 9 files changed, 572 insertions(+), 523 deletions(-) diff --git a/README.md b/README.md index cd5a7cd..f107363 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ If nobody is working on the feature, you can clone the dev branch and make your #### Fork Contributers - [TabulateJarl8](https://github.com/TabulateJarl8) - [dhogenson](https://github.com/dhogenson) +- [Y2Kwastaken](https://github.com/Y2Kwastaken) #### Original Owner diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 23cba9f..cee46de 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -146,18 +146,12 @@ "breathing_breaks": { "message": "Breathing Breaks" }, - "music": { - "message": "Music" - }, "daily_quotes": { "message": "Daily Quotes" }, "widget_short_focus": { "message": "Focus" }, - "widget_short_music": { - "message": "Music" - }, "widget_short_breathing": { "message": "Breathing" }, diff --git a/css/content.css b/css/content.css index 36f66b7..2fc904e 100644 --- a/css/content.css +++ b/css/content.css @@ -473,7 +473,7 @@ modal. */ /* ===== GPA calculator modal (rebuilt) ===== */ #canvasrefined-gpa-modal { - --cr-gpa-course-cols: 30px minmax(0,1fr) 66px 58px 118px 16px minmax(88px,auto); + --cr-gpa-course-cols: 30px minmax(0,1fr) 80px 58px 118px 16px minmax(88px,auto); --cr-gpa-accent: var(--ic-brand-primary,#0374b5); position:fixed;inset:0;z-index:2147483100;background:rgba(15,25,40,.5);backdrop-filter:blur(3px); display:flex;align-items:flex-start;justify-content:center;padding:7vh 16px 16px; @@ -544,12 +544,12 @@ .cr-gpa-suffix {flex:none;padding:0 8px 0 2px;font-size:11px;color:var(--bctext-2,#9aa3ab);} .cr-gpa-note {color:var(--bctext-2,#9aa3ab);font-size:12px;line-height:1.5;margin:14px 0 0;padding-top:12px;border-top:1px solid var(--bcborders,#c7cdd1);} @media (max-width:720px) { - #canvasrefined-gpa-modal {--cr-gpa-course-cols:26px minmax(0,1fr) 58px 52px 108px 15px 88px;padding:4vh 8px 8px;} + #canvasrefined-gpa-modal {--cr-gpa-course-cols:26px minmax(0,1fr) 72px 52px 108px 15px 88px;padding:4vh 8px 8px;} .cr-gpa-body {grid-template-columns:1fr;} .cr-gpa-pane-scale {border-left:none;border-top:1px solid var(--bcborders,#c7cdd1);} } -/* ===== Generic dashboard widgets: Focus Timer, Music, Breathing, Quotes ===== +/* ===== Generic dashboard widgets: Focus Timer, Breathing, Quotes ===== Shared shell follows the same card look as Notes/GPA: 262px grid cell with the 36px Canvas gutter when placed before/after the course cards, and a full-width flowing card when placed in the top/bottom container row. */ @@ -575,6 +575,7 @@ width: 100%; height: 100%; margin: 0; + min-height: 0; } .canvasrefined-widget-slot .canvasrefined-widget-card {width:100%;margin:0;height:100%;} .cr-widget-head {display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid var(--bcborders,#c7cdd1);} @@ -595,14 +596,14 @@ .cr-timer-preset.active {background:color-mix(in srgb, var(--bclinks,#0374b5) 18%, transparent);border-color:var(--bclinks,#0374b5);color:var(--bclinks,#0374b5);} .cr-timer-custom-row {display:flex;gap:8px;padding:10px 14px 0;align-items:center;} .cr-timer-custom-row[hidden] {display:none;} -.cr-time-fields {display:flex;gap:6px;flex:1;min-width:0;} -.cr-tf-box {display:flex;align-items:center;gap:2px;flex:1;min-width:0;border:1px solid var(--bcborders,#c7cdd1);border-radius:8px;padding:4px 8px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 5%, transparent);} -.cr-tf-box:focus-within {border-color:var(--bclinks,#0374b5);box-shadow:0 0 0 2px color-mix(in srgb, var(--bclinks,#0374b5) 22%, transparent);} -.cr-tf {width:100%;min-width:0;border:none;background:transparent;color:var(--bctext-0,#2d3b45);font:inherit;font-size:14px;font-weight:600;text-align:center;padding:2px 0;} -.cr-tf:focus {outline:none;} -.cr-tf::-webkit-outer-spin-button,.cr-tf::-webkit-inner-spin-button {-webkit-appearance:none;margin:0;} -.cr-tf-unit {font-size:10px;font-weight:700;color:var(--bctext-2,#9aa3ab);text-transform:uppercase;} -.cr-timer-custom-row .cr-widget-btn {flex:none;padding:8px 14px;} +/* Compact clock entry: 0 : 00 : 00, click a segment to edit it. */ +.cr-time-input {display:flex;align-items:center;justify-content:center;gap:1px;flex:1;min-width:0;border:1px solid var(--bcborders,#c7cdd1);border-radius:8px;padding:4px 8px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 5%, transparent);} +.cr-time-input:focus-within {border-color:var(--bclinks,#0374b5);box-shadow:0 0 0 2px color-mix(in srgb, var(--bclinks,#0374b5) 22%, transparent);} +.cr-tf {width:2.4ch;min-width:0;border:none;background:transparent;color:var(--bctext-0,#2d3b45);font:inherit;font-size:15px;font-weight:700;line-height:1.2;text-align:center;padding:0;border-radius:4px;} +.cr-tf:focus {outline:none;background:color-mix(in srgb, var(--bclinks,#0374b5) 20%, transparent);} +.cr-tf::placeholder {color:var(--bctext-2,#9aa3ab);} +.cr-tf-colon {font-size:15px;font-weight:700;color:var(--bctext-2,#9aa3ab);padding-bottom:1px;} +.cr-timer-custom-row .cr-widget-btn {flex:none;padding:7px 13px;} .cr-timer-controls {display:flex;gap:8px;padding:12px 14px 14px;} /* Breathing Breaks: 4s in / 6s out, colour shifts green -> blue when exhaling */ .canvasrefined-breathing {background:linear-gradient(160deg, color-mix(in srgb, #6ea8ff 14%, var(--bcbackground-1,#fff)), var(--bcbackground-1,#fff));} @@ -634,59 +635,21 @@ .canvasrefined-breathing.is-running .cr-breath-phase-out {animation: cr-breath-out-label 10s ease-in-out infinite;} @keyframes cr-breath-in-label {0%{opacity:1;}38%{opacity:1;}44%{opacity:0;}100%{opacity:0;}} @keyframes cr-breath-out-label {0%{opacity:0;}38%{opacity:0;}44%{opacity:1;}94%{opacity:1;}100%{opacity:0;}} -/* Music: mini player + slide-in library switcher (visual only) */ -.cr-music-list-btn {display:inline-flex;align-items:center;gap:5px;border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-0,#2d3b45);border-radius:999px;padding:4px 10px;font:inherit;font-size:11px;font-weight:600;cursor:pointer;} -.cr-music-list-btn:hover {border-color:var(--bctext-2,#9aa3ab);} -.cr-music-caret {transition:transform .2s;} -.canvasrefined-music.is-drawer-open .cr-music-caret {transform:rotate(180deg);} -.canvasrefined-music {position:relative;min-height:320px;} -.canvasrefined-music.container-mode {min-height:320px;} -.cr-player {display:flex;flex-direction:column;gap:12px;padding:16px 14px 10px;flex:1;justify-content:center;} -.cr-player-thumb {display:none;position:relative;height:104px;border-radius:12px;overflow:hidden;background:linear-gradient(135deg, color-mix(in srgb, var(--cr-thumb-color,#0374b5) 82%, #000 8%), color-mix(in srgb, var(--cr-thumb-color,#0374b5) 38%, var(--bcbackground-1,#fff)));} -.cr-player-thumb-mark {position:absolute;right:10px;bottom:8px;color:rgba(255,255,255,.55);line-height:0;} -.canvasrefined-music.thumb-bg .cr-player-thumb {display:block;} -.canvasrefined-music.thumb-bg .cr-player-art {display:none;} -.canvasrefined-music.no-thumb .cr-player-thumb, -.canvasrefined-music.no-thumb .cr-player-art {display:none;} -.cr-player-body {display:flex;align-items:center;gap:12px;} -.cr-player-art {flex:none;width:56px;height:56px;border-radius:14px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb, var(--bclinks,#0374b5) 16%, transparent);color:var(--bclinks,#0374b5);} -.cr-player-info {min-width:0;flex:1;} -.cr-player-title {font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} -.cr-player-sub {font-size:11.5px;color:var(--bctext-2,#9aa3ab);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} -.cr-player-progress {height:5px;margin:0 14px;border-radius:999px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 12%, transparent);overflow:hidden;} -.cr-player-progress-fill {height:100%;width:0;background:var(--bclinks,#0374b5);border-radius:999px;} -.cr-player-times {display:flex;justify-content:space-between;padding:5px 16px 0;font-size:10.5px;color:var(--bctext-2,#9aa3ab);font-variant-numeric:tabular-nums;} -.cr-player-controls {display:flex;align-items:center;justify-content:center;gap:16px;padding:10px 14px 14px;} -.cr-player-btn {display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:transparent;color:var(--bctext-0,#2d3b45);cursor:pointer;} -.cr-player-btn:hover {background:color-mix(in srgb, var(--bctext-0,#2d3b45) 8%, transparent);} -.cr-player-play {width:46px;height:46px;background:var(--bclinks,#0374b5);color:#fff;} -.cr-player-play:hover {background:var(--bclinks,#0374b5);filter:brightness(1.08);} -/* Slide-in library drawer: platforms / playlists / songs */ -.cr-music-drawer {position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;background:var(--bcbackground-1,#fff);color:var(--bctext-0,#2d3b45);transform:translateX(102%);transition:transform .28s ease;visibility:hidden;box-shadow:-10px 0 26px rgba(0,0,0,.2);} -.canvasrefined-music.is-drawer-open .cr-music-drawer {transform:translateX(0);visibility:visible;} -.cr-drawer-head {display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--bcborders,#c7cdd1);} -.cr-drawer-title {font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);} -.cr-drawer-close {border:none;background:transparent;color:var(--bctext-2,#9aa3ab);font-size:20px;line-height:1;cursor:pointer;padding:0 5px;border-radius:6px;} -.cr-drawer-close:hover {color:var(--bctext-0,#2d3b45);background:color-mix(in srgb, var(--bctext-0,#2d3b45) 8%, transparent);} -.cr-drawer-tabs {display:flex;gap:6px;padding:8px 12px;border-bottom:1px solid var(--bcborders,#c7cdd1);} -.cr-drawer-tab {flex:1;border:1px solid transparent;background:transparent;color:var(--bctext-2,#9aa3ab);border-radius:999px;padding:6px 8px;font:inherit;font-size:11.5px;font-weight:600;cursor:pointer;} -.cr-drawer-tab:hover {color:var(--bctext-0,#2d3b45);} -.cr-drawer-tab.active {background:color-mix(in srgb, var(--bclinks,#0374b5) 16%, transparent);color:var(--bclinks,#0374b5);border-color:color-mix(in srgb, var(--bclinks,#0374b5) 45%, transparent);} -.cr-drawer-body {flex:1;overflow-y:auto;padding:8px 10px 12px;} -.cr-drawer-pane[hidden] {display:none;} -.cr-drawer-row {display:flex;align-items:center;gap:10px;padding:8px;border-radius:9px;} -.cr-drawer-row:hover {background:color-mix(in srgb, var(--bctext-0,#2d3b45) 6%, transparent);} -.cr-drawer-icon {flex:none;display:flex;line-height:0;} -.cr-drawer-icon-pl {opacity:.85;} -.cr-drawer-meta {display:flex;flex-direction:column;min-width:0;flex:1;} -.cr-drawer-name {font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} -.cr-drawer-sub {font-size:11px;color:var(--bctext-2,#9aa3ab);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} -.cr-drawer-chevron {flex:none;color:var(--bctext-2,#9aa3ab);font-size:16px;} -.cr-drawer-index,.cr-drawer-len {flex:none;font-size:11px;color:var(--bctext-2,#9aa3ab);font-variant-numeric:tabular-nums;} -.cr-drawer-index {width:16px;text-align:right;} -/* Daily Quote: looks only for now */ -.canvasrefined-quotes {background:linear-gradient(150deg, color-mix(in srgb, #b98cff 14%, var(--bcbackground-1,#fff)), var(--bcbackground-1,#fff));} -.cr-quote-body {padding:14px 16px 16px;display:flex;flex-direction:column;gap:6px;flex:1;justify-content:center;} -.cr-quote-mark {font-size:24px;line-height:.5;font-weight:800;color:color-mix(in srgb, var(--bclinks,#0374b5) 55%, transparent);} -.cr-quote-text {margin:0;font-size:15px;line-height:1.5;font-style:italic;color:var(--bctext-0,#2d3b45);} -.cr-quote-author {font-size:12px;font-weight:600;color:var(--bctext-2,#9aa3ab);align-self:flex-end;} +/* Breathing layout: circle + time stacked above the controls (card mode), and + circle + time on the left with the controls on the right in Top/Bottom rows. */ +.cr-breath-layout {display:flex;flex-direction:column;flex:1;min-height:0;} +.cr-breath-left {display:flex;flex-direction:column;align-items:center;justify-content:center;} +.cr-breath-right {display:flex;flex-direction:column;justify-content:center;} +.canvasrefined-breathing.container-mode .cr-breath-layout {flex-direction:row;align-items:center;gap:26px;} +.canvasrefined-breathing.container-mode .cr-breath-left {flex:0 0 40%;width:40%;max-width:40%;} +.canvasrefined-breathing.container-mode .cr-breath-right {flex:1;min-width:0;} +.canvasrefined-breathing.container-mode .cr-breath-stage {padding:8px 0;} +.canvasrefined-breathing.container-mode .cr-timer-presets {padding-top:0;} +.canvasrefined-breathing.container-mode .cr-timer-controls {padding-bottom:0;} +/* Daily Quote: text scales with the widget's own width (small grid cell vs. + full-width container row), using container-query units. */ +.canvasrefined-quotes {container-type:inline-size;background:linear-gradient(150deg, color-mix(in srgb, #b98cff 14%, var(--bcbackground-1,#fff)), var(--bcbackground-1,#fff));} +.cr-quote-body {padding:clamp(12px,4.5cqw,26px);display:flex;flex-direction:column;gap:clamp(4px,1.6cqw,12px);flex:1;justify-content:center;} +.cr-quote-mark {font-size:clamp(20px,11cqw,44px);line-height:.5;font-weight:800;color:color-mix(in srgb, var(--bclinks,#0374b5) 55%, transparent);} +.cr-quote-text {margin:0;font-size:clamp(14px,7.5cqw,26px);line-height:1.45;font-style:italic;color:var(--bctext-0,#2d3b45);} +.cr-quote-author {font-size:clamp(11px,3.6cqw,17px);font-weight:600;color:var(--bctext-2,#9aa3ab);align-self:flex-end;} diff --git a/css/popup.css b/css/popup.css index 180acf6..3d14d90 100644 --- a/css/popup.css +++ b/css/popup.css @@ -407,11 +407,21 @@ input[type="checkbox"]:checked {background: #8dd28d;} .widget-order-strip[data-area="cards_end"] .wos-item {flex: 1 1 0;} /* Widget sections (Widgets tab): two side-by-side columns; add future widgets as one more .option-container inside .widgets-grid. */ -.widgets-grid {display:grid;grid-template-columns:1fr 1fr;gap:8px;align-items:stretch;margin-top:8px;} -.widgets-grid .option-container {margin-top:0;box-sizing:border-box;} +.widgets-grid {display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px;align-items:stretch;margin-top:8px;} +.widgets-grid .option-container {margin-top:0;box-sizing:border-box;min-width:0;} /* Widget position dropdowns (Widgets tab) — mirror #todo_timeframe exactly: same font, padding, radius, colors and full-width stacked layout. */ .widget-pos {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;} .widget-pos option {background:#1e1e1e;color:#e2e2e2;} .widget-pos option:checked, .widget-pos option:hover {background:#2e2e2e;color:#fff;} +/* Inline rows: the label sits on the same line as its control. */ +.widget-inline-row {display:flex;align-items:center;gap:8px;} +.widget-inline-label {flex:none;} +.widget-inline-select {flex:1;min-width:0;margin-top:0;margin-bottom:0;} +/* Compact clock entry (preset lengths), matching the on-canvas widget. */ +.popup-time-input {display:flex;align-items:center;justify-content:center;gap:1px;flex:1;min-width:0;background:var(--inputbg);border-radius:7px;padding:4px 6px;} +.popup-time-input:focus-within {box-shadow:0 0 0 2px rgba(86,202,240,.35);} +.popup-tf {width:2.2ch;min-width:0;border:none;background:transparent;color:#e2e2e2;font:inherit;font-size:13px;font-weight:700;text-align:center;padding:0;border-radius:4px;} +.popup-tf:focus {outline:none;background:rgba(127,127,127,.28);} +.popup-tf-colon {font-size:13px;font-weight:700;color:#9aa3ab;} diff --git a/html/popup.html b/html/popup.html index ec1cbf2..f57ba9b 100644 --- a/html/popup.html +++ b/html/popup.html @@ -1135,6 +1135,14 @@ +
+

Top / Bottom height

+
+ + + px +
+
@@ -1146,15 +1154,20 @@
Dashboard Notes
-
- -
+
+ + + px +
@@ -1168,15 +1181,20 @@ GPA calculator
-
- -
+
+ + + px +
@@ -1197,37 +1215,48 @@
Focus Timer
-
- -
-
- Button 1 -
- h - m - s +
+ + + px +
+
+ Button 1 +
-
- Button 2 -
- h - m - s +
+ Button 2 +
-
- Button 3 -
- h - m - s +
+ Button 3 +
@@ -1242,82 +1271,46 @@
Breathing Breaks
-
- -
-
- Button 1 -
- h - m - s +
+ + + px +
+
+ Button 1 +
-
- Button 2 -
- h - m - s +
+ Button 2 +
-
- Button 3 -
- h - m - s +
+ Button 3 +
-
-
- - -
-
-
-
Music -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
@@ -1328,15 +1321,20 @@
Daily Quotes
-
- -
+
+ + + px +
diff --git a/js/background.js b/js/background.js index 988f034..7663e3a 100644 --- a/js/background.js +++ b/js/background.js @@ -47,13 +47,6 @@ chrome.runtime.onInstalled.addListener(function () { "focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 }, "focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 }, "focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 }, - "music": false, - "music_position": "cards_end", - "music_spotify_url": "", - "music_apple_url": "", - "music_youtube_url": "", - "music_thumb_bg": true, - "music_no_thumb": false, "breathing": false, "breathing_position": "cards_end", "breathing_preset_1": { "h": 0, "m": 3, "s": 0 }, @@ -61,6 +54,12 @@ chrome.runtime.onInstalled.addListener(function () { "breathing_preset_3": { "h": 0, "m": 10, "s": 0 }, "daily_quotes": false, "daily_quotes_position": "top", + "widget_row_height": 0, + "dashboard_notes_height": 0, + "gpa_calc_height": 0, + "focus_timer_height": 0, + "breathing_height": 0, + "daily_quotes_height": 0, "better_todo": true, "todo_hr24": false, "todo_separate_scrollbar": false, @@ -200,6 +199,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { getNasaBackground().then(sendResponse); return true; } + if (message?.type === "getDailyQuote") { + getDailyQuote().then(sendResponse); + return true; + } }); let nasaRequestQueue = Promise.resolve(); @@ -268,3 +271,49 @@ async function callNasaApi(dateStr) { return await response.json().catch(() => null); } + +// --- Daily quote (stoic.tekloon.net) --------------------------------------- +// Cached in storage.local and refreshed at most once per local calendar day. +// Fetched from the extension worker (host_permissions) because the endpoint +// does not send CORS headers, so a page-context fetch would be blocked. +function dailyQuoteTodayKey() { + const d = new Date(); + return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); +} + +let dailyQuoteInFlight = null; +async function getDailyQuote() { + const today = dailyQuoteTodayKey(); + let cached = null; + try { + const st = await chrome.storage.local.get("daily_quote"); + cached = st && st.daily_quote; + } catch (e) { /* ignore storage issues */ } + + // Once-a-day: reuse today's cached quote without hitting the network. + if (cached && cached.date === today && cached.quote) return cached; + if (dailyQuoteInFlight) return dailyQuoteInFlight; + + const request = (async () => { + try { + const response = await fetch("https://stoic.tekloon.net/stoic-quote", { cache: "no-store" }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const json = await response.json(); + const data = (json && json.data) || {}; + const quote = String(data.quote || "").replace(/\s+/g, " ").trim(); + const author = String(data.author || "").trim(); + if (!quote) throw new Error("empty quote"); + const entry = { date: today, quote, author }; + await chrome.storage.local.set({ daily_quote: entry }); + return entry; + } catch (e) { + console.warn("[CanvasRefined] Daily quote fetch failed:", e); + // Fall back to the previous day's quote rather than losing it. + return cached || null; + } finally { + dailyQuoteInFlight = null; + } + })(); + dailyQuoteInFlight = request; + return request; +} diff --git a/js/content.js b/js/content.js index 0cf44a1..7e75976 100644 --- a/js/content.js +++ b/js/content.js @@ -1024,26 +1024,20 @@ function applyOptionsChanges(changes) { case "dashboard_notes_text": case "dashboard_notes_mode": case "dashboard_notes_position": + case "dashboard_notes_height": loadDashboardNotes(); break; case "focus_timer": case "focus_timer_position": + case "focus_timer_height": case "focus_timer_preset_1": case "focus_timer_preset_2": case "focus_timer_preset_3": loadFocusTimer(); break; - case "music": - case "music_position": - case "music_spotify_url": - case "music_apple_url": - case "music_youtube_url": - case "music_thumb_bg": - case "music_no_thumb": - loadMusicWidget(); - break; case "breathing": case "breathing_position": + case "breathing_height": case "breathing_preset_1": case "breathing_preset_2": case "breathing_preset_3": @@ -1051,8 +1045,15 @@ function applyOptionsChanges(changes) { break; case "daily_quotes": case "daily_quotes_position": + case "daily_quotes_height": loadDailyQuotesWidget(); break; + case "daily_quote": + // Background refreshed the cached quote for today. + document.querySelectorAll(".canvasrefined-quotes").forEach(el => { + renderDailyQuote(el, changes["daily_quote"].newValue); + }); + break; case "widget_order": ["top", "below"].forEach(area => { const row = document.querySelector(".canvasrefined-widget-row[data-area='" + area + "']"); @@ -1067,6 +1068,11 @@ function applyOptionsChanges(changes) { if (options.gpa_calc === true && current_page === "/") setupGPACalc(); loadAllSimpleWidgets(); break; + case "widget_row_height": + document.querySelectorAll(".canvasrefined-widget-row").forEach(row => { + applyWidgetRowLayout(row, row.dataset.area); + }); + break; case "dashboard_grades": case "grade_hover": case "card_letter": @@ -1187,6 +1193,7 @@ function applyOptionsChanges(changes) { case "gpa_calc": case "gpa_calc_prepend": case "gpa_calc_position": + case "gpa_calc_height": case "gpa_calc_weighted": case "gpa_calc_cumulative": if (!grades) getGrades(); @@ -1974,6 +1981,11 @@ function checkDashboardReady() { const observer = new MutationObserver(callback); observer.observe(document.documentElement, { childList: true, subtree: true }); + // On a fast load (or cached dashboard) Canvas can finish rendering before + // this observer is attached, so no structural mutation would ever trigger + // the setup pass and widgets (notably the GPA card) would only appear on + // the next unrelated DOM change. Kick one pass immediately. + callback([{ type: "childList" }]); } function recieveMessage(request, sender, sendResponse) { @@ -7644,6 +7656,9 @@ GPA calculator */ function calculateGPA2() { + // The shell renders before the grades data arrives; nothing to compute until + // the cumulative row exists (populated by setupGPACalc). + if (!document.querySelector("#canvasrefined-cumulative-gpa")) return; let qualityPoints = 0, numCredits = 0, weightedQualityPoints = 0, cumulativePoints = 0, cumulativeCredits = 0; document.querySelectorAll('.canvasrefined-gpa-course').forEach(course => { course.dataset.gradeLetter = "--"; @@ -7840,8 +7855,14 @@ function placeGPAElements(dashboardContainer, container, container2) { // While the calculator is hosted in the open modal, leave it alone — a // dashboard setup pass must not yank it out from under the user. if (container.closest && container.closest("#canvasrefined-gpa-modal")) return; + // Disabled: make sure the pair isn't still reserving a slot in a shared row. + if (options.gpa_calc !== true) { + releaseWidgetFromRow("gpa_calc", [container2, container], dashboardContainer); + return; + } const pos = gpaCalcPosition(); const containerMode = pos === "top" || pos === "below"; + crApplyCardHeight("gpa_calc", container2, !containerMode); container.classList.toggle("container-mode", containerMode); container2.classList.toggle("container-mode", containerMode); const parent = dashboardContainer.parentNode; @@ -7887,10 +7908,10 @@ function placeGPAElements(dashboardContainer, container, container2) { // Every dashboard widget known to the placement system. Kept in one place so // the ordering/filtering helpers don't each hardcode their own list. -const CR_WIDGET_IDS = ["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes"]; +const CR_WIDGET_IDS = ["dashboard_notes", "gpa_calc", "focus_timer", "breathing", "daily_quotes"]; // Widgets rendered by the generic card loader (single root element, no GPA-style // paired elements). -const CR_SIMPLE_WIDGET_IDS = ["focus_timer", "music", "breathing", "daily_quotes"]; +const CR_SIMPLE_WIDGET_IDS = ["focus_timer", "breathing", "daily_quotes"]; function isCRWidget(id) { return CR_WIDGET_IDS.indexOf(id) !== -1; } // Shared full-width container ("top"/"below" positions): widgets live in a @@ -7930,7 +7951,7 @@ function applyCardWidgetOrder(grid) { if (!grid) return; const saved = cardWidgetOrder(); const units = []; - // Simple card widgets (focus timer, music, breathing, quotes) are a + // Simple card widgets (focus timer, breathing, quotes) are a // single element each, so they join the ordering straight from the DOM. grid.querySelectorAll(":scope > .canvasrefined-widget-card[data-cr-widget]").forEach(el => { if (!el.classList.contains("in-cards")) return; @@ -8010,6 +8031,11 @@ function ensureWidgetRow(area, container) { function applyWidgetRowLayout(row, area) { const slots = [...row.querySelectorAll(":scope > .canvasrefined-widget-slot")]; if (!slots.length) { row.remove(); return; } + // Shared Top/Bottom row height (px; 0 = auto). Applied to the row so every + // widget in it matches; cards in container-mode stretch to fill it. + const rowHeight = parseInt(options.widget_row_height, 10); + if (Number.isFinite(rowHeight) && rowHeight > 0) row.style.height = rowHeight + "px"; + else row.style.height = ""; const entries = containerWidgetOrder(area); const byId = {}; slots.forEach(slot => { byId[slot.dataset.widget] = slot; }); @@ -8027,24 +8053,19 @@ function applyWidgetRowLayout(row, area) { row.querySelectorAll(":scope > .canvasrefined-widget-divider").forEach(d => d.remove()); // Sizing: saved % values are normalized across the widgets actually in - // the row, so when one widget leaves (or a new one joins) the rest - // instantly redistribute to fill the freed space. flex-grow weights do - // that redistribution automatically in CSS. - const present = entries.filter(e => order.includes(e.id)); - const defined = present.filter(e => Number.isFinite(e.size) && e.size > 0); - let weights; - if (defined.length === 0) { - weights = order.map(() => 1); - } else { - const totalDefined = defined.reduce((a, e) => a + e.size, 0); - const undefinedIds = order.filter(id => !defined.some(e => e.id === id)); - const byIdEntry = Object.fromEntries(present.map(e => [e.id, e])); - weights = order.map(id => { - const e = byIdEntry[id]; - if (e && Number.isFinite(e.size) && e.size > 0) return e.size / totalDefined; - return undefinedIds.length ? Math.max(0, 100 - totalDefined) / undefinedIds.length / 100 : 0.01; - }); - } + // the row. A widget with no saved size (e.g. just enabled) borrows the + // average of the sized ones, so the weights ALWAYS sum to 1 — otherwise a + // mix of sized + new widgets could exceed 100% or collapse a slot to 0. + const byIdEntry = Object.fromEntries(entries.map(e => [e.id, e])); + const raw = order.map(id => { + const e = byIdEntry[id]; + return e && Number.isFinite(e.size) && e.size > 0 ? e.size : null; + }); + const known = raw.filter(v => v !== null); + const fallback = known.length ? known.reduce((a, b) => a + b, 0) / known.length : 1; + const rawFilled = raw.map(v => v === null ? fallback : v); + const totalWeight = rawFilled.reduce((a, b) => a + b, 0) || 1; + const weights = rawFilled.map(v => v / totalWeight); order.forEach((id, i) => { byId[id].dataset.sizePct = weights[i] * 100; byId[id].style.flex = weights[i] + " 1 0%"; @@ -8260,66 +8281,87 @@ function openGpaModal(dashboardContainer, container, container2) { modal.addEventListener("mousedown", (e) => { if (e.target === modal) close(); }); } +// Bounded retry while Canvas finishes rendering the dashboard container, so +// the GPA card doesn't wait for the next MutationObserver pass. +let gpaSetupRetryTimer = null; +let gpaSetupAttempts = 0; function setupGPACalc() { if (current_page !== "/" && current_page !== "") return; try { + const sortableContainer = document.querySelector(".ic-DashboardCard__box__container"); + const dashboardContainer = sortableContainer || document.querySelector("#DashboardCard_Container"); + if (!dashboardContainer) { + // The dashboard DOM isn't there yet (Canvas renders it async). + // Retry briefly instead of silently giving up, otherwise the GPA + // card waits for the next mutation pass to show up. + if (!gpaSetupRetryTimer && gpaSetupAttempts < 20) { + gpaSetupAttempts++; + gpaSetupRetryTimer = setTimeout(() => { + gpaSetupRetryTimer = null; + setupGPACalc(); + }, 150); + } + return; + } + gpaSetupAttempts = 0; + + let container2 = document.querySelector(".canvasrefined-gpa-card"); + let container = document.querySelector(".canvasrefined-gpa"); + const alreadyRendered = container2?.dataset?.canvasrefinedGpaRendered === "true" && container?.dataset?.canvasrefinedGpaRendered === "true"; + + if (!container2) { + container2 = document.createElement("div"); + container2.className = "canvasrefined-gpa-card"; + } + if (!container) { + container = document.createElement("div"); + container.className = "canvasrefined-gpa"; + } + + container2.style.display = options.gpa_calc === true ? "inline-block" : "none"; + + if (!alreadyRendered) { + // Render the shell immediately (with "--" placeholders) so the GPA + // card joins the dashboard at the same time as every other widget. + // The numbers and course rows are filled in when the grades API + // resolves, which is the only part that actually needs the data. + container2.innerHTML = `

GPA

--

Current

--

Weighted

--

Cumulative

`; + let editBtn = makeElement("button", container2, { "className": "canvasrefined-gpa-edit-btn", "textContent": "Edit Calculator" }); + + container.innerHTML = '

GPA Calculator

'; + + editBtn.addEventListener("click", () => openGpaModal(dashboardContainer, container, container2)); + + container.style.display = "none"; + + container2.dataset.canvasrefinedGpaRendered = "true"; + container.dataset.canvasrefinedGpaRendered = "true"; + } else { + const weighted = container2.querySelector("#canvasrefined-gpa-weighted")?.parentElement; + const cumulative = container2.querySelector("#canvasrefined-gpa-cumulative")?.parentElement; + if (weighted) weighted.style.display = options.gpa_calc_weighted ? "block" : "none"; + if (cumulative) cumulative.style.display = options.gpa_calc_cumulative ? "block" : "none"; + } + + placeGPAElements(dashboardContainer, container, container2); + + try { + if (sortableContainer && window.jQuery && window.jQuery.fn && window.jQuery.fn.sortable) { + window.jQuery(sortableContainer).sortable('refresh'); + } + } catch (e) {} + + // Populate course rows + numbers once the grades request resolves. grades?.then(result => { - - const sortableContainer = document.querySelector(".ic-DashboardCard__box__container"); - const dashboardContainer = sortableContainer || document.querySelector("#DashboardCard_Container"); - if (!dashboardContainer) return; - - let container2 = document.querySelector(".canvasrefined-gpa-card"); - let container = document.querySelector(".canvasrefined-gpa"); - const alreadyRendered = container2?.dataset?.canvasrefinedGpaRendered === "true" && container?.dataset?.canvasrefinedGpaRendered === "true"; - - if (!container2) { - container2 = document.createElement("div"); - container2.className = "canvasrefined-gpa-card"; - } - if (!container) { - container = document.createElement("div"); - container.className = "canvasrefined-gpa"; - } - - container2.style.display = options.gpa_calc === true ? "inline-block" : "none"; - - if (!alreadyRendered) { - container2.innerHTML = `

GPA

Current

Weighted

Cumulative

`; - let editBtn = makeElement("button", container2, { "className": "canvasrefined-gpa-edit-btn", "textContent": "Edit Calculator" }); - - container.innerHTML = '

GPA Calculator

'; - - placeGPAElements(dashboardContainer, container, container2); - editBtn.textContent = "Edit Calculator"; - editBtn.addEventListener("click", () => openGpaModal(dashboardContainer, container, container2)); - - let location = document.querySelector(".canvasrefined-gpa-courses"); - if (!location) return; - - let cumulative = createGPACalcCourse(location, { "id": "cumulative", "enrollments": [{ "has_grading_periods": true, "current_period_computed_current_score": 0 }] }); - cumulative.id = "canvasrefined-cumulative-gpa"; + const location = container.querySelector(".canvasrefined-gpa-courses"); + if (!location || !container.isConnected) return; + if (container.dataset.canvasrefinedGpaPopulated !== "true") { + location.textContent = ""; + const cumulative = createGPACalcCourse(location, { "id": "cumulative", "enrollments": [{ "has_grading_periods": true, "current_period_computed_current_score": 0 }] }); + if (cumulative) cumulative.id = "canvasrefined-cumulative-gpa"; result.forEach(course => createGPACalcCourse(location, course)); - - container.style.display = "none"; - - container2.dataset.canvasrefinedGpaRendered = "true"; - container.dataset.canvasrefinedGpaRendered = "true"; - } else { - const weighted = container2.querySelector("#canvasrefined-gpa-weighted")?.parentElement; - const cumulative = container2.querySelector("#canvasrefined-gpa-cumulative")?.parentElement; - if (weighted) weighted.style.display = options.gpa_calc_weighted ? "block" : "none"; - if (cumulative) cumulative.style.display = options.gpa_calc_cumulative ? "block" : "none"; - - placeGPAElements(dashboardContainer, container, container2); + container.dataset.canvasrefinedGpaPopulated = "true"; } - - try { - if (sortableContainer && window.jQuery && window.jQuery.fn && window.jQuery.fn.sortable) { - window.jQuery(sortableContainer).sortable('refresh'); - } - } catch (e) {} - calculateGPA2(); }).catch(e => logError(e)); } catch (e) { @@ -8612,6 +8654,7 @@ function wireDashboardNotes(notes) { // below — full-width container below the grid, with some margin function placeDashboardNotes(notes, container) { const pos = options.dashboard_notes_position || "top"; + crApplyCardHeight("dashboard_notes", notes, pos === "cards_start" || pos === "cards_end"); if (pos === "top" || pos === "below") { notes.classList.remove("in-cards"); const slot = notes.closest && notes.closest(".canvasrefined-widget-slot"); @@ -8676,12 +8719,16 @@ function loadDashboardNotes() { placeDashboardNotes(notes, container); } else { let notes = document.querySelector('.canvasrefined-dashboard-notes'); - if (notes) notes.style.display = "none"; + if (notes) { + notes.style.display = "none"; + // Give its width back to the other widgets sharing the row. + releaseWidgetFromRow("dashboard_notes", [notes]); + } } } /* -Generic dashboard widgets (Focus Timer, Music, Breathing Breaks, Daily Quote) +Generic dashboard widgets (Focus Timer, Breathing Breaks, Daily Quote) These share the placement/ordering machinery built for Dashboard Notes and the GPA calculator, but render from one element each (see CR_SIMPLE_WIDGET_IDS). @@ -8691,7 +8738,6 @@ const CR_WIDGET_DEFAULT_POSITION = { dashboard_notes: "top", gpa_calc: "cards_end", focus_timer: "cards_end", - music: "cards_end", breathing: "cards_end", daily_quotes: "top", }; @@ -8702,11 +8748,27 @@ function crWidgetPosition(id) { return CR_WIDGET_DEFAULT_POSITION[id] || "cards_end"; } +// Per-widget height used when the widget sits before/after the course cards +// (px; 0 = auto). Cleared in container (Top/Bottom) mode so the shared row +// height controls that layout instead. +function crApplyCardHeight(id, el, inCards) { + if (!el) return; + const h = parseInt(options[id + "_height"], 10); + if (inCards && Number.isFinite(h) && h > 0) { + el.style.height = h + "px"; + el.style.minHeight = h + "px"; + } else { + el.style.height = ""; + el.style.minHeight = ""; + } +} + // Move a single-element widget to its configured spot, mirroring the Notes/GPA // placement rules: top/below join the shared full-width row, cards_* sit in the // course-card grid before/after the cards. function placeSimpleWidget(id, el, container) { const pos = crWidgetPosition(id); + crApplyCardHeight(id, el, pos === "cards_start" || pos === "cards_end"); if (pos === "top" || pos === "below") { el.classList.remove("in-cards"); el.classList.add("container-mode"); @@ -8736,7 +8798,11 @@ function placeSimpleWidget(id, el, container) { function crLoadWidget(id, spec) { const existing = document.querySelector("." + spec.className); if (options[id] !== true) { - if (existing) existing.style.display = "none"; + if (existing) { + existing.style.display = "none"; + // Give its width back to the other widgets sharing the row. + releaseWidgetFromRow(id, [existing]); + } return; } const container = document.querySelector("#DashboardCard_Container"); @@ -8866,16 +8932,26 @@ function crTimerHideCustom(el) { const row = el.querySelector(".cr-timer-custom-row"); if (row) row.hidden = true; } -// Read the h/m/s custom fields as a whole number of seconds. +// Read the compact h:mm:ss custom fields as a whole number of seconds. function crTimerCustomSeconds(el) { const num = (sel) => { const v = parseInt((el.querySelector(sel) || {}).value, 10); return Number.isFinite(v) && v > 0 ? v : 0; }; - const h = Math.min(99, num(".cr-tf-h")); - const m = Math.min(59, num(".cr-tf-m")); - const s = Math.min(59, num(".cr-tf-s")); - return h * 3600 + m * 60 + s; + return Math.min(99, num(".cr-tf-h")) * 3600 + + Math.min(59, num(".cr-tf-m")) * 60 + + Math.min(59, num(".cr-tf-s")); +} +// Seed the compact fields from the current timer length, two digits each. +function crTimerFillCustom(el, totalSec) { + const secs = Math.max(0, Math.round(totalSec || 0)); + const put = (sel, v) => { + const inp = el.querySelector(sel); + if (inp) inp.value = String(v).padStart(2, "0"); + }; + put(".cr-tf-h", Math.min(99, Math.floor(secs / 3600))); + put(".cr-tf-m", Math.floor((secs % 3600) / 60)); + put(".cr-tf-s", secs % 60); } // Listeners are wired once (update passes only refresh config labels). function crTimerWireControls(id, el) { @@ -8887,29 +8963,49 @@ function crTimerWireControls(id, el) { }); }); - // Custom: reveal inline hours/minutes/seconds fields. + // Custom: a compact h:mm:ss entry. Click a segment to edit it, Tab / arrows + // move between segments, typing a second digit auto-advances, Enter sets. const customBtn = el.querySelector(".cr-timer-custom"); const row = el.querySelector(".cr-timer-custom-row"); + const fields = [...el.querySelectorAll(".cr-timer-custom-row .cr-tf")]; + const focusField = (i) => { + const inp = fields[i]; + if (inp) { inp.focus(); inp.select(); } + }; const applyCustom = () => { const sec = crTimerCustomSeconds(el); if (sec <= 0) { - const mInput = el.querySelector(".cr-tf-m"); - if (mInput) { mInput.focus(); mInput.select(); } + focusField(fields.length - 1); return; } crTimerSet(id, el, sec); crTimerHideCustom(el); }; - if (customBtn && row) customBtn.addEventListener("click", () => { - if (!row.hidden) { crTimerHideCustom(el); return; } + const openCustom = () => { + crTimerFillCustom(el, crTimer(id).total); row.hidden = false; - const mInput = el.querySelector(".cr-tf-m"); - if (mInput) { mInput.focus(); mInput.select(); } + // Start on minutes when present, otherwise the first field. + const mi = fields.findIndex(f => f.classList.contains("cr-tf-m")); + focusField(mi === -1 ? 0 : mi); + }; + if (customBtn && row) customBtn.addEventListener("click", () => { + if (row.hidden) openCustom(); + else crTimerHideCustom(el); + }); + fields.forEach((inp, i) => { + inp.addEventListener("focus", () => inp.select()); + inp.addEventListener("input", () => { + inp.value = inp.value.replace(/[^0-9]/g, "").slice(0, 2); + if (inp.value.length >= 2 && i < fields.length - 1) focusField(i + 1); + }); + inp.addEventListener("keydown", e => { + if (e.key === "Enter") { e.preventDefault(); applyCustom(); } + else if (e.key === "Escape") { e.preventDefault(); crTimerHideCustom(el); } + else if ((e.key === "ArrowRight" || e.key === "Tab" && !e.shiftKey) && i < fields.length - 1) { e.preventDefault(); focusField(i + 1); } + else if ((e.key === "ArrowLeft" || e.key === "Tab" && e.shiftKey) && i > 0) { e.preventDefault(); focusField(i - 1); } + else if (e.key === "Backspace" && inp.value === "" && i > 0) { e.preventDefault(); focusField(i - 1); } + }); }); - el.querySelectorAll(".cr-timer-custom-row .cr-tf").forEach(inp => inp.addEventListener("keydown", e => { - if (e.key === "Enter") { e.preventDefault(); applyCustom(); } - else if (e.key === "Escape") { e.preventDefault(); crTimerHideCustom(el); } - })); const go = el.querySelector(".cr-timer-custom-go"); if (go) go.addEventListener("click", applyCustom); @@ -8967,10 +9063,12 @@ const FOCUS_TIMER_HTML = `