mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 13:57:17 +01:00
finished up widgets
This commit is contained in:
parent
2687239ecb
commit
80662e75e8
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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;}
|
||||
|
||||
@ -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;}
|
||||
|
||||
182
html/popup.html
182
html/popup.html
@ -1135,6 +1135,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Widgets: two-column grid; add one option-container per widget here -->
|
||||
<div class="option-container">
|
||||
<h3 class="header-small">Top / Bottom height</h3>
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="widget_row_height" class="sub-text widget-inline-label">Height</label>
|
||||
<input type="number" id="widget_row_height" class="card-input" min="0" max="2000" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="widgets-grid">
|
||||
<div class="option-container">
|
||||
<div class="option" id="dashboard_notes">
|
||||
@ -1146,15 +1154,20 @@
|
||||
</div><span class="option-name" data-i18n="dashboard_notes">Dashboard Notes</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="dashboard_notes_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="dashboard_notes_position" class="widget-pos">
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="dashboard_notes_position" class="sub-text widget-inline-label" data-i18n="widget_position">Position</label>
|
||||
<select id="dashboard_notes_position" class="widget-pos widget-inline-select">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<label for="dashboard_notes_height" class="sub-text widget-inline-label">Cards height</label>
|
||||
<input type="number" id="dashboard_notes_height" class="card-input" min="0" max="2000" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
@ -1168,15 +1181,20 @@
|
||||
<span class="option-name" data-i18n="gpa_calculator">GPA calculator</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="gpa_calc_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="gpa_calc_position" class="widget-pos">
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="gpa_calc_position" class="sub-text widget-inline-label" data-i18n="widget_position">Position</label>
|
||||
<select id="gpa_calc_position" class="widget-pos widget-inline-select">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<label for="gpa_calc_height" class="sub-text widget-inline-label">Cards height</label>
|
||||
<input type="number" id="gpa_calc_height" class="card-input" min="0" max="2000" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_cumulative" name="gpa_calc_cumulative">
|
||||
<label for="gpa_calc_cumulative" class="sub-text" data-i18n="gpa_calc_cumulative">Show cumulative GPA</label>
|
||||
@ -1197,37 +1215,48 @@
|
||||
</div><span class="option-name" data-i18n="focus_timer">Focus Timer</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="focus_timer_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="focus_timer_position" class="widget-pos">
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="focus_timer_position" class="sub-text widget-inline-label" data-i18n="widget_position">Position</label>
|
||||
<select id="focus_timer_position" class="widget-pos widget-inline-select">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:3px;">
|
||||
<span class="sub-text">Button 1</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<input type="number" id="focus_timer_preset_1_h" class="card-input" min="0" max="99" value="0" style="width:46px;margin-top:0;"><span class="sub-text">h</span>
|
||||
<input type="number" id="focus_timer_preset_1_m" class="card-input" min="0" max="59" value="15" style="width:46px;margin-top:0;"><span class="sub-text">m</span>
|
||||
<input type="number" id="focus_timer_preset_1_s" class="card-input" min="0" max="59" value="0" style="width:46px;margin-top:0;"><span class="sub-text">s</span>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<label for="focus_timer_height" class="sub-text widget-inline-label">Cards height</label>
|
||||
<input type="number" id="focus_timer_height" class="card-input" min="0" max="2000" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<span class="sub-text widget-inline-label">Button 1</span>
|
||||
<div class="popup-time-input">
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_1_h" inputmode="numeric" maxlength="2" value="00">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_1_m" inputmode="numeric" maxlength="2" value="15">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_1_s" inputmode="numeric" maxlength="2" value="00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:3px;">
|
||||
<span class="sub-text">Button 2</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<input type="number" id="focus_timer_preset_2_h" class="card-input" min="0" max="99" value="0" style="width:46px;margin-top:0;"><span class="sub-text">h</span>
|
||||
<input type="number" id="focus_timer_preset_2_m" class="card-input" min="0" max="59" value="25" style="width:46px;margin-top:0;"><span class="sub-text">m</span>
|
||||
<input type="number" id="focus_timer_preset_2_s" class="card-input" min="0" max="59" value="0" style="width:46px;margin-top:0;"><span class="sub-text">s</span>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<span class="sub-text widget-inline-label">Button 2</span>
|
||||
<div class="popup-time-input">
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_2_h" inputmode="numeric" maxlength="2" value="00">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_2_m" inputmode="numeric" maxlength="2" value="25">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_2_s" inputmode="numeric" maxlength="2" value="00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:3px;">
|
||||
<span class="sub-text">Button 3</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<input type="number" id="focus_timer_preset_3_h" class="card-input" min="0" max="99" value="0" style="width:46px;margin-top:0;"><span class="sub-text">h</span>
|
||||
<input type="number" id="focus_timer_preset_3_m" class="card-input" min="0" max="59" value="45" style="width:46px;margin-top:0;"><span class="sub-text">m</span>
|
||||
<input type="number" id="focus_timer_preset_3_s" class="card-input" min="0" max="59" value="0" style="width:46px;margin-top:0;"><span class="sub-text">s</span>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<span class="sub-text widget-inline-label">Button 3</span>
|
||||
<div class="popup-time-input">
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_3_h" inputmode="numeric" maxlength="2" value="00">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_3_m" inputmode="numeric" maxlength="2" value="45">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="focus_timer_preset_3_s" inputmode="numeric" maxlength="2" value="00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1242,82 +1271,46 @@
|
||||
</div><span class="option-name" data-i18n="breathing_breaks">Breathing Breaks</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="breathing_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="breathing_position" class="widget-pos">
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="breathing_position" class="sub-text widget-inline-label" data-i18n="widget_position">Position</label>
|
||||
<select id="breathing_position" class="widget-pos widget-inline-select">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:3px;">
|
||||
<span class="sub-text">Button 1</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<input type="number" id="breathing_preset_1_h" class="card-input" min="0" max="99" value="0" style="width:46px;margin-top:0;"><span class="sub-text">h</span>
|
||||
<input type="number" id="breathing_preset_1_m" class="card-input" min="0" max="59" value="3" style="width:46px;margin-top:0;"><span class="sub-text">m</span>
|
||||
<input type="number" id="breathing_preset_1_s" class="card-input" min="0" max="59" value="0" style="width:46px;margin-top:0;"><span class="sub-text">s</span>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<label for="breathing_height" class="sub-text widget-inline-label">Cards height</label>
|
||||
<input type="number" id="breathing_height" class="card-input" min="0" max="2000" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<span class="sub-text widget-inline-label">Button 1</span>
|
||||
<div class="popup-time-input">
|
||||
<input type="text" class="popup-tf" id="breathing_preset_1_m" inputmode="numeric" maxlength="2" value="03">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="breathing_preset_1_s" inputmode="numeric" maxlength="2" value="00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:3px;">
|
||||
<span class="sub-text">Button 2</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<input type="number" id="breathing_preset_2_h" class="card-input" min="0" max="99" value="0" style="width:46px;margin-top:0;"><span class="sub-text">h</span>
|
||||
<input type="number" id="breathing_preset_2_m" class="card-input" min="0" max="59" value="5" style="width:46px;margin-top:0;"><span class="sub-text">m</span>
|
||||
<input type="number" id="breathing_preset_2_s" class="card-input" min="0" max="59" value="0" style="width:46px;margin-top:0;"><span class="sub-text">s</span>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<span class="sub-text widget-inline-label">Button 2</span>
|
||||
<div class="popup-time-input">
|
||||
<input type="text" class="popup-tf" id="breathing_preset_2_m" inputmode="numeric" maxlength="2" value="05">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="breathing_preset_2_s" inputmode="numeric" maxlength="2" value="00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:3px;">
|
||||
<span class="sub-text">Button 3</span>
|
||||
<div style="display:flex;gap:5px;align-items:center;">
|
||||
<input type="number" id="breathing_preset_3_h" class="card-input" min="0" max="99" value="0" style="width:46px;margin-top:0;"><span class="sub-text">h</span>
|
||||
<input type="number" id="breathing_preset_3_m" class="card-input" min="0" max="59" value="10" style="width:46px;margin-top:0;"><span class="sub-text">m</span>
|
||||
<input type="number" id="breathing_preset_3_s" class="card-input" min="0" max="59" value="0" style="width:46px;margin-top:0;"><span class="sub-text">s</span>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<span class="sub-text widget-inline-label">Button 3</span>
|
||||
<div class="popup-time-input">
|
||||
<input type="text" class="popup-tf" id="breathing_preset_3_m" inputmode="numeric" maxlength="2" value="10">
|
||||
<span class="popup-tf-colon">:</span>
|
||||
<input type="text" class="popup-tf" id="breathing_preset_3_s" inputmode="numeric" maxlength="2" value="00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="music">
|
||||
<input type="radio" id="off" name="music">
|
||||
<input type="radio" id="on" name="music">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div><span class="option-name" data-i18n="music">Music</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="music_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="music_position" class="widget-pos">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;margin-top:6px;">
|
||||
<label for="music_spotify_url" class="sub-text">Spotify playlist</label>
|
||||
<input type="text" id="music_spotify_url" class="card-input" placeholder="https://open.spotify.com/playlist/...">
|
||||
</div>
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;margin-top:6px;">
|
||||
<label for="music_apple_url" class="sub-text">Apple Music playlist</label>
|
||||
<input type="text" id="music_apple_url" class="card-input" placeholder="https://music.apple.com/...">
|
||||
</div>
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;margin-top:6px;">
|
||||
<label for="music_youtube_url" class="sub-text">YouTube playlist</label>
|
||||
<input type="text" id="music_youtube_url" class="card-input" placeholder="https://youtube.com/playlist?...">
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:9px">
|
||||
<input type="checkbox" id="music_thumb_bg" name="music_thumb_bg">
|
||||
<label for="music_thumb_bg" class="sub-text">Thumbnail background</label>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="music_no_thumb" name="music_no_thumb">
|
||||
<label for="music_no_thumb" class="sub-text">Remove thumbnail</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="daily_quotes">
|
||||
<input type="radio" id="off" name="daily_quotes">
|
||||
@ -1328,15 +1321,20 @@
|
||||
</div><span class="option-name" data-i18n="daily_quotes">Daily Quotes</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="daily_quotes_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="daily_quotes_position" class="widget-pos">
|
||||
<div class="sub-option widget-inline-row">
|
||||
<label for="daily_quotes_position" class="sub-text widget-inline-label" data-i18n="widget_position">Position</label>
|
||||
<select id="daily_quotes_position" class="widget-pos widget-inline-select">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option widget-inline-row" style="margin-top:6px;">
|
||||
<label for="daily_quotes_height" class="sub-text widget-inline-label">Cards height</label>
|
||||
<input type="number" id="daily_quotes_height" class="card-input" min="0" max="2000" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
581
js/content.js
581
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 = `<h3 class="canvasrefined-gpa-header">GPA</h3><div><div><p id="canvasrefined-gpa-unweighted">--</p><p>Current</p></div><div style="display:${options["gpa_calc_weighted"] ? "block" : "none"}"><p id="canvasrefined-gpa-weighted">--</p><p>Weighted</p></div><div style="display:${options["gpa_calc_cumulative"] ? "block" : "none"}"><p id="canvasrefined-gpa-cumulative">--</p><p>Cumulative</p></div></div>`;
|
||||
let editBtn = makeElement("button", container2, { "className": "canvasrefined-gpa-edit-btn", "textContent": "Edit Calculator" });
|
||||
|
||||
container.innerHTML = '<h3 class="canvasrefined-gpa-header">GPA Calculator</h3><div class="canvasrefined-gpa-courses-container"><div class="canvasrefined-gpa-courses"></div></div>';
|
||||
|
||||
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 = `<h3 class="canvasrefined-gpa-header">GPA</h3><div><div><p id="canvasrefined-gpa-unweighted"></p><p>Current</p></div><div style="display:${options["gpa_calc_weighted"] ? "block" : "none"}"><p id="canvasrefined-gpa-weighted"></p><p>Weighted</p></div><div style="display:${options["gpa_calc_cumulative"] ? "block" : "none"}"><p id="canvasrefined-gpa-cumulative"></p><p>Cumulative</p></div></div>`;
|
||||
let editBtn = makeElement("button", container2, { "className": "canvasrefined-gpa-edit-btn", "textContent": "Edit Calculator" });
|
||||
|
||||
container.innerHTML = '<h3 class="canvasrefined-gpa-header">GPA Calculator</h3><div class="canvasrefined-gpa-courses-container"><div class="canvasrefined-gpa-courses"></div></div>';
|
||||
|
||||
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 = `
|
||||
<button type="button" class="cr-timer-preset cr-timer-custom">Custom</button>
|
||||
</div>
|
||||
<div class="cr-timer-custom-row" hidden>
|
||||
<div class="cr-time-fields">
|
||||
<label class="cr-tf-box"><input type="number" class="cr-tf cr-tf-h" min="0" max="99" inputmode="numeric" placeholder="0"><span class="cr-tf-unit">h</span></label>
|
||||
<label class="cr-tf-box"><input type="number" class="cr-tf cr-tf-m" min="0" max="59" inputmode="numeric" placeholder="0"><span class="cr-tf-unit">m</span></label>
|
||||
<label class="cr-tf-box"><input type="number" class="cr-tf cr-tf-s" min="0" max="59" inputmode="numeric" placeholder="0"><span class="cr-tf-unit">s</span></label>
|
||||
<div class="cr-time-input">
|
||||
<input type="text" class="cr-tf cr-tf-h" inputmode="numeric" maxlength="2" value="00" aria-label="Hours">
|
||||
<span class="cr-tf-colon" aria-hidden="true">:</span>
|
||||
<input type="text" class="cr-tf cr-tf-m" inputmode="numeric" maxlength="2" value="00" aria-label="Minutes">
|
||||
<span class="cr-tf-colon" aria-hidden="true">:</span>
|
||||
<input type="text" class="cr-tf cr-tf-s" inputmode="numeric" maxlength="2" value="00" aria-label="Seconds">
|
||||
</div>
|
||||
<button type="button" class="cr-widget-btn cr-timer-custom-go">Set</button>
|
||||
</div>
|
||||
@ -9008,31 +9106,37 @@ function breathingCfg() {
|
||||
|
||||
const BREATHING_HTML = `
|
||||
<div class="cr-widget-head"><span class="cr-widget-title">Breathing Breaks</span></div>
|
||||
<div class="cr-breath-stage">
|
||||
<div class="cr-breath-circle">
|
||||
<span class="cr-breath-phase cr-breath-phase-in">Breathe in</span>
|
||||
<span class="cr-breath-phase cr-breath-phase-out">Breathe out</span>
|
||||
<span class="cr-breath-idle">Ready</span>
|
||||
<div class="cr-breath-layout">
|
||||
<div class="cr-breath-left">
|
||||
<div class="cr-breath-stage">
|
||||
<div class="cr-breath-circle">
|
||||
<span class="cr-breath-phase cr-breath-phase-in">Breathe in</span>
|
||||
<span class="cr-breath-phase cr-breath-phase-out">Breathe out</span>
|
||||
<span class="cr-breath-idle">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cr-timer-time cr-timer-display cr-breath-display" aria-live="polite">05:00</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cr-timer-time cr-timer-display cr-breath-display" aria-live="polite">05:00</div>
|
||||
<div class="cr-timer-presets">
|
||||
<button type="button" class="cr-timer-preset" data-slot="1">3 min</button>
|
||||
<button type="button" class="cr-timer-preset" data-slot="2">5 min</button>
|
||||
<button type="button" class="cr-timer-preset" data-slot="3">10 min</button>
|
||||
<button type="button" class="cr-timer-preset cr-timer-custom">Custom</button>
|
||||
</div>
|
||||
<div class="cr-timer-custom-row" hidden>
|
||||
<div class="cr-time-fields">
|
||||
<label class="cr-tf-box"><input type="number" class="cr-tf cr-tf-h" min="0" max="99" inputmode="numeric" placeholder="0"><span class="cr-tf-unit">h</span></label>
|
||||
<label class="cr-tf-box"><input type="number" class="cr-tf cr-tf-m" min="0" max="59" inputmode="numeric" placeholder="0"><span class="cr-tf-unit">m</span></label>
|
||||
<label class="cr-tf-box"><input type="number" class="cr-tf cr-tf-s" min="0" max="59" inputmode="numeric" placeholder="0"><span class="cr-tf-unit">s</span></label>
|
||||
<div class="cr-breath-right">
|
||||
<div class="cr-timer-presets">
|
||||
<button type="button" class="cr-timer-preset" data-slot="1">3 min</button>
|
||||
<button type="button" class="cr-timer-preset" data-slot="2">5 min</button>
|
||||
<button type="button" class="cr-timer-preset" data-slot="3">10 min</button>
|
||||
<button type="button" class="cr-timer-preset cr-timer-custom">Custom</button>
|
||||
</div>
|
||||
<div class="cr-timer-custom-row" hidden>
|
||||
<div class="cr-time-input">
|
||||
<input type="text" class="cr-tf cr-tf-m" inputmode="numeric" maxlength="2" value="00" aria-label="Minutes">
|
||||
<span class="cr-tf-colon" aria-hidden="true">:</span>
|
||||
<input type="text" class="cr-tf cr-tf-s" inputmode="numeric" maxlength="2" value="00" aria-label="Seconds">
|
||||
</div>
|
||||
<button type="button" class="cr-widget-btn cr-timer-custom-go">Set</button>
|
||||
</div>
|
||||
<div class="cr-timer-controls">
|
||||
<button type="button" class="cr-widget-btn cr-timer-start">Start</button>
|
||||
<button type="button" class="cr-widget-btn cr-widget-btn-ghost cr-timer-reset">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="cr-widget-btn cr-timer-custom-go">Set</button>
|
||||
</div>
|
||||
<div class="cr-timer-controls">
|
||||
<button type="button" class="cr-widget-btn cr-timer-start">Start</button>
|
||||
<button type="button" class="cr-widget-btn cr-widget-btn-ghost cr-timer-reset">Reset</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -9054,191 +9158,67 @@ function loadBreathingWidget() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Music (looks only) ---- */
|
||||
/* ---- Daily Quote (stoic.tekloon.net, refreshed once a day) ---- */
|
||||
|
||||
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: '<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#1DB954"/><path d="M7 9.6c3.3-1 7-.6 9.8 1M7.4 12.6c2.6-.8 5.4-.4 7.6 .9M8 15.5c2-.6 4-.3 5.6 .7" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>',
|
||||
apple: '<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#FA243C"/><path d="M15.2 6.5l-5 1.1a.8.8 0 0 0-.6.8v5.2a2 2 0 1 0 .9 1.7V9.3l4-0.9v3.6a2 2 0 1 0 .9 1.7V7.3a.8.8 0 0 0-.2-.8z" fill="#fff"/></svg>',
|
||||
youtube: '<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><rect x="1.5" y="5" width="21" height="14" rx="4" fill="#FF0000"/><path d="M10 9l5 3-5 3z" fill="#fff"/></svg>',
|
||||
note: '<svg viewBox="0 0 24 24" width="26" height="26" fill="currentColor" aria-hidden="true"><path d="M20 3v11.6a3.6 3.6 0 1 1-2-3.2V6.3l-8 1.6v8.7a3.6 3.6 0 1 1-2-3.2V5.4L20 3z"/></svg>',
|
||||
prev: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M6 6h2v12H6zM20 6v12l-9-6z"/></svg>',
|
||||
play: '<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>',
|
||||
next: '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M16 6h2v12h-2zM4 6v12l9-6z"/></svg>',
|
||||
const DAILY_QUOTE_DEFAULT = {
|
||||
quote: "The secret of getting ahead is getting started.",
|
||||
author: "Mark Twain",
|
||||
};
|
||||
|
||||
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 = `
|
||||
<div class="cr-widget-head">
|
||||
<span class="cr-widget-title">Music</span>
|
||||
<button type="button" class="cr-music-list-btn">Playlists <span class="cr-music-caret" aria-hidden="true">▾</span></button>
|
||||
</div>
|
||||
<div class="cr-player">
|
||||
<div class="cr-player-thumb" aria-hidden="true"><span class="cr-player-thumb-mark">${MUSIC_ICONS.note}</span></div>
|
||||
<div class="cr-player-body">
|
||||
<div class="cr-player-art" aria-hidden="true">${MUSIC_ICONS.note}</div>
|
||||
<div class="cr-player-info">
|
||||
<div class="cr-player-title">Nothing playing</div>
|
||||
<div class="cr-player-sub">Pick a playlist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cr-player-progress"><div class="cr-player-progress-fill"></div></div>
|
||||
<div class="cr-player-times"><span>0:00</span><span>0:00</span></div>
|
||||
<div class="cr-player-controls">
|
||||
<button type="button" class="cr-player-btn" aria-label="Previous track">${MUSIC_ICONS.prev}</button>
|
||||
<button type="button" class="cr-player-btn cr-player-play" aria-label="Play">${MUSIC_ICONS.play}</button>
|
||||
<button type="button" class="cr-player-btn" aria-label="Next track">${MUSIC_ICONS.next}</button>
|
||||
</div>
|
||||
<div class="cr-music-drawer" aria-hidden="true">
|
||||
<div class="cr-drawer-head">
|
||||
<span class="cr-drawer-title">Library</span>
|
||||
<button type="button" class="cr-drawer-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="cr-drawer-tabs">
|
||||
<button type="button" class="cr-drawer-tab active" data-tab="platforms">Platforms</button>
|
||||
<button type="button" class="cr-drawer-tab" data-tab="playlists">Playlists</button>
|
||||
<button type="button" class="cr-drawer-tab" data-tab="songs">Songs</button>
|
||||
</div>
|
||||
<div class="cr-drawer-body">
|
||||
<div class="cr-drawer-pane" data-pane="platforms">
|
||||
${MUSIC_PLATFORMS.map(p => `
|
||||
<div class="cr-drawer-row cr-drawer-platform" data-platform="${p.id}">
|
||||
<span class="cr-drawer-icon">${MUSIC_ICONS[p.id]}</span>
|
||||
<span class="cr-drawer-meta"><span class="cr-drawer-name">${p.name}</span><span class="cr-drawer-sub" data-role="platform-sub">Not linked</span></span>
|
||||
<span class="cr-drawer-chevron" aria-hidden="true">›</span>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
<div class="cr-drawer-pane" data-pane="playlists" hidden>
|
||||
${MUSIC_PLATFORMS.map(p => `
|
||||
<div class="cr-drawer-row cr-drawer-playlist" data-platform="${p.id}">
|
||||
<span class="cr-drawer-icon cr-drawer-icon-pl">${MUSIC_ICONS[p.id]}</span>
|
||||
<span class="cr-drawer-meta"><span class="cr-drawer-name" data-role="playlist-name">${MUSIC_SAMPLE_PLAYLISTS[p.id][0]}</span><span class="cr-drawer-sub">${p.name}</span></span>
|
||||
<span class="cr-drawer-chevron" aria-hidden="true">›</span>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
<div class="cr-drawer-pane" data-pane="songs" hidden>
|
||||
${MUSIC_SAMPLE_SONGS.map((s, i) => `
|
||||
<div class="cr-drawer-row cr-drawer-song">
|
||||
<span class="cr-drawer-index">${i + 1}</span>
|
||||
<span class="cr-drawer-meta"><span class="cr-drawer-name">${s.title}</span><span class="cr-drawer-sub">${s.artist}</span></span>
|
||||
<span class="cr-drawer-len">${s.len}</span>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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 = `
|
||||
<div class="cr-widget-head"><span class="cr-widget-title">Daily Quote</span></div>
|
||||
<div class="cr-quote-body">
|
||||
<span class="cr-quote-mark" aria-hidden="true">“</span>
|
||||
<p class="cr-quote-text">The secret of getting ahead is getting started.</p>
|
||||
<span class="cr-quote-author">— Mark Twain</span>
|
||||
<p class="cr-quote-text">${DAILY_QUOTE_DEFAULT.quote}</p>
|
||||
<span class="cr-quote-author">— ${DAILY_QUOTE_DEFAULT.author}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function renderDailyQuote(el, data) {
|
||||
if (!el) return;
|
||||
const quote = data && data.quote ? data.quote : DAILY_QUOTE_DEFAULT.quote;
|
||||
const author = data && data.author ? data.author : DAILY_QUOTE_DEFAULT.author;
|
||||
const textEl = el.querySelector(".cr-quote-text");
|
||||
const authorEl = el.querySelector(".cr-quote-author");
|
||||
if (textEl) textEl.textContent = quote;
|
||||
if (authorEl) authorEl.textContent = author ? "\u2014 " + author : "";
|
||||
}
|
||||
|
||||
// Ask the background worker to refresh (it caches per calendar day, so this is
|
||||
// a no-op after the first successful fetch of the day).
|
||||
let dailyQuoteRequested = false;
|
||||
function ensureDailyQuote() {
|
||||
if (dailyQuoteRequested) return;
|
||||
dailyQuoteRequested = true;
|
||||
try {
|
||||
chrome.runtime.sendMessage({ type: "getDailyQuote" }, () => {
|
||||
// Reading lastError suppresses the unchecked-error warning; on
|
||||
// failure allow a retry from a later pass.
|
||||
if (chrome.runtime.lastError) dailyQuoteRequested = false;
|
||||
});
|
||||
} catch (e) {
|
||||
dailyQuoteRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDailyQuoteWidget(el) {
|
||||
chrome.storage.local.get("daily_quote", st => {
|
||||
if (st && st.daily_quote) renderDailyQuote(el, st.daily_quote);
|
||||
});
|
||||
ensureDailyQuote();
|
||||
}
|
||||
|
||||
function loadDailyQuotesWidget() {
|
||||
crLoadWidget("daily_quotes", {
|
||||
className: "canvasrefined-quotes",
|
||||
html: QUOTES_HTML,
|
||||
wire: refreshDailyQuoteWidget,
|
||||
update: refreshDailyQuoteWidget,
|
||||
});
|
||||
}
|
||||
|
||||
function loadAllSimpleWidgets() {
|
||||
loadFocusTimer();
|
||||
loadMusicWidget();
|
||||
loadBreathingWidget();
|
||||
loadDailyQuotesWidget();
|
||||
}
|
||||
@ -10501,6 +10481,11 @@ function cleanCustomAssignments() {
|
||||
function getGrades() {
|
||||
if (options.gpa_calc === true || options.dashboard_grades === true) {
|
||||
grades = getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}include[]=concluded&include[]=total_scores&include[]=computed_current_score&include[]=current_grading_period_scores&per_page=100`);
|
||||
// Render the GPA card the moment grade data arrives, rather than waiting
|
||||
// for the next dashboard mutation pass to call setupGPACalc().
|
||||
if (options.gpa_calc === true) {
|
||||
grades.then(() => setupGPACalc()).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
150
js/popup.js
150
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', '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 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', '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",
|
||||
@ -24,17 +24,17 @@ const syncedSubOptions = [
|
||||
"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",
|
||||
"widget_row_height",
|
||||
"dashboard_notes_height",
|
||||
"gpa_calc_height",
|
||||
"focus_timer_height",
|
||||
"breathing_height",
|
||||
"daily_quotes_height",
|
||||
"auto_dark",
|
||||
"auto_dark_start",
|
||||
"auto_dark_end",
|
||||
@ -150,13 +150,6 @@ const defaultOptions = {
|
||||
"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 },
|
||||
@ -164,6 +157,14 @@ const defaultOptions = {
|
||||
"breathing_preset_3": { "h": 0, "m": 10, "s": 0 },
|
||||
"daily_quotes": false,
|
||||
"daily_quotes_position": "top",
|
||||
// Height settings (px; 0 = auto). widget_row_height applies to the
|
||||
// shared Top/Bottom row; *_height applies to a widget in Before/After.
|
||||
"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,
|
||||
"better_sidebar": false,
|
||||
"sidebar_scale": 100,
|
||||
@ -1042,7 +1043,6 @@ 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" },
|
||||
];
|
||||
@ -1164,6 +1164,15 @@ function renderWidgetOrderStrip(strip, entries) {
|
||||
strip.textContent = "";
|
||||
// Before/after strips are sequence only — no width resizing, no dividers.
|
||||
const resizable = !strip.dataset.area.startsWith("cards");
|
||||
// Normalize widths to exactly 100%: a widget with no saved size borrows
|
||||
// the average of the sized ones. Without this, adding a third widget to a
|
||||
// previously 50/50 pair produced 50+50+33 = 133% and the strip overflowed.
|
||||
let sizes = entries.map(e => (Number.isFinite(e.size) && e.size > 0) ? e.size : null);
|
||||
const known = sizes.filter(v => v !== null);
|
||||
const fallback = known.length ? known.reduce((a, b) => a + b, 0) / known.length : 100 / Math.max(1, entries.length);
|
||||
sizes = sizes.map(v => v === null ? fallback : v);
|
||||
const total = sizes.reduce((a, b) => a + b, 0) || 1;
|
||||
const pcts = sizes.map(v => v / total * 100);
|
||||
entries.forEach((entry, i) => {
|
||||
if (resizable && i > 0) {
|
||||
const divider = document.createElement("div");
|
||||
@ -1175,8 +1184,8 @@ function renderWidgetOrderStrip(strip, entries) {
|
||||
item.className = "wos-item";
|
||||
item.dataset.id = entry.id;
|
||||
if (resizable) {
|
||||
item.dataset.size = entry.size == null ? (100 / entries.length) : entry.size;
|
||||
item.style.flexBasis = item.dataset.size + "%";
|
||||
item.dataset.size = pcts[i];
|
||||
item.style.flexBasis = pcts[i] + "%";
|
||||
}
|
||||
item.draggable = true;
|
||||
item.textContent = widgetDisplayName(entry.id);
|
||||
@ -1268,7 +1277,6 @@ 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 }) => {
|
||||
@ -1289,48 +1297,78 @@ function setupWidgetPosition() {
|
||||
});
|
||||
}
|
||||
|
||||
// Widget height settings (px). 0 / blank means auto.
|
||||
function setupWidgetHeightInput(id, initial) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const parse = (v) => {
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||
};
|
||||
el.value = parse(initial) || "";
|
||||
el.addEventListener("change", function () {
|
||||
const n = Math.min(2000, Math.max(0, parseInt(this.value, 10) || 0));
|
||||
this.value = n > 0 ? n : "";
|
||||
chrome.storage.sync.set({ [id]: n });
|
||||
});
|
||||
}
|
||||
|
||||
// Hours/minutes/seconds widgets settings (Focus Timer / Breathing presets).
|
||||
// The hours field is optional — Breathing only exposes minutes/seconds.
|
||||
// Fields are compact h:mm:ss inputs: click to edit a segment, typing a second
|
||||
// digit auto-advances, and the value is saved (zero-padded) on blur/change.
|
||||
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;
|
||||
if (!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 fields = [hEl, mEl, sEl].filter(Boolean);
|
||||
const pad = (n) => String(Math.max(0, parseInt(n, 10) || 0)).padStart(2, "0");
|
||||
if (hEl) hEl.value = pad(v.h);
|
||||
mEl.value = pad(v.m);
|
||||
sEl.value = pad(v.s);
|
||||
|
||||
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);
|
||||
let h = hEl ? clamp(hEl, 99) : 0;
|
||||
let m = clamp(mEl, 59);
|
||||
let 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;
|
||||
h = hEl ? d.h : 0; m = d.m; s = d.s;
|
||||
}
|
||||
hEl.value = h; mEl.value = m; sEl.value = s;
|
||||
if (hEl) hEl.value = pad(h);
|
||||
mEl.value = pad(m);
|
||||
sEl.value = pad(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() });
|
||||
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) fields[i + 1].focus();
|
||||
});
|
||||
inp.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") { e.preventDefault(); save(); inp.blur(); }
|
||||
else if (e.key === "ArrowRight" && i < fields.length - 1) { e.preventDefault(); fields[i + 1].focus(); }
|
||||
else if (e.key === "ArrowLeft" && i > 0) { e.preventDefault(); fields[i - 1].focus(); }
|
||||
});
|
||||
inp.addEventListener("change", save);
|
||||
inp.addEventListener("blur", save);
|
||||
});
|
||||
}
|
||||
|
||||
// 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", "focus_timer", "music", "breathing", "daily_quotes", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"];
|
||||
const togglesWithSubOptions = ["dashboard_notes", "gpa_calc", "focus_timer", "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;
|
||||
@ -1735,8 +1773,6 @@ function setup() {
|
||||
"customBackgroundNasaDaily",
|
||||
"fitImageToScreen",
|
||||
"card_transparency",
|
||||
"music_thumb_bg",
|
||||
"music_no_thumb",
|
||||
"customCardStyles",
|
||||
"card_grid",
|
||||
"card_grid_center_rows",
|
||||
@ -1931,6 +1967,30 @@ function setup() {
|
||||
identifier: "anon_progress_total",
|
||||
setup: (initial) => setupAnonProgressInput("anon_progress_total", initial, false),
|
||||
},
|
||||
{
|
||||
identifier: "widget_row_height",
|
||||
setup: (initial) => setupWidgetHeightInput("widget_row_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "dashboard_notes_height",
|
||||
setup: (initial) => setupWidgetHeightInput("dashboard_notes_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "gpa_calc_height",
|
||||
setup: (initial) => setupWidgetHeightInput("gpa_calc_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "focus_timer_height",
|
||||
setup: (initial) => setupWidgetHeightInput("focus_timer_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "breathing_height",
|
||||
setup: (initial) => setupWidgetHeightInput("breathing_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "daily_quotes_height",
|
||||
setup: (initial) => setupWidgetHeightInput("daily_quotes_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "focus_timer_preset_1",
|
||||
setup: (initial) => setupWidgetTimeInput("focus_timer_preset_1", initial),
|
||||
@ -1955,18 +2015,6 @@ function setup() {
|
||||
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),
|
||||
@ -2012,7 +2060,7 @@ function setup() {
|
||||
chrome.storage.sync.set({ "hide_course_images": false });
|
||||
}
|
||||
toggleSubOptionsVisibility(option, status);
|
||||
if (option === "dashboard_notes" || option === "gpa_calc" || option === "focus_timer" || option === "music" || option === "breathing" || option === "daily_quotes") {
|
||||
if (option === "dashboard_notes" || option === "gpa_calc" || option === "focus_timer" || option === "breathing" || option === "daily_quotes") {
|
||||
if (refreshWidgetOrder) refreshWidgetOrder();
|
||||
}
|
||||
});
|
||||
@ -2022,7 +2070,7 @@ function setup() {
|
||||
chrome.storage.sync.get("todo_celebrations_enabled", v => {
|
||||
toggleCelebrationOptionsVisibility(v["todo_celebrations_enabled"] !== false);
|
||||
});
|
||||
["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => {
|
||||
["dashboard_notes", "gpa_calc", "focus_timer", "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);
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
}
|
||||
],
|
||||
"permissions": ["storage", "unlimitedStorage"],
|
||||
"host_permissions": ["https://stoic.tekloon.net/*"],
|
||||
"default_locale": "en",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user