mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 22:07:18 +01:00
base for more widgets
This commit is contained in:
parent
1ee82a68b0
commit
b05f9b58c8
@ -140,6 +140,30 @@
|
||||
"widget_short_gpa": {
|
||||
"message": "GPA"
|
||||
},
|
||||
"focus_timer": {
|
||||
"message": "Focus Timer"
|
||||
},
|
||||
"breathing_breaks": {
|
||||
"message": "Breathing Breaks"
|
||||
},
|
||||
"music": {
|
||||
"message": "Music"
|
||||
},
|
||||
"daily_quotes": {
|
||||
"message": "Daily Quotes"
|
||||
},
|
||||
"widget_short_focus": {
|
||||
"message": "Focus"
|
||||
},
|
||||
"widget_short_music": {
|
||||
"message": "Music"
|
||||
},
|
||||
"widget_short_breathing": {
|
||||
"message": "Breathing"
|
||||
},
|
||||
"widget_short_quotes": {
|
||||
"message": "Quotes"
|
||||
},
|
||||
"grade_settings": {
|
||||
"message": "Grade Settings"
|
||||
},
|
||||
|
||||
144
css/content.css
144
css/content.css
@ -70,7 +70,7 @@
|
||||
.canvasrefined-course-percent, .canvasrefined-course-credit {border: 1px solid #ccc; color: var(--ic-brand-font-color-dark)}
|
||||
|
||||
.canvasrefined-course-percent, .canvasrefined-course-weight, .canvasrefined-course-credit {
|
||||
height: 22px; text-align: center;border-radius: 3px;width: 50px; font-size: 13px; padding: 0;
|
||||
height: 22px; text-align: center;border-radius: 3px;width: 150px; font-size: 13px; padding: 0;
|
||||
}
|
||||
|
||||
.canvasrefined-course-weight {
|
||||
@ -548,3 +548,145 @@
|
||||
.cr-gpa-body {grid-template-columns:1fr;}
|
||||
.cr-gpa-pane-scale {border-left:none;border-top:1px solid var(--bcborders,#c7cdd1);}
|
||||
}
|
||||
|
||||
/* ===== Generic dashboard widgets: Focus Timer, Music, Breathing, Quotes =====
|
||||
Shared shell follows the same card look as Notes/GPA: 262px grid cell with
|
||||
the 36px Canvas gutter when placed before/after the course cards, and a
|
||||
full-width flowing card when placed in the top/bottom container row. */
|
||||
.canvasrefined-widget-card {
|
||||
width: 262px;
|
||||
min-height: 200px;
|
||||
box-sizing: border-box;
|
||||
background: var(--bcbackground-1,#fff);
|
||||
color: var(--bctext-0,#2d3b45);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
}
|
||||
.canvasrefined-widget-card.in-cards {
|
||||
margin: 36px 0 0 36px;
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
}
|
||||
.canvasrefined-widget-card.container-mode {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.canvasrefined-widget-slot .canvasrefined-widget-card {width:100%;margin:0;height:100%;}
|
||||
.cr-widget-head {display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid var(--bcborders,#c7cdd1);}
|
||||
.cr-widget-title {font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-widget-btn {flex:1;border:none;border-radius:8px;padding:9px 12px;font:inherit;font-weight:600;font-size:13px;cursor:pointer;background:var(--ic-brand-primary,#0374b5);color:#fff;}
|
||||
.cr-widget-btn:hover {filter:brightness(1.07);}
|
||||
.cr-widget-btn-ghost {background:transparent;color:var(--bctext-0,#2d3b45);border:1px solid var(--bcborders,#c7cdd1);}
|
||||
/* Timer engine (Focus Timer + Breathing Breaks) */
|
||||
.cr-time-box {position:relative;margin:14px 14px 0;border-radius:12px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 6%, transparent);border:1px solid var(--bcborders,#c7cdd1);overflow:hidden;}
|
||||
/* The whole time rectangle is the progress bar, tinted with the dark-mode
|
||||
link color so it matches the active theme in both light and dark mode. */
|
||||
.cr-time-box .cr-timer-progress-bar {position:absolute;left:0;top:0;bottom:0;width:0;background:color-mix(in srgb, var(--bclinks,#0374b5) 32%, transparent);border-right:2px solid color-mix(in srgb, var(--bclinks,#0374b5) 80%, transparent);}
|
||||
.cr-timer-time {position:relative;z-index:1;font-size:44px;font-weight:700;text-align:center;letter-spacing:.02em;font-variant-numeric:tabular-nums;padding:22px 14px;line-height:1;color:var(--bctext-0,#2d3b45);}
|
||||
.cr-breath-display {padding:12px 14px 6px;}
|
||||
.cr-timer-presets {display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:12px 14px 0;}
|
||||
.cr-timer-preset {border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-0,#2d3b45);border-radius:8px;padding:8px;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;}
|
||||
.cr-timer-preset:hover {border-color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-timer-preset.active {background:color-mix(in srgb, var(--bclinks,#0374b5) 18%, transparent);border-color:var(--bclinks,#0374b5);color:var(--bclinks,#0374b5);}
|
||||
.cr-timer-custom-row {display:flex;gap:8px;padding:10px 14px 0;align-items:center;}
|
||||
.cr-timer-custom-row[hidden] {display:none;}
|
||||
.cr-time-fields {display:flex;gap:6px;flex:1;min-width:0;}
|
||||
.cr-tf-box {display:flex;align-items:center;gap:2px;flex:1;min-width:0;border:1px solid var(--bcborders,#c7cdd1);border-radius:8px;padding:4px 8px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 5%, transparent);}
|
||||
.cr-tf-box:focus-within {border-color:var(--bclinks,#0374b5);box-shadow:0 0 0 2px color-mix(in srgb, var(--bclinks,#0374b5) 22%, transparent);}
|
||||
.cr-tf {width:100%;min-width:0;border:none;background:transparent;color:var(--bctext-0,#2d3b45);font:inherit;font-size:14px;font-weight:600;text-align:center;padding:2px 0;}
|
||||
.cr-tf:focus {outline:none;}
|
||||
.cr-tf::-webkit-outer-spin-button,.cr-tf::-webkit-inner-spin-button {-webkit-appearance:none;margin:0;}
|
||||
.cr-tf-unit {font-size:10px;font-weight:700;color:var(--bctext-2,#9aa3ab);text-transform:uppercase;}
|
||||
.cr-timer-custom-row .cr-widget-btn {flex:none;padding:8px 14px;}
|
||||
.cr-timer-controls {display:flex;gap:8px;padding:12px 14px 14px;}
|
||||
/* Breathing Breaks: 4s in / 6s out, colour shifts green -> blue when exhaling */
|
||||
.canvasrefined-breathing {background:linear-gradient(160deg, color-mix(in srgb, #6ea8ff 14%, var(--bcbackground-1,#fff)), var(--bcbackground-1,#fff));}
|
||||
.cr-breath-stage {display:flex;align-items:center;justify-content:center;padding:20px 14px 6px;}
|
||||
.cr-breath-circle {position:relative;width:112px;height:112px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:radial-gradient(circle at 50% 40%, #74e3c4, #2fb595);box-shadow:0 0 18px 2px rgba(65,209,175,.4);}
|
||||
.cr-breath-circle::before {content:"";position:absolute;inset:0;border-radius:50%;background:radial-gradient(circle at 50% 40%, #93a8ff, #536ee0);opacity:0;box-shadow:0 0 26px 5px rgba(101,124,255,.45);}
|
||||
.canvasrefined-breathing.is-running .cr-breath-circle {animation: cr-breath-pulse 10s ease-in-out infinite, cr-breath-glow 10s ease-in-out infinite;}
|
||||
.canvasrefined-breathing.is-running .cr-breath-circle::before {animation: cr-breath-tint 10s ease-in-out infinite;}
|
||||
@keyframes cr-breath-pulse {
|
||||
0% {transform:scale(.68);}
|
||||
40% {transform:scale(1);}
|
||||
100% {transform:scale(.68);}
|
||||
}
|
||||
@keyframes cr-breath-glow {
|
||||
0% {box-shadow:0 0 16px 2px rgba(65,209,175,.35);}
|
||||
40% {box-shadow:0 0 36px 9px rgba(65,209,175,.65);}
|
||||
100% {box-shadow:0 0 16px 2px rgba(65,209,175,.35);}
|
||||
}
|
||||
@keyframes cr-breath-tint {
|
||||
0%,38% {opacity:0;}
|
||||
60% {opacity:.95;}
|
||||
100% {opacity:0;}
|
||||
}
|
||||
.cr-breath-phase,.cr-breath-idle {position:absolute;z-index:1;font-size:12px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.32);}
|
||||
.cr-breath-phase {opacity:0;}
|
||||
.cr-breath-idle {opacity:1;}
|
||||
.canvasrefined-breathing.is-running .cr-breath-idle {opacity:0;}
|
||||
.canvasrefined-breathing.is-running .cr-breath-phase-in {animation: cr-breath-in-label 10s ease-in-out infinite;}
|
||||
.canvasrefined-breathing.is-running .cr-breath-phase-out {animation: cr-breath-out-label 10s ease-in-out infinite;}
|
||||
@keyframes cr-breath-in-label {0%{opacity:1;}38%{opacity:1;}44%{opacity:0;}100%{opacity:0;}}
|
||||
@keyframes cr-breath-out-label {0%{opacity:0;}38%{opacity:0;}44%{opacity:1;}94%{opacity:1;}100%{opacity:0;}}
|
||||
/* Music: mini player + slide-in library switcher (visual only) */
|
||||
.cr-music-list-btn {display:inline-flex;align-items:center;gap:5px;border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-0,#2d3b45);border-radius:999px;padding:4px 10px;font:inherit;font-size:11px;font-weight:600;cursor:pointer;}
|
||||
.cr-music-list-btn:hover {border-color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-music-caret {transition:transform .2s;}
|
||||
.canvasrefined-music.is-drawer-open .cr-music-caret {transform:rotate(180deg);}
|
||||
.canvasrefined-music {position:relative;min-height:320px;}
|
||||
.canvasrefined-music.container-mode {min-height:320px;}
|
||||
.cr-player {display:flex;flex-direction:column;gap:12px;padding:16px 14px 10px;flex:1;justify-content:center;}
|
||||
.cr-player-thumb {display:none;position:relative;height:104px;border-radius:12px;overflow:hidden;background:linear-gradient(135deg, color-mix(in srgb, var(--cr-thumb-color,#0374b5) 82%, #000 8%), color-mix(in srgb, var(--cr-thumb-color,#0374b5) 38%, var(--bcbackground-1,#fff)));}
|
||||
.cr-player-thumb-mark {position:absolute;right:10px;bottom:8px;color:rgba(255,255,255,.55);line-height:0;}
|
||||
.canvasrefined-music.thumb-bg .cr-player-thumb {display:block;}
|
||||
.canvasrefined-music.thumb-bg .cr-player-art {display:none;}
|
||||
.canvasrefined-music.no-thumb .cr-player-thumb,
|
||||
.canvasrefined-music.no-thumb .cr-player-art {display:none;}
|
||||
.cr-player-body {display:flex;align-items:center;gap:12px;}
|
||||
.cr-player-art {flex:none;width:56px;height:56px;border-radius:14px;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb, var(--bclinks,#0374b5) 16%, transparent);color:var(--bclinks,#0374b5);}
|
||||
.cr-player-info {min-width:0;flex:1;}
|
||||
.cr-player-title {font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.cr-player-sub {font-size:11.5px;color:var(--bctext-2,#9aa3ab);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.cr-player-progress {height:5px;margin:0 14px;border-radius:999px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 12%, transparent);overflow:hidden;}
|
||||
.cr-player-progress-fill {height:100%;width:0;background:var(--bclinks,#0374b5);border-radius:999px;}
|
||||
.cr-player-times {display:flex;justify-content:space-between;padding:5px 16px 0;font-size:10.5px;color:var(--bctext-2,#9aa3ab);font-variant-numeric:tabular-nums;}
|
||||
.cr-player-controls {display:flex;align-items:center;justify-content:center;gap:16px;padding:10px 14px 14px;}
|
||||
.cr-player-btn {display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:transparent;color:var(--bctext-0,#2d3b45);cursor:pointer;}
|
||||
.cr-player-btn:hover {background:color-mix(in srgb, var(--bctext-0,#2d3b45) 8%, transparent);}
|
||||
.cr-player-play {width:46px;height:46px;background:var(--bclinks,#0374b5);color:#fff;}
|
||||
.cr-player-play:hover {background:var(--bclinks,#0374b5);filter:brightness(1.08);}
|
||||
/* Slide-in library drawer: platforms / playlists / songs */
|
||||
.cr-music-drawer {position:absolute;inset:0;z-index:5;display:flex;flex-direction:column;background:var(--bcbackground-1,#fff);color:var(--bctext-0,#2d3b45);transform:translateX(102%);transition:transform .28s ease;visibility:hidden;box-shadow:-10px 0 26px rgba(0,0,0,.2);}
|
||||
.canvasrefined-music.is-drawer-open .cr-music-drawer {transform:translateX(0);visibility:visible;}
|
||||
.cr-drawer-head {display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--bcborders,#c7cdd1);}
|
||||
.cr-drawer-title {font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-drawer-close {border:none;background:transparent;color:var(--bctext-2,#9aa3ab);font-size:20px;line-height:1;cursor:pointer;padding:0 5px;border-radius:6px;}
|
||||
.cr-drawer-close:hover {color:var(--bctext-0,#2d3b45);background:color-mix(in srgb, var(--bctext-0,#2d3b45) 8%, transparent);}
|
||||
.cr-drawer-tabs {display:flex;gap:6px;padding:8px 12px;border-bottom:1px solid var(--bcborders,#c7cdd1);}
|
||||
.cr-drawer-tab {flex:1;border:1px solid transparent;background:transparent;color:var(--bctext-2,#9aa3ab);border-radius:999px;padding:6px 8px;font:inherit;font-size:11.5px;font-weight:600;cursor:pointer;}
|
||||
.cr-drawer-tab:hover {color:var(--bctext-0,#2d3b45);}
|
||||
.cr-drawer-tab.active {background:color-mix(in srgb, var(--bclinks,#0374b5) 16%, transparent);color:var(--bclinks,#0374b5);border-color:color-mix(in srgb, var(--bclinks,#0374b5) 45%, transparent);}
|
||||
.cr-drawer-body {flex:1;overflow-y:auto;padding:8px 10px 12px;}
|
||||
.cr-drawer-pane[hidden] {display:none;}
|
||||
.cr-drawer-row {display:flex;align-items:center;gap:10px;padding:8px;border-radius:9px;}
|
||||
.cr-drawer-row:hover {background:color-mix(in srgb, var(--bctext-0,#2d3b45) 6%, transparent);}
|
||||
.cr-drawer-icon {flex:none;display:flex;line-height:0;}
|
||||
.cr-drawer-icon-pl {opacity:.85;}
|
||||
.cr-drawer-meta {display:flex;flex-direction:column;min-width:0;flex:1;}
|
||||
.cr-drawer-name {font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.cr-drawer-sub {font-size:11px;color:var(--bctext-2,#9aa3ab);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.cr-drawer-chevron {flex:none;color:var(--bctext-2,#9aa3ab);font-size:16px;}
|
||||
.cr-drawer-index,.cr-drawer-len {flex:none;font-size:11px;color:var(--bctext-2,#9aa3ab);font-variant-numeric:tabular-nums;}
|
||||
.cr-drawer-index {width:16px;text-align:right;}
|
||||
/* Daily Quote: looks only for now */
|
||||
.canvasrefined-quotes {background:linear-gradient(150deg, color-mix(in srgb, #b98cff 14%, var(--bcbackground-1,#fff)), var(--bcbackground-1,#fff));}
|
||||
.cr-quote-body {padding:14px 16px 16px;display:flex;flex-direction:column;gap:6px;flex:1;justify-content:center;}
|
||||
.cr-quote-mark {font-size:24px;line-height:.5;font-weight:800;color:color-mix(in srgb, var(--bclinks,#0374b5) 55%, transparent);}
|
||||
.cr-quote-text {margin:0;font-size:15px;line-height:1.5;font-style:italic;color:var(--bctext-0,#2d3b45);}
|
||||
.cr-quote-author {font-size:12px;font-weight:600;color:var(--bctext-2,#9aa3ab);align-self:flex-end;}
|
||||
|
||||
152
html/popup.html
152
html/popup.html
@ -1187,6 +1187,158 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="focus_timer">
|
||||
<input type="radio" id="off" name="focus_timer">
|
||||
<input type="radio" id="on" name="focus_timer">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</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">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="breathing">
|
||||
<input type="radio" id="off" name="breathing">
|
||||
<input type="radio" id="on" name="breathing">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</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">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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">
|
||||
<input type="radio" id="on" name="daily_quotes">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -42,6 +42,25 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
"dashboard_notes_mode": "edit",
|
||||
"dashboard_notes_position": "top",
|
||||
"widget_order": {},
|
||||
"focus_timer": false,
|
||||
"focus_timer_position": "cards_end",
|
||||
"focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 },
|
||||
"focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 },
|
||||
"focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 },
|
||||
"music": false,
|
||||
"music_position": "cards_end",
|
||||
"music_spotify_url": "",
|
||||
"music_apple_url": "",
|
||||
"music_youtube_url": "",
|
||||
"music_thumb_bg": true,
|
||||
"music_no_thumb": false,
|
||||
"breathing": false,
|
||||
"breathing_position": "cards_end",
|
||||
"breathing_preset_1": { "h": 0, "m": 3, "s": 0 },
|
||||
"breathing_preset_2": { "h": 0, "m": 5, "s": 0 },
|
||||
"breathing_preset_3": { "h": 0, "m": 10, "s": 0 },
|
||||
"daily_quotes": false,
|
||||
"daily_quotes_position": "top",
|
||||
"better_todo": true,
|
||||
"todo_hr24": false,
|
||||
"todo_separate_scrollbar": false,
|
||||
|
||||
612
js/content.js
612
js/content.js
@ -1026,6 +1026,33 @@ function applyOptionsChanges(changes) {
|
||||
case "dashboard_notes_position":
|
||||
loadDashboardNotes();
|
||||
break;
|
||||
case "focus_timer":
|
||||
case "focus_timer_position":
|
||||
case "focus_timer_preset_1":
|
||||
case "focus_timer_preset_2":
|
||||
case "focus_timer_preset_3":
|
||||
loadFocusTimer();
|
||||
break;
|
||||
case "music":
|
||||
case "music_position":
|
||||
case "music_spotify_url":
|
||||
case "music_apple_url":
|
||||
case "music_youtube_url":
|
||||
case "music_thumb_bg":
|
||||
case "music_no_thumb":
|
||||
loadMusicWidget();
|
||||
break;
|
||||
case "breathing":
|
||||
case "breathing_position":
|
||||
case "breathing_preset_1":
|
||||
case "breathing_preset_2":
|
||||
case "breathing_preset_3":
|
||||
loadBreathingWidget();
|
||||
break;
|
||||
case "daily_quotes":
|
||||
case "daily_quotes_position":
|
||||
loadDailyQuotesWidget();
|
||||
break;
|
||||
case "widget_order":
|
||||
["top", "below"].forEach(area => {
|
||||
const row = document.querySelector(".canvasrefined-widget-row[data-area='" + area + "']");
|
||||
@ -1038,6 +1065,7 @@ function applyOptionsChanges(changes) {
|
||||
// behind would keep the old layout until reload.
|
||||
if (options.dashboard_notes === true) loadDashboardNotes();
|
||||
if (options.gpa_calc === true && current_page === "/") setupGPACalc();
|
||||
loadAllSimpleWidgets();
|
||||
break;
|
||||
case "dashboard_grades":
|
||||
case "grade_hover":
|
||||
@ -1917,6 +1945,7 @@ function checkDashboardReady() {
|
||||
insertGrades();
|
||||
loadDashboardNotes();
|
||||
setupGPACalc();
|
||||
loadAllSimpleWidgets();
|
||||
showUpdateMsg();
|
||||
createNasaInfoOverlay();
|
||||
}
|
||||
@ -7856,6 +7885,14 @@ function placeGPAElements(dashboardContainer, container, container2) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every dashboard widget known to the placement system. Kept in one place so
|
||||
// the ordering/filtering helpers don't each hardcode their own list.
|
||||
const CR_WIDGET_IDS = ["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes"];
|
||||
// Widgets rendered by the generic card loader (single root element, no GPA-style
|
||||
// paired elements).
|
||||
const CR_SIMPLE_WIDGET_IDS = ["focus_timer", "music", "breathing", "daily_quotes"];
|
||||
function isCRWidget(id) { return CR_WIDGET_IDS.indexOf(id) !== -1; }
|
||||
|
||||
// Shared full-width container ("top"/"below" positions): widgets live in a
|
||||
// flex row; each widget's slot width is the % chosen in the Widgets tab.
|
||||
function containerWidgetOrder(area) {
|
||||
@ -7868,7 +7905,7 @@ function containerWidgetOrder(area) {
|
||||
return { id: e.id, size: Number.isFinite(n) && n > 0 ? n : null };
|
||||
}
|
||||
return null;
|
||||
}).filter(e => e && (e.id === "dashboard_notes" || e.id === "gpa_calc"));
|
||||
}).filter(e => e && isCRWidget(e.id));
|
||||
}
|
||||
|
||||
// Card-grid order ("Before cards"/"After cards" positions): saved order of
|
||||
@ -7881,7 +7918,7 @@ function cardWidgetOrder() {
|
||||
if (typeof e === "string") return e;
|
||||
if (e && typeof e === "object" && e.id) return e.id;
|
||||
return null;
|
||||
}).filter(id => id === "dashboard_notes" || id === "gpa_calc");
|
||||
}).filter(id => isCRWidget(id));
|
||||
}
|
||||
|
||||
// Normalize the relative order of card-shaped widgets inside the card grid.
|
||||
@ -7893,6 +7930,14 @@ function applyCardWidgetOrder(grid) {
|
||||
if (!grid) return;
|
||||
const saved = cardWidgetOrder();
|
||||
const units = [];
|
||||
// Simple card widgets (focus timer, music, breathing, quotes) are a
|
||||
// single element each, so they join the ordering straight from the DOM.
|
||||
grid.querySelectorAll(":scope > .canvasrefined-widget-card[data-cr-widget]").forEach(el => {
|
||||
if (!el.classList.contains("in-cards")) return;
|
||||
const id = el.dataset.crWidget;
|
||||
if (CR_SIMPLE_WIDGET_IDS.indexOf(id) === -1) return;
|
||||
units.push({ id, anchor: (options[id + "_position"] === "cards_start") ? "start" : "end", els: [el] });
|
||||
});
|
||||
const notes = grid.querySelector(":scope > .canvasrefined-dashboard-notes");
|
||||
if (notes && notes.classList.contains("in-cards")) {
|
||||
units.push({
|
||||
@ -8635,6 +8680,569 @@ function loadDashboardNotes() {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Generic dashboard widgets (Focus Timer, Music, Breathing Breaks, Daily Quote)
|
||||
|
||||
These share the placement/ordering machinery built for Dashboard Notes and the
|
||||
GPA calculator, but render from one element each (see CR_SIMPLE_WIDGET_IDS).
|
||||
*/
|
||||
|
||||
const CR_WIDGET_DEFAULT_POSITION = {
|
||||
dashboard_notes: "top",
|
||||
gpa_calc: "cards_end",
|
||||
focus_timer: "cards_end",
|
||||
music: "cards_end",
|
||||
breathing: "cards_end",
|
||||
daily_quotes: "top",
|
||||
};
|
||||
|
||||
function crWidgetPosition(id) {
|
||||
const v = options[id + "_position"];
|
||||
if (v === "top" || v === "below" || v === "cards_start" || v === "cards_end") return v;
|
||||
return CR_WIDGET_DEFAULT_POSITION[id] || "cards_end";
|
||||
}
|
||||
|
||||
// Move a single-element widget to its configured spot, mirroring the Notes/GPA
|
||||
// placement rules: top/below join the shared full-width row, cards_* sit in the
|
||||
// course-card grid before/after the cards.
|
||||
function placeSimpleWidget(id, el, container) {
|
||||
const pos = crWidgetPosition(id);
|
||||
if (pos === "top" || pos === "below") {
|
||||
el.classList.remove("in-cards");
|
||||
el.classList.add("container-mode");
|
||||
const slot = el.closest && el.closest(".canvasrefined-widget-slot");
|
||||
const row = slot && slot.parentNode;
|
||||
if (el.dataset.crPos === pos && row && row.dataset.area === pos && slot.dataset.widget === id) return;
|
||||
placeWidgetInRow(id, pos, [el], container);
|
||||
} else {
|
||||
el.classList.add("in-cards");
|
||||
el.classList.remove("container-mode");
|
||||
const grid = document.querySelector(".ic-DashboardCard__box__container") || container;
|
||||
if (el.dataset.crPos === pos && el.parentNode === grid) return;
|
||||
releaseWidgetFromRow(id, [el], container);
|
||||
if (pos === "cards_start") grid.prepend(el);
|
||||
else grid.appendChild(el);
|
||||
applyCardWidgetOrder(grid);
|
||||
try {
|
||||
if (grid !== container && window.jQuery && window.jQuery.fn && window.jQuery.fn.sortable) {
|
||||
window.jQuery(grid).sortable('refresh');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
el.dataset.crPos = pos;
|
||||
}
|
||||
|
||||
// Create (once), show/hide and place one generic widget from its spec.
|
||||
function crLoadWidget(id, spec) {
|
||||
const existing = document.querySelector("." + spec.className);
|
||||
if (options[id] !== true) {
|
||||
if (existing) existing.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const container = document.querySelector("#DashboardCard_Container");
|
||||
if (!container) return;
|
||||
let el = existing;
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.className = "canvasrefined-widget-card " + spec.className;
|
||||
el.dataset.crWidget = id;
|
||||
el.innerHTML = spec.html;
|
||||
try { if (spec.wire) spec.wire(el); } catch (e) { logError(e); }
|
||||
} else {
|
||||
el.style.display = "";
|
||||
try { if (spec.update) spec.update(el); } catch (e) { logError(e); }
|
||||
}
|
||||
placeSimpleWidget(id, el, container);
|
||||
}
|
||||
|
||||
/* ---- Shared timer engine (Focus Timer + Breathing Breaks) ---- */
|
||||
|
||||
const crTimers = {};
|
||||
function crTimer(id) {
|
||||
if (!crTimers[id]) crTimers[id] = { total: 0, remaining: 0, running: false, raf: null, endTime: 0 };
|
||||
return crTimers[id];
|
||||
}
|
||||
// Countdown readout: ceil so a fresh 25:00 only shows 24:59 after a full second.
|
||||
function crTimerFormat(sec) {
|
||||
const s = Math.max(0, Math.ceil(sec - 0.001));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const r = s % 60;
|
||||
if (h > 0) return h + ":" + String(m).padStart(2, "0") + ":" + String(r).padStart(2, "0");
|
||||
return String(m).padStart(2, "0") + ":" + String(r).padStart(2, "0");
|
||||
}
|
||||
// Human label for a preset button ("15 min", "1h 30m", "45s").
|
||||
function crFormatDuration(sec) {
|
||||
const s = Math.max(0, Math.round(sec));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const r = s % 60;
|
||||
if (h && m) return h + "h " + m + "m";
|
||||
if (h) return h + "h" + (r ? " " + r + "s" : "");
|
||||
if (m && r) return m + "m " + r + "s";
|
||||
if (m) return m + " min";
|
||||
return r + "s";
|
||||
}
|
||||
function crTimerCancel(st) {
|
||||
if (st && st.raf) { cancelAnimationFrame(st.raf); st.raf = null; }
|
||||
}
|
||||
function crTimerRender(id, el) {
|
||||
const st = crTimer(id);
|
||||
const disp = el.querySelector(".cr-timer-time") || el.querySelector(".cr-timer-display");
|
||||
if (disp) {
|
||||
const txt = crTimerFormat(st.remaining);
|
||||
if (disp.textContent !== txt) disp.textContent = txt;
|
||||
}
|
||||
const start = el.querySelector(".cr-timer-start");
|
||||
if (start) {
|
||||
const label = st.running ? "Pause" : "Start";
|
||||
if (start.textContent !== label) start.textContent = label;
|
||||
}
|
||||
el.classList.toggle("is-running", st.running);
|
||||
const bar = el.querySelector(".cr-timer-progress-bar");
|
||||
if (bar) bar.style.width = (st.total ? ((st.total - st.remaining) / st.total) * 100 : 0) + "%";
|
||||
const presets = [...el.querySelectorAll(".cr-timer-preset[data-slot]")];
|
||||
const totals = presets.map(b => parseInt(b.dataset.sec, 10));
|
||||
const matched = totals.indexOf(st.total) !== -1;
|
||||
presets.forEach((btn, i) => btn.classList.toggle("active", !st.running && totals[i] === st.total));
|
||||
const customBtn = el.querySelector(".cr-timer-custom");
|
||||
if (customBtn) customBtn.classList.toggle("active", !st.running && !matched && st.total > 0);
|
||||
}
|
||||
// rAF loop: the bar (and the readout) are driven by real elapsed time, so the
|
||||
// fill moves continuously instead of stepping once a second.
|
||||
function crTimerFrame(id, el) {
|
||||
const st = crTimer(id);
|
||||
if (!st.running) return;
|
||||
st.remaining = Math.max(0, (st.endTime - Date.now()) / 1000);
|
||||
if (st.remaining <= 0) {
|
||||
st.remaining = 0;
|
||||
st.running = false;
|
||||
st.raf = null;
|
||||
}
|
||||
crTimerRender(id, el);
|
||||
if (st.running) st.raf = requestAnimationFrame(() => crTimerFrame(id, el));
|
||||
}
|
||||
function crTimerStart(id, el) {
|
||||
const st = crTimer(id);
|
||||
if (st.running || st.remaining <= 0) return;
|
||||
st.running = true;
|
||||
st.endTime = Date.now() + st.remaining * 1000;
|
||||
crTimerCancel(st);
|
||||
st.raf = requestAnimationFrame(() => crTimerFrame(id, el));
|
||||
crTimerRender(id, el);
|
||||
}
|
||||
function crTimerPause(id, el) {
|
||||
const st = crTimer(id);
|
||||
if (st.running) st.remaining = Math.max(0, (st.endTime - Date.now()) / 1000);
|
||||
st.running = false;
|
||||
crTimerCancel(st);
|
||||
crTimerRender(id, el);
|
||||
}
|
||||
function crTimerSet(id, el, seconds) {
|
||||
const st = crTimer(id);
|
||||
st.running = false;
|
||||
crTimerCancel(st);
|
||||
st.total = Math.max(1, Math.round(seconds));
|
||||
st.remaining = st.total;
|
||||
crTimerRender(id, el);
|
||||
}
|
||||
function crTimerReset(id, el) {
|
||||
const st = crTimer(id);
|
||||
st.running = false;
|
||||
crTimerCancel(st);
|
||||
st.remaining = st.total;
|
||||
crTimerRender(id, el);
|
||||
}
|
||||
// Only the three preset slots are configurable; Custom is typed on the widget.
|
||||
function crTimerApplyCfg(el, cfg) {
|
||||
el.querySelectorAll(".cr-timer-preset[data-slot]").forEach(btn => {
|
||||
const slot = parseInt(btn.dataset.slot, 10);
|
||||
const sec = cfg.presets[slot - 1];
|
||||
btn.dataset.sec = sec;
|
||||
btn.textContent = crFormatDuration(sec);
|
||||
});
|
||||
}
|
||||
function crTimerHideCustom(el) {
|
||||
const row = el.querySelector(".cr-timer-custom-row");
|
||||
if (row) row.hidden = true;
|
||||
}
|
||||
// Read the h/m/s custom fields as a whole number of seconds.
|
||||
function crTimerCustomSeconds(el) {
|
||||
const num = (sel) => {
|
||||
const v = parseInt((el.querySelector(sel) || {}).value, 10);
|
||||
return Number.isFinite(v) && v > 0 ? v : 0;
|
||||
};
|
||||
const h = Math.min(99, num(".cr-tf-h"));
|
||||
const m = Math.min(59, num(".cr-tf-m"));
|
||||
const s = Math.min(59, num(".cr-tf-s"));
|
||||
return h * 3600 + m * 60 + s;
|
||||
}
|
||||
// Listeners are wired once (update passes only refresh config labels).
|
||||
function crTimerWireControls(id, el) {
|
||||
el.querySelectorAll(".cr-timer-preset[data-slot]").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const sec = parseInt(btn.dataset.sec, 10);
|
||||
if (Number.isFinite(sec) && sec > 0) crTimerSet(id, el, sec);
|
||||
crTimerHideCustom(el);
|
||||
});
|
||||
});
|
||||
|
||||
// Custom: reveal inline hours/minutes/seconds fields.
|
||||
const customBtn = el.querySelector(".cr-timer-custom");
|
||||
const row = el.querySelector(".cr-timer-custom-row");
|
||||
const applyCustom = () => {
|
||||
const sec = crTimerCustomSeconds(el);
|
||||
if (sec <= 0) {
|
||||
const mInput = el.querySelector(".cr-tf-m");
|
||||
if (mInput) { mInput.focus(); mInput.select(); }
|
||||
return;
|
||||
}
|
||||
crTimerSet(id, el, sec);
|
||||
crTimerHideCustom(el);
|
||||
};
|
||||
if (customBtn && row) customBtn.addEventListener("click", () => {
|
||||
if (!row.hidden) { crTimerHideCustom(el); return; }
|
||||
row.hidden = false;
|
||||
const mInput = el.querySelector(".cr-tf-m");
|
||||
if (mInput) { mInput.focus(); mInput.select(); }
|
||||
});
|
||||
el.querySelectorAll(".cr-timer-custom-row .cr-tf").forEach(inp => inp.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") { e.preventDefault(); applyCustom(); }
|
||||
else if (e.key === "Escape") { e.preventDefault(); crTimerHideCustom(el); }
|
||||
}));
|
||||
const go = el.querySelector(".cr-timer-custom-go");
|
||||
if (go) go.addEventListener("click", applyCustom);
|
||||
|
||||
const start = el.querySelector(".cr-timer-start");
|
||||
if (start) start.addEventListener("click", () => {
|
||||
const st = crTimer(id);
|
||||
if (st.running) crTimerPause(id, el);
|
||||
else crTimerStart(id, el);
|
||||
});
|
||||
const reset = el.querySelector(".cr-timer-reset");
|
||||
if (reset) reset.addEventListener("click", () => crTimerReset(id, el));
|
||||
}
|
||||
function crTimerInitial(id, el, cfg) {
|
||||
const st = crTimer(id);
|
||||
if (!st.total) { st.total = cfg.presets[1]; st.remaining = st.total; }
|
||||
crTimerRender(id, el);
|
||||
}
|
||||
// Presets now store {h,m,s}; legacy values were plain minutes, so a number is
|
||||
// read as minutes. `fallback` is in seconds.
|
||||
function crTimerReadSlots(prefix, fallback) {
|
||||
return [1, 2, 3].map(i => {
|
||||
const v = options[prefix + "_preset_" + i];
|
||||
if (v && typeof v === "object") {
|
||||
const h = Math.max(0, parseInt(v.h, 10) || 0);
|
||||
const m = Math.max(0, parseInt(v.m, 10) || 0);
|
||||
const s = Math.max(0, parseInt(v.s, 10) || 0);
|
||||
const total = h * 3600 + m * 60 + s;
|
||||
if (total > 0) return total;
|
||||
} else {
|
||||
const n = parseInt(v, 10);
|
||||
if (Number.isFinite(n) && n > 0) return n * 60;
|
||||
}
|
||||
return fallback[i - 1];
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Focus Timer ---- */
|
||||
|
||||
const FOCUS_TIMER_DEFAULT_PRESETS = [15 * 60, 25 * 60, 45 * 60];
|
||||
|
||||
function focusTimerCfg() {
|
||||
return { presets: crTimerReadSlots("focus_timer", FOCUS_TIMER_DEFAULT_PRESETS) };
|
||||
}
|
||||
|
||||
const FOCUS_TIMER_HTML = `
|
||||
<div class="cr-widget-head"><span class="cr-widget-title">Focus Timer</span></div>
|
||||
<div class="cr-time-box cr-focus-display">
|
||||
<div class="cr-time-fill cr-timer-progress-bar"></div>
|
||||
<div class="cr-timer-time cr-timer-display" aria-live="polite">25:00</div>
|
||||
</div>
|
||||
<div class="cr-timer-presets">
|
||||
<button type="button" class="cr-timer-preset" data-slot="1">15 min</button>
|
||||
<button type="button" class="cr-timer-preset" data-slot="2">25 min</button>
|
||||
<button type="button" class="cr-timer-preset" data-slot="3">45 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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
function wireFocusTimer(el) {
|
||||
crTimerApplyCfg(el, focusTimerCfg());
|
||||
crTimerWireControls("focus_timer", el);
|
||||
crTimerInitial("focus_timer", el, focusTimerCfg());
|
||||
}
|
||||
function updateFocusTimer(el) {
|
||||
crTimerApplyCfg(el, focusTimerCfg());
|
||||
crTimerRender("focus_timer", el);
|
||||
}
|
||||
function loadFocusTimer() {
|
||||
crLoadWidget("focus_timer", {
|
||||
className: "canvasrefined-focus-timer",
|
||||
html: FOCUS_TIMER_HTML,
|
||||
wire: wireFocusTimer,
|
||||
update: updateFocusTimer,
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Breathing Breaks (a zen timer) ---- */
|
||||
|
||||
const BREATHING_DEFAULT_PRESETS = [3 * 60, 5 * 60, 10 * 60];
|
||||
|
||||
function breathingCfg() {
|
||||
return { presets: crTimerReadSlots("breathing", BREATHING_DEFAULT_PRESETS) };
|
||||
}
|
||||
|
||||
const BREATHING_HTML = `
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
function wireBreathingWidget(el) {
|
||||
crTimerApplyCfg(el, breathingCfg());
|
||||
crTimerWireControls("breathing", el);
|
||||
crTimerInitial("breathing", el, breathingCfg());
|
||||
}
|
||||
function updateBreathingWidget(el) {
|
||||
crTimerApplyCfg(el, breathingCfg());
|
||||
crTimerRender("breathing", el);
|
||||
}
|
||||
function loadBreathingWidget() {
|
||||
crLoadWidget("breathing", {
|
||||
className: "canvasrefined-breathing",
|
||||
html: BREATHING_HTML,
|
||||
wire: wireBreathingWidget,
|
||||
update: updateBreathingWidget,
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Music (looks only) ---- */
|
||||
|
||||
const MUSIC_PLATFORMS = [
|
||||
{ id: "spotify", name: "Spotify", option: "music_spotify_url", color: "#1DB954" },
|
||||
{ id: "apple", name: "Apple Music", option: "music_apple_url", color: "#FA243C" },
|
||||
{ id: "youtube", name: "YouTube", option: "music_youtube_url", color: "#FF0000" },
|
||||
];
|
||||
|
||||
const MUSIC_ICONS = {
|
||||
spotify: '<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>',
|
||||
};
|
||||
|
||||
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>
|
||||
</div>
|
||||
`;
|
||||
function loadDailyQuotesWidget() {
|
||||
crLoadWidget("daily_quotes", {
|
||||
className: "canvasrefined-quotes",
|
||||
html: QUOTES_HTML,
|
||||
});
|
||||
}
|
||||
|
||||
function loadAllSimpleWidgets() {
|
||||
loadFocusTimer();
|
||||
loadMusicWidget();
|
||||
loadBreathingWidget();
|
||||
loadDailyQuotesWidget();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Custom font
|
||||
|
||||
138
js/popup.js
138
js/popup.js
@ -1,4 +1,4 @@
|
||||
const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'todo_celebrations_enabled', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics', 'hide_personal_details'];
|
||||
const syncedSwitches = ['remind', 'tab_icons', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'focus_timer', 'music', 'breathing', 'daily_quotes', 'better_todo', 'todo_celebrations_enabled', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'hide_sequence_footer', 'center_cards', 'quiz_safe_mode', 'global_search', 'grade_analytics', 'hide_personal_details'];
|
||||
const syncedSubOptions = [
|
||||
"grade_analytics_zones",
|
||||
"hide_course_images",
|
||||
@ -20,6 +20,21 @@ const syncedSubOptions = [
|
||||
"gpa_calc_prepend",
|
||||
"gpa_calc_cumulative",
|
||||
"gpa_calc_weighted",
|
||||
"focus_timer_position",
|
||||
"focus_timer_preset_1",
|
||||
"focus_timer_preset_2",
|
||||
"focus_timer_preset_3",
|
||||
"music_position",
|
||||
"music_spotify_url",
|
||||
"music_apple_url",
|
||||
"music_youtube_url",
|
||||
"music_thumb_bg",
|
||||
"music_no_thumb",
|
||||
"breathing_position",
|
||||
"breathing_preset_1",
|
||||
"breathing_preset_2",
|
||||
"breathing_preset_3",
|
||||
"daily_quotes_position",
|
||||
"auto_dark",
|
||||
"auto_dark_start",
|
||||
"auto_dark_end",
|
||||
@ -130,6 +145,25 @@ const defaultOptions = {
|
||||
"dashboard_notes_mode": "edit",
|
||||
"dashboard_notes_position": "top",
|
||||
"gpa_calc_position": "cards_end",
|
||||
"focus_timer": false,
|
||||
"focus_timer_position": "cards_end",
|
||||
"focus_timer_preset_1": { "h": 0, "m": 15, "s": 0 },
|
||||
"focus_timer_preset_2": { "h": 0, "m": 25, "s": 0 },
|
||||
"focus_timer_preset_3": { "h": 0, "m": 45, "s": 0 },
|
||||
"music": false,
|
||||
"music_position": "cards_end",
|
||||
"music_spotify_url": "",
|
||||
"music_apple_url": "",
|
||||
"music_youtube_url": "",
|
||||
"music_thumb_bg": true,
|
||||
"music_no_thumb": false,
|
||||
"breathing": false,
|
||||
"breathing_position": "cards_end",
|
||||
"breathing_preset_1": { "h": 0, "m": 3, "s": 0 },
|
||||
"breathing_preset_2": { "h": 0, "m": 5, "s": 0 },
|
||||
"breathing_preset_3": { "h": 0, "m": 10, "s": 0 },
|
||||
"daily_quotes": false,
|
||||
"daily_quotes_position": "top",
|
||||
"better_todo": true,
|
||||
"better_sidebar": false,
|
||||
"sidebar_scale": 100,
|
||||
@ -1007,6 +1041,10 @@ function toggleAlternateColorsVisibility(darkModeOn) {
|
||||
const WIDGET_LIST = [
|
||||
{ id: "dashboard_notes", nameKey: "dashboard_notes", shortKey: "widget_short_notes" },
|
||||
{ id: "gpa_calc", nameKey: "gpa_calculator", shortKey: "widget_short_gpa" },
|
||||
{ id: "focus_timer", nameKey: "focus_timer", shortKey: "widget_short_focus" },
|
||||
{ id: "music", nameKey: "music", shortKey: "widget_short_music" },
|
||||
{ id: "breathing", nameKey: "breathing_breaks", shortKey: "widget_short_breathing" },
|
||||
{ id: "daily_quotes", nameKey: "daily_quotes", shortKey: "widget_short_quotes" },
|
||||
];
|
||||
|
||||
function widgetPosFromSync(id, sync) {
|
||||
@ -1015,7 +1053,7 @@ function widgetPosFromSync(id, sync) {
|
||||
if (sync.gpa_calc_position !== undefined) return sync.gpa_calc_position;
|
||||
return sync.gpa_calc_prepend === true ? "cards_start" : "cards_end";
|
||||
}
|
||||
return "top";
|
||||
return sync[id + "_position"] || (defaultOptions.sync[id + "_position"]) || "top";
|
||||
}
|
||||
|
||||
const WIDGET_ORDER_MIN_PCT = 15;
|
||||
@ -1037,7 +1075,11 @@ function parseWidgetOrderEntries(raw) {
|
||||
|
||||
function widgetDisplayName(id) {
|
||||
const info = WIDGET_LIST.find(w => w.id === id);
|
||||
return (info && chrome.i18n.getMessage(info.shortKey || info.nameKey)) || id;
|
||||
const msg = info && chrome.i18n.getMessage(info.shortKey || info.nameKey);
|
||||
if (msg) return msg;
|
||||
// Locales that predate a widget have no message: show a friendly label
|
||||
// instead of the raw storage id.
|
||||
return id.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
// Live-refresh hook for the widget-order UI, set by setupWidgetOrder and
|
||||
@ -1050,7 +1092,9 @@ function setupWidgetOrder() {
|
||||
const wrap = document.getElementById("widget-order-container");
|
||||
if (!wrap) return;
|
||||
|
||||
const KEYS = ["dashboard_notes", "gpa_calc", "dashboard_notes_position", "gpa_calc_position", "gpa_calc_prepend", "widget_order"];
|
||||
const KEYS = WIDGET_LIST.map(w => w.id)
|
||||
.concat(WIDGET_LIST.map(w => w.id + "_position"))
|
||||
.concat(["gpa_calc_prepend", "widget_order"]);
|
||||
|
||||
const AREAS = ["top", "cards_start", "cards_end", "below"];
|
||||
|
||||
@ -1223,6 +1267,10 @@ function setupWidgetPosition() {
|
||||
[
|
||||
{ select: "dashboard_notes_position", key: "dashboard_notes_position" },
|
||||
{ select: "gpa_calc_position", key: "gpa_calc_position" },
|
||||
{ select: "focus_timer_position", key: "focus_timer_position" },
|
||||
{ select: "music_position", key: "music_position" },
|
||||
{ select: "breathing_position", key: "breathing_position" },
|
||||
{ select: "daily_quotes_position", key: "daily_quotes_position" },
|
||||
].forEach(({ select, key }) => {
|
||||
const el = document.getElementById(select);
|
||||
if (!el) return;
|
||||
@ -1241,10 +1289,48 @@ function setupWidgetPosition() {
|
||||
});
|
||||
}
|
||||
|
||||
// Hours/minutes/seconds widgets settings (Focus Timer / Breathing presets).
|
||||
function setupWidgetTimeInput(prefix, initial) {
|
||||
const hEl = document.getElementById(prefix + "_h");
|
||||
const mEl = document.getElementById(prefix + "_m");
|
||||
const sEl = document.getElementById(prefix + "_s");
|
||||
if (!hEl || !mEl || !sEl) return;
|
||||
let v = initial;
|
||||
if (!v || typeof v !== "object") {
|
||||
// Legacy plain-minutes value.
|
||||
const n = parseInt(v, 10);
|
||||
v = Number.isFinite(n) && n > 0 ? { h: 0, m: n, s: 0 } : (defaultOptions.sync[prefix] || { h: 0, m: 0, s: 0 });
|
||||
}
|
||||
hEl.value = v.h || 0;
|
||||
mEl.value = v.m || 0;
|
||||
sEl.value = v.s || 0;
|
||||
const save = () => {
|
||||
const clamp = (el, max) => Math.min(max, Math.max(0, parseInt(el.value, 10) || 0));
|
||||
let h = clamp(hEl, 99), m = clamp(mEl, 59), s = clamp(sEl, 59);
|
||||
if (h === 0 && m === 0 && s === 0) {
|
||||
const d = defaultOptions.sync[prefix] || { h: 0, m: 5, s: 0 };
|
||||
h = d.h; m = d.m; s = d.s;
|
||||
}
|
||||
hEl.value = h; mEl.value = m; sEl.value = s;
|
||||
chrome.storage.sync.set({ [prefix]: { h, m, s } });
|
||||
};
|
||||
[hEl, mEl, sEl].forEach(el => el.addEventListener("change", save));
|
||||
}
|
||||
|
||||
// Text/URL widget settings (Music playlist links).
|
||||
function setupWidgetTextInput(id, initial) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.value = initial || "";
|
||||
el.addEventListener("change", function () {
|
||||
chrome.storage.sync.set({ [id]: this.value.trim() });
|
||||
});
|
||||
}
|
||||
|
||||
// Hide a toggle's sub-options when it's off; auto_dark only hides its time
|
||||
// clocks.
|
||||
function toggleSubOptionsVisibility(option, isOn) {
|
||||
const togglesWithSubOptions = ["dashboard_notes", "gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"];
|
||||
const togglesWithSubOptions = ["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"];
|
||||
if (!togglesWithSubOptions.includes(option)) return;
|
||||
const optionEl = document.getElementById(option);
|
||||
if (!optionEl) return;
|
||||
@ -1649,6 +1735,8 @@ function setup() {
|
||||
"customBackgroundNasaDaily",
|
||||
"fitImageToScreen",
|
||||
"card_transparency",
|
||||
"music_thumb_bg",
|
||||
"music_no_thumb",
|
||||
"customCardStyles",
|
||||
"card_grid",
|
||||
"card_grid_center_rows",
|
||||
@ -1843,6 +1931,42 @@ function setup() {
|
||||
identifier: "anon_progress_total",
|
||||
setup: (initial) => setupAnonProgressInput("anon_progress_total", initial, false),
|
||||
},
|
||||
{
|
||||
identifier: "focus_timer_preset_1",
|
||||
setup: (initial) => setupWidgetTimeInput("focus_timer_preset_1", initial),
|
||||
},
|
||||
{
|
||||
identifier: "focus_timer_preset_2",
|
||||
setup: (initial) => setupWidgetTimeInput("focus_timer_preset_2", initial),
|
||||
},
|
||||
{
|
||||
identifier: "focus_timer_preset_3",
|
||||
setup: (initial) => setupWidgetTimeInput("focus_timer_preset_3", initial),
|
||||
},
|
||||
{
|
||||
identifier: "breathing_preset_1",
|
||||
setup: (initial) => setupWidgetTimeInput("breathing_preset_1", initial),
|
||||
},
|
||||
{
|
||||
identifier: "breathing_preset_2",
|
||||
setup: (initial) => setupWidgetTimeInput("breathing_preset_2", initial),
|
||||
},
|
||||
{
|
||||
identifier: "breathing_preset_3",
|
||||
setup: (initial) => setupWidgetTimeInput("breathing_preset_3", initial),
|
||||
},
|
||||
{
|
||||
identifier: "music_spotify_url",
|
||||
setup: (initial) => setupWidgetTextInput("music_spotify_url", initial),
|
||||
},
|
||||
{
|
||||
identifier: "music_apple_url",
|
||||
setup: (initial) => setupWidgetTextInput("music_apple_url", initial),
|
||||
},
|
||||
{
|
||||
identifier: "music_youtube_url",
|
||||
setup: (initial) => setupWidgetTextInput("music_youtube_url", initial),
|
||||
},
|
||||
{
|
||||
identifier: "todo_more_expanded",
|
||||
setup: (initial) => setupTodoMoreOptions(initial),
|
||||
@ -1888,7 +2012,7 @@ function setup() {
|
||||
chrome.storage.sync.set({ "hide_course_images": false });
|
||||
}
|
||||
toggleSubOptionsVisibility(option, status);
|
||||
if (option === "dashboard_notes" || option === "gpa_calc") {
|
||||
if (option === "dashboard_notes" || option === "gpa_calc" || option === "focus_timer" || option === "music" || option === "breathing" || option === "daily_quotes") {
|
||||
if (refreshWidgetOrder) refreshWidgetOrder();
|
||||
}
|
||||
});
|
||||
@ -1898,7 +2022,7 @@ function setup() {
|
||||
chrome.storage.sync.get("todo_celebrations_enabled", v => {
|
||||
toggleCelebrationOptionsVisibility(v["todo_celebrations_enabled"] !== false);
|
||||
});
|
||||
["dashboard_notes", "gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => {
|
||||
["dashboard_notes", "gpa_calc", "focus_timer", "music", "breathing", "daily_quotes", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => {
|
||||
toggleSubOptionsVisibility(opt, sync[opt] === true);
|
||||
});
|
||||
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user