From b05f9b58c839c14cc9a36bb52b79e13f396b8f33 Mon Sep 17 00:00:00 2001 From: Guy Sandler Date: Fri, 16 Oct 2026 20:36:30 -0700 Subject: [PATCH] base for more widgets --- _locales/en/messages.json | 24 ++ css/content.css | 144 ++++++++- html/popup.html | 152 ++++++++++ js/background.js | 19 ++ js/content.js | 612 +++++++++++++++++++++++++++++++++++++- js/popup.js | 138 ++++++++- 6 files changed, 1079 insertions(+), 10 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cbd0ecf..23cba9f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -140,6 +140,30 @@ "widget_short_gpa": { "message": "GPA" }, + "focus_timer": { + "message": "Focus Timer" + }, + "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" + }, + "widget_short_quotes": { + "message": "Quotes" + }, "grade_settings": { "message": "Grade Settings" }, diff --git a/css/content.css b/css/content.css index 51d8d08..36f66b7 100644 --- a/css/content.css +++ b/css/content.css @@ -70,7 +70,7 @@ .canvasrefined-course-percent, .canvasrefined-course-credit {border: 1px solid #ccc; color: var(--ic-brand-font-color-dark)} .canvasrefined-course-percent, .canvasrefined-course-weight, .canvasrefined-course-credit { - height: 22px; text-align: center;border-radius: 3px;width: 50px; font-size: 13px; padding: 0; + height: 22px; text-align: center;border-radius: 3px;width: 150px; font-size: 13px; padding: 0; } .canvasrefined-course-weight { @@ -548,3 +548,145 @@ .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 ===== + 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. */ +.canvasrefined-widget-card { + width: 262px; + min-height: 200px; + box-sizing: border-box; + background: var(--bcbackground-1,#fff); + color: var(--bctext-0,#2d3b45); + border-radius: 4px; + box-shadow: 0 2px 5px rgba(0,0,0,.3); + display: flex; + flex-direction: column; + overflow: hidden; + font-size: 13px; +} +.canvasrefined-widget-card.in-cards { + margin: 36px 0 0 36px; + display: inline-flex; + vertical-align: top; +} +.canvasrefined-widget-card.container-mode { + width: 100%; + height: 100%; + margin: 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);} +.cr-widget-title {font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);} +.cr-widget-btn {flex:1;border:none;border-radius:8px;padding:9px 12px;font:inherit;font-weight:600;font-size:13px;cursor:pointer;background:var(--ic-brand-primary,#0374b5);color:#fff;} +.cr-widget-btn:hover {filter:brightness(1.07);} +.cr-widget-btn-ghost {background:transparent;color:var(--bctext-0,#2d3b45);border:1px solid var(--bcborders,#c7cdd1);} +/* Timer engine (Focus Timer + Breathing Breaks) */ +.cr-time-box {position:relative;margin:14px 14px 0;border-radius:12px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 6%, transparent);border:1px solid var(--bcborders,#c7cdd1);overflow:hidden;} +/* The whole time rectangle is the progress bar, tinted with the dark-mode + link color so it matches the active theme in both light and dark mode. */ +.cr-time-box .cr-timer-progress-bar {position:absolute;left:0;top:0;bottom:0;width:0;background:color-mix(in srgb, var(--bclinks,#0374b5) 32%, transparent);border-right:2px solid color-mix(in srgb, var(--bclinks,#0374b5) 80%, transparent);} +.cr-timer-time {position:relative;z-index:1;font-size:44px;font-weight:700;text-align:center;letter-spacing:.02em;font-variant-numeric:tabular-nums;padding:22px 14px;line-height:1;color:var(--bctext-0,#2d3b45);} +.cr-breath-display {padding:12px 14px 6px;} +.cr-timer-presets {display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:12px 14px 0;} +.cr-timer-preset {border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-0,#2d3b45);border-radius:8px;padding:8px;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;} +.cr-timer-preset:hover {border-color:var(--bctext-2,#9aa3ab);} +.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;} +.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));} +.cr-breath-stage {display:flex;align-items:center;justify-content:center;padding:20px 14px 6px;} +.cr-breath-circle {position:relative;width:112px;height:112px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 40%, #74e3c4, #2fb595);box-shadow:0 0 18px 2px rgba(65,209,175,.4);} +.cr-breath-circle::before {content:"";position:absolute;inset:0;border-radius:50%;background:radial-gradient(circle at 50% 40%, #93a8ff, #536ee0);opacity:0;box-shadow:0 0 26px 5px rgba(101,124,255,.45);} +.canvasrefined-breathing.is-running .cr-breath-circle {animation: cr-breath-pulse 10s ease-in-out infinite, cr-breath-glow 10s ease-in-out infinite;} +.canvasrefined-breathing.is-running .cr-breath-circle::before {animation: cr-breath-tint 10s ease-in-out infinite;} +@keyframes cr-breath-pulse { + 0% {transform:scale(.68);} + 40% {transform:scale(1);} + 100% {transform:scale(.68);} +} +@keyframes cr-breath-glow { + 0% {box-shadow:0 0 16px 2px rgba(65,209,175,.35);} + 40% {box-shadow:0 0 36px 9px rgba(65,209,175,.65);} + 100% {box-shadow:0 0 16px 2px rgba(65,209,175,.35);} +} +@keyframes cr-breath-tint { + 0%,38% {opacity:0;} + 60% {opacity:.95;} + 100% {opacity:0;} +} +.cr-breath-phase,.cr-breath-idle {position:absolute;z-index:1;font-size:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.32);} +.cr-breath-phase {opacity:0;} +.cr-breath-idle {opacity:1;} +.canvasrefined-breathing.is-running .cr-breath-idle {opacity:0;} +.canvasrefined-breathing.is-running .cr-breath-phase-in {animation: cr-breath-in-label 10s ease-in-out infinite;} +.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;} diff --git a/html/popup.html b/html/popup.html index 7c68f0c..22316c4 100644 --- a/html/popup.html +++ b/html/popup.html @@ -1187,6 +1187,158 @@ +
+
+ + +
+
+
+
Focus Timer +
+
+
+ + +
+
+ Button 1 +
+ h + m + s +
+
+
+ Button 2 +
+ h + m + s +
+
+
+ Button 3 +
+ h + m + s +
+
+
+
+
+
+ + +
+
+
+
Breathing Breaks +
+
+
+ + +
+
+ Button 1 +
+ h + m + s +
+
+
+ Button 2 +
+ h + m + s +
+
+
+ Button 3 +
+ h + m + s +
+
+
+
+
+
+ + +
+
+
+
Music +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
Daily Quotes +
+
+
+ + +
+
+
diff --git a/js/background.js b/js/background.js index bd64175..988f034 100644 --- a/js/background.js +++ b/js/background.js @@ -42,6 +42,25 @@ chrome.runtime.onInstalled.addListener(function () { "dashboard_notes_mode": "edit", "dashboard_notes_position": "top", "widget_order": {}, + "focus_timer": false, + "focus_timer_position": "cards_end", + "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 }, + "breathing_preset_2": { "h": 0, "m": 5, "s": 0 }, + "breathing_preset_3": { "h": 0, "m": 10, "s": 0 }, + "daily_quotes": false, + "daily_quotes_position": "top", "better_todo": true, "todo_hr24": false, "todo_separate_scrollbar": false, diff --git a/js/content.js b/js/content.js index 55e735a..0cf44a1 100644 --- a/js/content.js +++ b/js/content.js @@ -1026,6 +1026,33 @@ function applyOptionsChanges(changes) { case "dashboard_notes_position": loadDashboardNotes(); break; + case "focus_timer": + case "focus_timer_position": + 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_preset_1": + case "breathing_preset_2": + case "breathing_preset_3": + loadBreathingWidget(); + break; + case "daily_quotes": + case "daily_quotes_position": + loadDailyQuotesWidget(); + break; case "widget_order": ["top", "below"].forEach(area => { const row = document.querySelector(".canvasrefined-widget-row[data-area='" + area + "']"); @@ -1038,6 +1065,7 @@ function applyOptionsChanges(changes) { // behind would keep the old layout until reload. if (options.dashboard_notes === true) loadDashboardNotes(); if (options.gpa_calc === true && current_page === "/") setupGPACalc(); + loadAllSimpleWidgets(); break; case "dashboard_grades": case "grade_hover": @@ -1917,6 +1945,7 @@ function checkDashboardReady() { insertGrades(); loadDashboardNotes(); setupGPACalc(); + loadAllSimpleWidgets(); showUpdateMsg(); createNasaInfoOverlay(); } @@ -7856,6 +7885,14 @@ 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"]; +// 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"]; +function isCRWidget(id) { return CR_WIDGET_IDS.indexOf(id) !== -1; } + // Shared full-width container ("top"/"below" positions): widgets live in a // flex row; each widget's slot width is the % chosen in the Widgets tab. function containerWidgetOrder(area) { @@ -7868,7 +7905,7 @@ function containerWidgetOrder(area) { return { id: e.id, size: Number.isFinite(n) && n > 0 ? n : null }; } return null; - }).filter(e => e && (e.id === "dashboard_notes" || e.id === "gpa_calc")); + }).filter(e => e && isCRWidget(e.id)); } // Card-grid order ("Before cards"/"After cards" positions): saved order of @@ -7881,7 +7918,7 @@ function cardWidgetOrder() { if (typeof e === "string") return e; if (e && typeof e === "object" && e.id) return e.id; return null; - }).filter(id => id === "dashboard_notes" || id === "gpa_calc"); + }).filter(id => isCRWidget(id)); } // Normalize the relative order of card-shaped widgets inside the card grid. @@ -7893,6 +7930,14 @@ function applyCardWidgetOrder(grid) { if (!grid) return; const saved = cardWidgetOrder(); const units = []; + // Simple card widgets (focus timer, music, 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; + const id = el.dataset.crWidget; + if (CR_SIMPLE_WIDGET_IDS.indexOf(id) === -1) return; + units.push({ id, anchor: (options[id + "_position"] === "cards_start") ? "start" : "end", els: [el] }); + }); const notes = grid.querySelector(":scope > .canvasrefined-dashboard-notes"); if (notes && notes.classList.contains("in-cards")) { units.push({ @@ -8635,6 +8680,569 @@ function loadDashboardNotes() { } } +/* +Generic dashboard widgets (Focus Timer, Music, 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). +*/ + +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", +}; + +function crWidgetPosition(id) { + const v = options[id + "_position"]; + if (v === "top" || v === "below" || v === "cards_start" || v === "cards_end") return v; + return CR_WIDGET_DEFAULT_POSITION[id] || "cards_end"; +} + +// 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); + if (pos === "top" || pos === "below") { + el.classList.remove("in-cards"); + el.classList.add("container-mode"); + const slot = el.closest && el.closest(".canvasrefined-widget-slot"); + const row = slot && slot.parentNode; + if (el.dataset.crPos === pos && row && row.dataset.area === pos && slot.dataset.widget === id) return; + placeWidgetInRow(id, pos, [el], container); + } else { + el.classList.add("in-cards"); + el.classList.remove("container-mode"); + const grid = document.querySelector(".ic-DashboardCard__box__container") || container; + if (el.dataset.crPos === pos && el.parentNode === grid) return; + releaseWidgetFromRow(id, [el], container); + if (pos === "cards_start") grid.prepend(el); + else grid.appendChild(el); + applyCardWidgetOrder(grid); + try { + if (grid !== container && window.jQuery && window.jQuery.fn && window.jQuery.fn.sortable) { + window.jQuery(grid).sortable('refresh'); + } + } catch (e) {} + } + el.dataset.crPos = pos; +} + +// Create (once), show/hide and place one generic widget from its spec. +function crLoadWidget(id, spec) { + const existing = document.querySelector("." + spec.className); + if (options[id] !== true) { + if (existing) existing.style.display = "none"; + return; + } + const container = document.querySelector("#DashboardCard_Container"); + if (!container) return; + let el = existing; + if (!el) { + el = document.createElement("div"); + el.className = "canvasrefined-widget-card " + spec.className; + el.dataset.crWidget = id; + el.innerHTML = spec.html; + try { if (spec.wire) spec.wire(el); } catch (e) { logError(e); } + } else { + el.style.display = ""; + try { if (spec.update) spec.update(el); } catch (e) { logError(e); } + } + placeSimpleWidget(id, el, container); +} + +/* ---- Shared timer engine (Focus Timer + Breathing Breaks) ---- */ + +const crTimers = {}; +function crTimer(id) { + if (!crTimers[id]) crTimers[id] = { total: 0, remaining: 0, running: false, raf: null, endTime: 0 }; + return crTimers[id]; +} +// Countdown readout: ceil so a fresh 25:00 only shows 24:59 after a full second. +function crTimerFormat(sec) { + const s = Math.max(0, Math.ceil(sec - 0.001)); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const r = s % 60; + if (h > 0) return h + ":" + String(m).padStart(2, "0") + ":" + String(r).padStart(2, "0"); + return String(m).padStart(2, "0") + ":" + String(r).padStart(2, "0"); +} +// Human label for a preset button ("15 min", "1h 30m", "45s"). +function crFormatDuration(sec) { + const s = Math.max(0, Math.round(sec)); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const r = s % 60; + if (h && m) return h + "h " + m + "m"; + if (h) return h + "h" + (r ? " " + r + "s" : ""); + if (m && r) return m + "m " + r + "s"; + if (m) return m + " min"; + return r + "s"; +} +function crTimerCancel(st) { + if (st && st.raf) { cancelAnimationFrame(st.raf); st.raf = null; } +} +function crTimerRender(id, el) { + const st = crTimer(id); + const disp = el.querySelector(".cr-timer-time") || el.querySelector(".cr-timer-display"); + if (disp) { + const txt = crTimerFormat(st.remaining); + if (disp.textContent !== txt) disp.textContent = txt; + } + const start = el.querySelector(".cr-timer-start"); + if (start) { + const label = st.running ? "Pause" : "Start"; + if (start.textContent !== label) start.textContent = label; + } + el.classList.toggle("is-running", st.running); + const bar = el.querySelector(".cr-timer-progress-bar"); + if (bar) bar.style.width = (st.total ? ((st.total - st.remaining) / st.total) * 100 : 0) + "%"; + const presets = [...el.querySelectorAll(".cr-timer-preset[data-slot]")]; + const totals = presets.map(b => parseInt(b.dataset.sec, 10)); + const matched = totals.indexOf(st.total) !== -1; + presets.forEach((btn, i) => btn.classList.toggle("active", !st.running && totals[i] === st.total)); + const customBtn = el.querySelector(".cr-timer-custom"); + if (customBtn) customBtn.classList.toggle("active", !st.running && !matched && st.total > 0); +} +// rAF loop: the bar (and the readout) are driven by real elapsed time, so the +// fill moves continuously instead of stepping once a second. +function crTimerFrame(id, el) { + const st = crTimer(id); + if (!st.running) return; + st.remaining = Math.max(0, (st.endTime - Date.now()) / 1000); + if (st.remaining <= 0) { + st.remaining = 0; + st.running = false; + st.raf = null; + } + crTimerRender(id, el); + if (st.running) st.raf = requestAnimationFrame(() => crTimerFrame(id, el)); +} +function crTimerStart(id, el) { + const st = crTimer(id); + if (st.running || st.remaining <= 0) return; + st.running = true; + st.endTime = Date.now() + st.remaining * 1000; + crTimerCancel(st); + st.raf = requestAnimationFrame(() => crTimerFrame(id, el)); + crTimerRender(id, el); +} +function crTimerPause(id, el) { + const st = crTimer(id); + if (st.running) st.remaining = Math.max(0, (st.endTime - Date.now()) / 1000); + st.running = false; + crTimerCancel(st); + crTimerRender(id, el); +} +function crTimerSet(id, el, seconds) { + const st = crTimer(id); + st.running = false; + crTimerCancel(st); + st.total = Math.max(1, Math.round(seconds)); + st.remaining = st.total; + crTimerRender(id, el); +} +function crTimerReset(id, el) { + const st = crTimer(id); + st.running = false; + crTimerCancel(st); + st.remaining = st.total; + crTimerRender(id, el); +} +// Only the three preset slots are configurable; Custom is typed on the widget. +function crTimerApplyCfg(el, cfg) { + el.querySelectorAll(".cr-timer-preset[data-slot]").forEach(btn => { + const slot = parseInt(btn.dataset.slot, 10); + const sec = cfg.presets[slot - 1]; + btn.dataset.sec = sec; + btn.textContent = crFormatDuration(sec); + }); +} +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. +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; +} +// Listeners are wired once (update passes only refresh config labels). +function crTimerWireControls(id, el) { + el.querySelectorAll(".cr-timer-preset[data-slot]").forEach(btn => { + btn.addEventListener("click", () => { + const sec = parseInt(btn.dataset.sec, 10); + if (Number.isFinite(sec) && sec > 0) crTimerSet(id, el, sec); + crTimerHideCustom(el); + }); + }); + + // Custom: reveal inline hours/minutes/seconds fields. + const customBtn = el.querySelector(".cr-timer-custom"); + const row = el.querySelector(".cr-timer-custom-row"); + const applyCustom = () => { + const sec = crTimerCustomSeconds(el); + if (sec <= 0) { + const mInput = el.querySelector(".cr-tf-m"); + if (mInput) { mInput.focus(); mInput.select(); } + return; + } + crTimerSet(id, el, sec); + crTimerHideCustom(el); + }; + if (customBtn && row) customBtn.addEventListener("click", () => { + if (!row.hidden) { crTimerHideCustom(el); return; } + row.hidden = false; + const mInput = el.querySelector(".cr-tf-m"); + if (mInput) { mInput.focus(); mInput.select(); } + }); + 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); + + const start = el.querySelector(".cr-timer-start"); + if (start) start.addEventListener("click", () => { + const st = crTimer(id); + if (st.running) crTimerPause(id, el); + else crTimerStart(id, el); + }); + const reset = el.querySelector(".cr-timer-reset"); + if (reset) reset.addEventListener("click", () => crTimerReset(id, el)); +} +function crTimerInitial(id, el, cfg) { + const st = crTimer(id); + if (!st.total) { st.total = cfg.presets[1]; st.remaining = st.total; } + crTimerRender(id, el); +} +// Presets now store {h,m,s}; legacy values were plain minutes, so a number is +// read as minutes. `fallback` is in seconds. +function crTimerReadSlots(prefix, fallback) { + return [1, 2, 3].map(i => { + const v = options[prefix + "_preset_" + i]; + if (v && typeof v === "object") { + const h = Math.max(0, parseInt(v.h, 10) || 0); + const m = Math.max(0, parseInt(v.m, 10) || 0); + const s = Math.max(0, parseInt(v.s, 10) || 0); + const total = h * 3600 + m * 60 + s; + if (total > 0) return total; + } else { + const n = parseInt(v, 10); + if (Number.isFinite(n) && n > 0) return n * 60; + } + return fallback[i - 1]; + }); +} + +/* ---- Focus Timer ---- */ + +const FOCUS_TIMER_DEFAULT_PRESETS = [15 * 60, 25 * 60, 45 * 60]; + +function focusTimerCfg() { + return { presets: crTimerReadSlots("focus_timer", FOCUS_TIMER_DEFAULT_PRESETS) }; +} + +const FOCUS_TIMER_HTML = ` +
Focus Timer
+
+
+
25:00
+
+
+ + + + +
+ +
+ + +
+`; + +function wireFocusTimer(el) { + crTimerApplyCfg(el, focusTimerCfg()); + crTimerWireControls("focus_timer", el); + crTimerInitial("focus_timer", el, focusTimerCfg()); +} +function updateFocusTimer(el) { + crTimerApplyCfg(el, focusTimerCfg()); + crTimerRender("focus_timer", el); +} +function loadFocusTimer() { + crLoadWidget("focus_timer", { + className: "canvasrefined-focus-timer", + html: FOCUS_TIMER_HTML, + wire: wireFocusTimer, + update: updateFocusTimer, + }); +} + +/* ---- Breathing Breaks (a zen timer) ---- */ + +const BREATHING_DEFAULT_PRESETS = [3 * 60, 5 * 60, 10 * 60]; + +function breathingCfg() { + return { presets: crTimerReadSlots("breathing", BREATHING_DEFAULT_PRESETS) }; +} + +const BREATHING_HTML = ` +
Breathing Breaks
+
+
+ Breathe in + Breathe out + Ready +
+
+
05:00
+
+ + + + +
+ +
+ + +
+`; + +function wireBreathingWidget(el) { + crTimerApplyCfg(el, breathingCfg()); + crTimerWireControls("breathing", el); + crTimerInitial("breathing", el, breathingCfg()); +} +function updateBreathingWidget(el) { + crTimerApplyCfg(el, breathingCfg()); + crTimerRender("breathing", el); +} +function loadBreathingWidget() { + crLoadWidget("breathing", { + className: "canvasrefined-breathing", + html: BREATHING_HTML, + wire: wireBreathingWidget, + update: updateBreathingWidget, + }); +} + +/* ---- Music (looks only) ---- */ + +const MUSIC_PLATFORMS = [ + { id: "spotify", name: "Spotify", option: "music_spotify_url", color: "#1DB954" }, + { id: "apple", name: "Apple Music", option: "music_apple_url", color: "#FA243C" }, + { id: "youtube", name: "YouTube", option: "music_youtube_url", color: "#FF0000" }, +]; + +const MUSIC_ICONS = { + spotify: '', + apple: '', + youtube: '', + note: '', + prev: '', + play: '', + next: '', +}; + +function musicPlatformLabel(url) { + try { + const host = new URL(url).hostname.replace(/^www\./, ""); + return host || "Open playlist"; + } catch (e) { + return "Open playlist"; + } +} + +// Sample library used by the switcher drawer until playback is wired up. +const MUSIC_SAMPLE_PLAYLISTS = { + spotify: ["Focus Flow", "Lo-fi Study"], + apple: ["Morning Mix", "Reading Room"], + youtube: ["Study With Me", "Ambient Spaces"], +}; +const MUSIC_SAMPLE_SONGS = [ + { title: "Golden Hour", artist: "Ambient Co.", len: "3:12" }, + { title: "Slow Tide", artist: "Nova", len: "4:05" }, + { title: "Paper Planes", artist: "The Loft", len: "2:48" }, + { title: "Night Drive", artist: "Kite", len: "3:57" }, + { title: "Warm Static", artist: "Halcyon", len: "5:21" }, +]; + +const MUSIC_HTML = ` +
+ Music + +
+
+ +
+ +
+
Nothing playing
+
Pick a playlist
+
+
+
+
+
0:000:00
+
+ + + +
+ +`; + +// No playback yet: this only keeps the look in sync with the saved links and +// display options. +function updateMusicWidget(el) { + const noThumb = options.music_no_thumb === true; + const thumbBg = !noThumb && options.music_thumb_bg !== false; + el.classList.toggle("no-thumb", noThumb); + el.classList.toggle("thumb-bg", thumbBg); + + const configured = []; + MUSIC_PLATFORMS.forEach(p => { + const url = (options[p.option] || "").trim(); + if (url) configured.push({ p, url }); + const sub = el.querySelector('.cr-drawer-platform[data-platform="' + p.id + '"] [data-role="platform-sub"]'); + if (sub) sub.textContent = url ? musicPlatformLabel(url) : "Not linked"; + const plName = el.querySelector('.cr-drawer-playlist[data-platform="' + p.id + '"] [data-role="playlist-name"]'); + if (plName) plName.textContent = url ? musicPlatformLabel(url) : MUSIC_SAMPLE_PLAYLISTS[p.id][0]; + }); + + // Mirror the first configured playlist into the player header. + const titleEl = el.querySelector(".cr-player-title"); + const subEl = el.querySelector(".cr-player-sub"); + const artEl = el.querySelector(".cr-player-art"); + const first = configured[0]; + if (first) { + if (titleEl) titleEl.textContent = first.p.name + " playlist"; + if (subEl) subEl.textContent = musicPlatformLabel(first.url); + if (artEl) artEl.innerHTML = MUSIC_ICONS[first.p.id]; + el.style.setProperty("--cr-thumb-color", first.p.color); + } else { + if (titleEl) titleEl.textContent = "Nothing playing"; + if (subEl) subEl.textContent = "Pick a playlist"; + if (artEl) artEl.innerHTML = MUSIC_ICONS.note; + el.style.removeProperty("--cr-thumb-color"); + } +} +function wireMusicWidget(el) { + const btn = el.querySelector(".cr-music-list-btn"); + const drawer = el.querySelector(".cr-music-drawer"); + const setOpen = (v) => { + el.classList.toggle("is-drawer-open", v); + if (drawer) drawer.setAttribute("aria-hidden", v ? "false" : "true"); + }; + if (btn && drawer) btn.addEventListener("click", () => setOpen(!el.classList.contains("is-drawer-open"))); + const close = el.querySelector(".cr-drawer-close"); + if (close) close.addEventListener("click", () => setOpen(false)); + el.querySelectorAll(".cr-drawer-tab").forEach(tab => { + tab.addEventListener("click", () => { + const name = tab.dataset.tab; + el.querySelectorAll(".cr-drawer-tab").forEach(t => t.classList.toggle("active", t === tab)); + el.querySelectorAll(".cr-drawer-pane").forEach(p => { p.hidden = p.dataset.pane !== name; }); + }); + }); + updateMusicWidget(el); +} +function loadMusicWidget() { + crLoadWidget("music", { + className: "canvasrefined-music", + html: MUSIC_HTML, + wire: wireMusicWidget, + update: updateMusicWidget, + }); +} + +/* ---- Daily Quote (looks only, content comes later) ---- */ + +const QUOTES_HTML = ` +
Daily Quote
+
+ +

The secret of getting ahead is getting started.

+ — Mark Twain +
+`; +function loadDailyQuotesWidget() { + crLoadWidget("daily_quotes", { + className: "canvasrefined-quotes", + html: QUOTES_HTML, + }); +} + +function loadAllSimpleWidgets() { + loadFocusTimer(); + loadMusicWidget(); + loadBreathingWidget(); + loadDailyQuotesWidget(); +} + /* Custom font diff --git a/js/popup.js b/js/popup.js index 6b5b76c..a102bb1 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,4 +1,4 @@ -const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'todo_celebrations_enabled', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics', 'hide_personal_details']; +const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'focus_timer', 'music', 'breathing', 'daily_quotes', 'better_todo', 'todo_celebrations_enabled', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics', 'hide_personal_details']; const syncedSubOptions = [ "grade_analytics_zones", "hide_course_images", @@ -20,6 +20,21 @@ const syncedSubOptions = [ "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", + "focus_timer_position", + "focus_timer_preset_1", + "focus_timer_preset_2", + "focus_timer_preset_3", + "music_position", + "music_spotify_url", + "music_apple_url", + "music_youtube_url", + "music_thumb_bg", + "music_no_thumb", + "breathing_position", + "breathing_preset_1", + "breathing_preset_2", + "breathing_preset_3", + "daily_quotes_position", "auto_dark", "auto_dark_start", "auto_dark_end", @@ -130,6 +145,25 @@ const defaultOptions = { "dashboard_notes_mode": "edit", "dashboard_notes_position": "top", "gpa_calc_position": "cards_end", + "focus_timer": false, + "focus_timer_position": "cards_end", + "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 }, + "breathing_preset_2": { "h": 0, "m": 5, "s": 0 }, + "breathing_preset_3": { "h": 0, "m": 10, "s": 0 }, + "daily_quotes": false, + "daily_quotes_position": "top", "better_todo": true, "better_sidebar": false, "sidebar_scale": 100, @@ -1007,6 +1041,10 @@ function toggleAlternateColorsVisibility(darkModeOn) { const WIDGET_LIST = [ { id: "dashboard_notes", nameKey: "dashboard_notes", shortKey: "widget_short_notes" }, { id: "gpa_calc", nameKey: "gpa_calculator", shortKey: "widget_short_gpa" }, + { id: "focus_timer", nameKey: "focus_timer", shortKey: "widget_short_focus" }, + { id: "music", nameKey: "music", shortKey: "widget_short_music" }, + { id: "breathing", nameKey: "breathing_breaks", shortKey: "widget_short_breathing" }, + { id: "daily_quotes", nameKey: "daily_quotes", shortKey: "widget_short_quotes" }, ]; function widgetPosFromSync(id, sync) { @@ -1015,7 +1053,7 @@ function widgetPosFromSync(id, sync) { if (sync.gpa_calc_position !== undefined) return sync.gpa_calc_position; return sync.gpa_calc_prepend === true ? "cards_start" : "cards_end"; } - return "top"; + return sync[id + "_position"] || (defaultOptions.sync[id + "_position"]) || "top"; } const WIDGET_ORDER_MIN_PCT = 15; @@ -1037,7 +1075,11 @@ function parseWidgetOrderEntries(raw) { function widgetDisplayName(id) { const info = WIDGET_LIST.find(w => w.id === id); - return (info && chrome.i18n.getMessage(info.shortKey || info.nameKey)) || id; + const msg = info && chrome.i18n.getMessage(info.shortKey || info.nameKey); + if (msg) return msg; + // Locales that predate a widget have no message: show a friendly label + // instead of the raw storage id. + return id.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); } // Live-refresh hook for the widget-order UI, set by setupWidgetOrder and @@ -1050,7 +1092,9 @@ function setupWidgetOrder() { const wrap = document.getElementById("widget-order-container"); if (!wrap) return; - const KEYS = ["dashboard_notes", "gpa_calc", "dashboard_notes_position", "gpa_calc_position", "gpa_calc_prepend", "widget_order"]; + const KEYS = WIDGET_LIST.map(w => w.id) + .concat(WIDGET_LIST.map(w => w.id + "_position")) + .concat(["gpa_calc_prepend", "widget_order"]); const AREAS = ["top", "cards_start", "cards_end", "below"]; @@ -1223,6 +1267,10 @@ function setupWidgetPosition() { [ { select: "dashboard_notes_position", key: "dashboard_notes_position" }, { select: "gpa_calc_position", key: "gpa_calc_position" }, + { select: "focus_timer_position", key: "focus_timer_position" }, + { select: "music_position", key: "music_position" }, + { select: "breathing_position", key: "breathing_position" }, + { select: "daily_quotes_position", key: "daily_quotes_position" }, ].forEach(({ select, key }) => { const el = document.getElementById(select); if (!el) return; @@ -1241,10 +1289,48 @@ function setupWidgetPosition() { }); } +// Hours/minutes/seconds widgets settings (Focus Timer / Breathing presets). +function setupWidgetTimeInput(prefix, initial) { + const hEl = document.getElementById(prefix + "_h"); + const mEl = document.getElementById(prefix + "_m"); + const sEl = document.getElementById(prefix + "_s"); + if (!hEl || !mEl || !sEl) return; + let v = initial; + if (!v || typeof v !== "object") { + // Legacy plain-minutes value. + const n = parseInt(v, 10); + v = Number.isFinite(n) && n > 0 ? { h: 0, m: n, s: 0 } : (defaultOptions.sync[prefix] || { h: 0, m: 0, s: 0 }); + } + hEl.value = v.h || 0; + mEl.value = v.m || 0; + sEl.value = v.s || 0; + const save = () => { + const clamp = (el, max) => Math.min(max, Math.max(0, parseInt(el.value, 10) || 0)); + let h = clamp(hEl, 99), m = clamp(mEl, 59), s = clamp(sEl, 59); + if (h === 0 && m === 0 && s === 0) { + const d = defaultOptions.sync[prefix] || { h: 0, m: 5, s: 0 }; + h = d.h; m = d.m; s = d.s; + } + hEl.value = h; mEl.value = m; sEl.value = s; + chrome.storage.sync.set({ [prefix]: { h, m, s } }); + }; + [hEl, mEl, sEl].forEach(el => el.addEventListener("change", save)); +} + +// Text/URL widget settings (Music playlist links). +function setupWidgetTextInput(id, initial) { + const el = document.getElementById(id); + if (!el) return; + el.value = initial || ""; + el.addEventListener("change", function () { + chrome.storage.sync.set({ [id]: this.value.trim() }); + }); +} + // Hide a toggle's sub-options when it's off; auto_dark only hides its time // clocks. function toggleSubOptionsVisibility(option, isOn) { - const togglesWithSubOptions = ["dashboard_notes", "gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"]; + const togglesWithSubOptions = ["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"]; if (!togglesWithSubOptions.includes(option)) return; const optionEl = document.getElementById(option); if (!optionEl) return; @@ -1649,6 +1735,8 @@ function setup() { "customBackgroundNasaDaily", "fitImageToScreen", "card_transparency", + "music_thumb_bg", + "music_no_thumb", "customCardStyles", "card_grid", "card_grid_center_rows", @@ -1843,6 +1931,42 @@ function setup() { identifier: "anon_progress_total", setup: (initial) => setupAnonProgressInput("anon_progress_total", initial, false), }, + { + identifier: "focus_timer_preset_1", + setup: (initial) => setupWidgetTimeInput("focus_timer_preset_1", initial), + }, + { + identifier: "focus_timer_preset_2", + setup: (initial) => setupWidgetTimeInput("focus_timer_preset_2", initial), + }, + { + identifier: "focus_timer_preset_3", + setup: (initial) => setupWidgetTimeInput("focus_timer_preset_3", initial), + }, + { + identifier: "breathing_preset_1", + setup: (initial) => setupWidgetTimeInput("breathing_preset_1", initial), + }, + { + identifier: "breathing_preset_2", + setup: (initial) => setupWidgetTimeInput("breathing_preset_2", initial), + }, + { + identifier: "breathing_preset_3", + setup: (initial) => setupWidgetTimeInput("breathing_preset_3", initial), + }, + { + identifier: "music_spotify_url", + setup: (initial) => setupWidgetTextInput("music_spotify_url", initial), + }, + { + identifier: "music_apple_url", + setup: (initial) => setupWidgetTextInput("music_apple_url", initial), + }, + { + identifier: "music_youtube_url", + setup: (initial) => setupWidgetTextInput("music_youtube_url", initial), + }, { identifier: "todo_more_expanded", setup: (initial) => setupTodoMoreOptions(initial), @@ -1888,7 +2012,7 @@ function setup() { chrome.storage.sync.set({ "hide_course_images": false }); } toggleSubOptionsVisibility(option, status); - if (option === "dashboard_notes" || option === "gpa_calc") { + if (option === "dashboard_notes" || option === "gpa_calc" || option === "focus_timer" || option === "music" || option === "breathing" || option === "daily_quotes") { if (refreshWidgetOrder) refreshWidgetOrder(); } }); @@ -1898,7 +2022,7 @@ function setup() { chrome.storage.sync.get("todo_celebrations_enabled", v => { toggleCelebrationOptionsVisibility(v["todo_celebrations_enabled"] !== false); }); - ["dashboard_notes", "gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => { + ["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => { toggleSubOptionsVisibility(opt, sync[opt] === true); }); toggleAlternateColorsVisibility(sync["dark_mode"] === true);