mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 22:07:18 +01:00
started the widget system
This commit is contained in:
parent
94ff530e2e
commit
6268150cd6
@ -219,6 +219,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)
|
||||
|
||||
#### Original Owner
|
||||
|
||||
|
||||
@ -125,6 +125,33 @@
|
||||
"gpa_calculator": {
|
||||
"message": "GPA Calculator"
|
||||
},
|
||||
"widgets": {
|
||||
"message": "Widgets"
|
||||
},
|
||||
"widget_position": {
|
||||
"message": "Position"
|
||||
},
|
||||
"widget_order_top": {
|
||||
"message": "Top container order"
|
||||
},
|
||||
"widget_order_bottom": {
|
||||
"message": "Bottom container order"
|
||||
},
|
||||
"grade_settings": {
|
||||
"message": "Grade Settings"
|
||||
},
|
||||
"pos_top": {
|
||||
"message": "Top"
|
||||
},
|
||||
"pos_cards_start": {
|
||||
"message": "Before cards"
|
||||
},
|
||||
"pos_cards_end": {
|
||||
"message": "After cards"
|
||||
},
|
||||
"pos_below": {
|
||||
"message": "Bottom"
|
||||
},
|
||||
"gpa_settings": {
|
||||
"message": "GPA Settings"
|
||||
},
|
||||
@ -164,9 +191,6 @@
|
||||
"themes": {
|
||||
"message": "Themes"
|
||||
},
|
||||
"move_top": {
|
||||
"message": "Move To Top"
|
||||
},
|
||||
"show_past_due": {
|
||||
"message": "Show Past Due"
|
||||
},
|
||||
|
||||
117
css/content.css
117
css/content.css
@ -429,3 +429,120 @@
|
||||
.css-fkmnqn-view {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
/* Shared widget container ("Top"/"Bottom" positions): a full-width flex row
|
||||
holding one slot per widget; slot widths are the % set in the Widgets tab.
|
||||
Row margins mirror the notes container's original 18/14 spacing. */
|
||||
.canvasrefined-widget-row {display:flex;align-items:stretch;width:100%;box-sizing:border-box;margin:18px 0 14px;column-gap:14px;}
|
||||
.canvasrefined-widget-slot {min-width:0;box-sizing:border-box;display:flex;flex-direction:column;}
|
||||
.canvasrefined-widget-slot > * {width:100%;}
|
||||
/* Equal height across widgets in the shared container: every slot stretches
|
||||
to the row height and its widget content fills it top-to-bottom. */
|
||||
.canvasrefined-widget-slot .canvasrefined-dashboard-notes {width:100%;margin:0;height:100%;display:flex;flex-direction:column;}
|
||||
.canvasrefined-widget-slot .canvasrefined-dashboard-notes .canvasrefined-notes-surface {flex:1;display:flex;flex-direction:column;}
|
||||
.canvasrefined-widget-slot .canvasrefined-dashboard-notes .canvasrefined-notes-editor,
|
||||
.canvasrefined-widget-slot .canvasrefined-dashboard-notes .canvasrefined-notes-rendered {flex:1;overflow:auto;}
|
||||
.canvasrefined-widget-slot .canvasrefined-gpa-card {width:100%;margin:0;box-sizing:border-box;display:block;flex:1 1 auto;min-height:0;}
|
||||
.canvasrefined-widget-slot .canvasrefined-gpa {margin:0;flex:1 1 auto;min-height:0;overflow:auto;}
|
||||
/* GPA calculator in container mode ("Top"/"Bottom" positions): the elements
|
||||
are moved outside the card grid, so the collapsed GPA card becomes a wide
|
||||
container-style card and the expanded calculator keeps full width. */
|
||||
.canvasrefined-gpa-card.container-mode {width:100%;box-sizing:border-box;margin:0 0 4px;display:block;}
|
||||
.canvasrefined-gpa.container-mode {margin:0 0 18px;}
|
||||
/* Dashboard notes placed inside the card grid (before/after cards): shaped
|
||||
like a course card (262px wide, rounded, Canvas card shadow) and kept at a
|
||||
similar height so the grid row stays balanced. */
|
||||
.canvasrefined-dashboard-notes.in-cards {
|
||||
width: 262px;
|
||||
min-height: 260px;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.canvasrefined-dashboard-notes.in-cards .canvasrefined-notes-surface {flex: 1; display: flex; flex-direction: column;}
|
||||
.canvasrefined-dashboard-notes.in-cards .canvasrefined-notes-editor,
|
||||
.canvasrefined-dashboard-notes.in-cards .canvasrefined-notes-rendered {flex: 1; resize: none; border: none; outline: none; background: transparent;}
|
||||
/* Notes placed in the shared widget row get their margins from the row
|
||||
(see .canvasrefined-widget-slot rules above). */
|
||||
/* GPA calculator modal (Edit Calculator button): combines the course
|
||||
calculator with the grade-scale editor, styled like the global search
|
||||
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-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;
|
||||
}
|
||||
.cr-gpa-modal {width:min(1080px,96vw);max-height:86vh;display:flex;flex-direction:column;background:var(--bcbackground-0,#fff);color:var(--bctext-0,#2d3b45);border:1px solid var(--bcborders,#c7cdd1);border-radius:16px;box-shadow:0 24px 70px rgba(0,0,0,.4);overflow:hidden;font-size:13px;}
|
||||
/* Header: title on the left, live GPA tiles on the right */
|
||||
.cr-gpa-head {display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid var(--bcborders,#c7cdd1);}
|
||||
.cr-gpa-head-titles {display:flex;flex-direction:column;gap:2px;margin-right:auto;min-width:140px;}
|
||||
.cr-gpa-title {font-size:17px;font-weight:700;}
|
||||
.cr-gpa-subtitle {font-size:12px;color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-gpa-results {display:flex;gap:10px;align-items:stretch;}
|
||||
.cr-gpa-result {display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;min-width:76px;padding:6px 12px;border-radius:12px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 6%, transparent);}
|
||||
.cr-gpa-result-value {font-size:20px;font-weight:700;line-height:1.15;font-variant-numeric:tabular-nums;}
|
||||
.cr-gpa-result-label {font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-gpa-close {display:flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border:none;background:transparent;color:inherit;border-radius:8px;cursor:pointer;}
|
||||
.cr-gpa-close:hover {background:rgba(127,127,127,.15);}
|
||||
/* Body: courses on the left, grade scale on the right; panes scroll alone */
|
||||
.cr-gpa-body {display:grid;grid-template-columns:minmax(0,2.2fr) minmax(210px,1fr);flex:1;min-height:0;}
|
||||
.cr-gpa-pane {min-width:0;overflow-y:auto;padding:14px 20px 20px;}
|
||||
.cr-gpa-pane-scale {border-left:1px solid var(--bcborders,#c7cdd1);padding-left:14px;}
|
||||
.cr-gpa-pane-head {font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);margin:0 0 10px;}
|
||||
/* Calculator body moved into the modal: neutralize panel chrome */
|
||||
.cr-gpa-modal .canvasrefined-gpa {display:block!important;margin:0;padding:0;border:none;box-shadow:none!important;overflow:visible;position:static;background:transparent!important;border-radius:0;}
|
||||
.cr-gpa-modal .canvasrefined-gpa .canvasrefined-gpa-header {display:none;}
|
||||
.cr-gpa-modal .canvasrefined-gpa-courses-container {margin:0;width:100%;overflow:visible;}
|
||||
.cr-gpa-modal .canvasrefined-gpa-courses {display:flex;flex-direction:column;min-width:0;width:100%;}
|
||||
/* Course rows: shared grid so the column headers line up with the rows */
|
||||
.cr-gpa-colhead,
|
||||
.cr-gpa-modal .canvasrefined-gpa-course,
|
||||
.cr-gpa-modal #canvasrefined-cumulative-gpa {display:grid;grid-template-columns:var(--cr-gpa-course-cols);column-gap:10px;align-items:center;}
|
||||
.cr-gpa-colhead {padding:0 8px 6px;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-gpa-colhead .cr-gpa-colhead-custom {grid-column:6 / span 2;}
|
||||
.cr-gpa-modal .canvasrefined-gpa-course {padding:7px 8px;border-radius:10px;}
|
||||
.cr-gpa-modal .canvasrefined-gpa-course:hover {background:color-mix(in srgb, var(--bctext-0,#2d3b45) 5%, transparent);}
|
||||
/* Letter badge: color-coded pill driven by data-grade-letter (set in calculateGPA2) */
|
||||
.cr-gpa-modal .canvasrefined-gpa-letter-grade {width:auto;margin:0;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 2px;border-radius:8px;font-size:12px;font-weight:700;background:transparent;color:var(--bctext-0,#2d3b45);}
|
||||
.cr-gpa-modal [data-grade-letter] .canvasrefined-gpa-letter-grade {background:transparent;color:var(--bctext-0,#2d3b45);}
|
||||
.cr-gpa-modal .canvasrefined-gpa-name {margin:0;width:auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:500;}
|
||||
.cr-gpa-modal .canvasrefined-gpa-percent-container,
|
||||
.cr-gpa-modal .canvasrefined-course-credits {margin:0;display:flex;align-items:center;gap:4px;}
|
||||
.cr-gpa-modal .canvasrefined-course-percent,
|
||||
.cr-gpa-modal .canvasrefined-course-credit {height:30px;width:100%;font-size:14px;border-radius:8px;padding:0 8px;box-sizing:border-box;border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-0,#2d3b45);text-align:right;}
|
||||
.cr-gpa-modal .canvasrefined-course-percent-sign {color:var(--bctext-2,#9aa3ab);font-size:12px;}
|
||||
.cr-gpa-modal .canvasrefined-course-weights {margin:0;}
|
||||
.cr-gpa-modal .canvasrefined-course-weight {height:30px;width:100%;min-width:0;margin:0;font-size:13px;border-radius:8px;padding:0 6px;box-sizing:border-box;border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-0,#2d3b45);}
|
||||
.cr-gpa-modal .canvasrefined-course-percent:focus,
|
||||
.cr-gpa-modal .canvasrefined-course-credit:focus,
|
||||
.cr-gpa-modal .canvasrefined-course-weight:focus {outline:none;border-color:var(--cr-gpa-accent);}
|
||||
.cr-gpa-modal .canvasrefined-course-customgr {margin:0;width:16px;height:16px;accent-color:var(--cr-gpa-accent);cursor:pointer;}
|
||||
.cr-gpa-modal .canvasrefined-course-customgr-label {margin:0;font-size:12px;color:var(--bctext-2,#9aa3ab);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
/* Cumulative row: pulled to the bottom as an emphasized summary row */
|
||||
.cr-gpa-modal #canvasrefined-cumulative-gpa {order:100;margin-top:10px;padding:12px 8px 4px;border-top:1px dashed var(--bcborders,#c7cdd1);}
|
||||
.cr-gpa-modal #canvasrefined-cumulative-gpa .canvasrefined-gpa-name {font-weight:700;}
|
||||
/* Grade scale pane */
|
||||
.cr-gpa-seg {display:flex;gap:8px;padding:0;background:transparent;margin:0 0 14px;}
|
||||
.cr-gpa-preset {flex:1;border:1px solid var(--bcborders,#c7cdd1);background:transparent;color:var(--bctext-2,#9aa3ab);border-radius:8px;padding:7px 10px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;}
|
||||
.cr-gpa-preset:hover {color:var(--bctext-0,#2d3b45);}
|
||||
.cr-gpa-preset.active {background:var(--bcbackground-0,#fff);color:var(--bctext-0,#2d3b45);border-color:var(--bctext-2,#9aa3ab);}
|
||||
/* Grade scale rows: letter chip + compact "value + unit" fields */
|
||||
.cr-gpa-scale-head {display:grid;grid-template-columns:40px 1fr 1fr;gap:8px;padding:0 2px 4px;font-size:10.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--bctext-2,#9aa3ab);}
|
||||
.cr-gpa-row {display:grid;grid-template-columns:40px 1fr 1fr;gap:8px;align-items:center;padding:2px 0;}
|
||||
.cr-gpa-row:hover {background:color-mix(in srgb, var(--bctext-0,#2d3b45) 4%, transparent);border-radius:8px;}
|
||||
.cr-gpa-chip {display:flex;align-items:center;justify-content:center;height:24px;border-radius:7px;background:color-mix(in srgb, var(--bctext-0,#2d3b45) 7%, transparent);font-weight:700;font-size:12px;}
|
||||
.cr-gpa-field {display:flex;align-items:center;min-width:0;border:1px solid var(--bcborders,#c7cdd1);border-radius:8px;height:28px;background:transparent;overflow:hidden;}
|
||||
.cr-gpa-field:focus-within {border-color:var(--cr-gpa-accent,var(--ic-brand-primary,#0374b5));box-shadow:0 0 0 2px color-mix(in srgb, var(--cr-gpa-accent,var(--ic-brand-primary,#0374b5)) 22%, transparent);}
|
||||
.cr-gpa-input {width:100%;min-width:0;border:none;background:transparent;color:inherit;height:100%;padding:0 4px 0 8px;font:inherit;font-size:13px;text-align:right;box-sizing:border-box;}
|
||||
.cr-gpa-input:focus {outline:none;box-shadow:none;}
|
||||
.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;}
|
||||
.cr-gpa-body {grid-template-columns:1fr;}
|
||||
.cr-gpa-pane-scale {border-left:none;border-top:1px solid var(--bcborders,#c7cdd1);}
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ h2 {margin-top: 20px;font-weight:600}
|
||||
.todo-more-options.expanded .todo-expanded-label {display:inline;}
|
||||
.todo-more-items {display:none;flex-direction:column;}
|
||||
.todo-more-options.expanded .todo-more-items {display:flex;padding-left:2px;}
|
||||
#advanced-settings, #gpa-bounds-btn, #custom-font-btn, #customize-dark-btn, #card-colors-btn {width: 100%;}
|
||||
#advanced-settings, #custom-font-btn, #customize-dark-btn, #card-colors-btn {width: 100%;}
|
||||
.back-btn {width: 120px;height:100%;margin-top:0}
|
||||
.big-button {border: none;border-radius: 7px;background: var(--containerbg);color: #fff;font-family: inherit;padding: 12px 10px;font-weight:600}
|
||||
.social-buttons {display: flex;gap: 8px;margin-top: 8px;}
|
||||
@ -138,9 +138,6 @@ h2 {margin-top: 20px;font-weight:600}
|
||||
.social-toggle:hover {color: #e2e2e2}
|
||||
.social-buttons.hidden {display: none}
|
||||
#setSingleColor, #setGradientColor {padding: 8px 20px;margin-top:5px;}
|
||||
.gpa-bounds-input {font-size: 14px;background: var(--inputbg); border: none;padding:6px;color: #e2e2e2;margin-top: 6px;border-radius: 6px; font-family: inherit; width: 55px; box-sizing: border-box;text-align: right}
|
||||
.gpa-bounds span {font-size: 16px; width: 25px;display: inline-block}
|
||||
.gpa-bounds-explanation {width:200px;height:100%;flex: 1;}
|
||||
.tab-header {display: flex;justify-content: space-between;gap:20px;align-items:center}
|
||||
.custom-font-flex {display: flex; align-items: center;font-size: 11px;}
|
||||
#custom-font-family, #custom-font-link {width:150px}
|
||||
@ -381,3 +378,17 @@ input[type="checkbox"]:checked {background: #8dd28d;}
|
||||
}
|
||||
.cr-info-bubble:hover .cr-info-bubble-text,
|
||||
.cr-info-bubble:focus .cr-info-bubble-text { visibility: visible; opacity: 1; }
|
||||
|
||||
/* Widget container-order strips (Widgets tab): boxes sized by each widget's
|
||||
% of the shared container, separated by draggable dividers. */
|
||||
.widget-order-strip {display:flex;align-items:stretch;width:100%;height:64px;margin-top:8px;}
|
||||
.wos-item {border:1px solid #ffffff2e;background:var(--inputbg);border-radius:7px;display:flex;align-items:center;padding:0 10px;font-size:12px;color:#e2e2e2;min-width:0;overflow:hidden;white-space:nowrap;cursor:grab;user-select:none;}
|
||||
.wos-item.dragging {opacity:.45;}
|
||||
.wos-divider {flex:0 0 6px;width:6px;align-self:stretch;margin:0 5px;border-radius:3px;background:#8a8a8a;cursor:col-resize;}
|
||||
.wos-divider:hover {background:#e2e2e2;}
|
||||
/* 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;}
|
||||
|
||||
112
html/popup.html
112
html/popup.html
@ -37,7 +37,7 @@
|
||||
<span data-i18n="edit_dark_mode">Edit Dark Mode</span>
|
||||
</button>
|
||||
<button id="advanced-settings" class="big-button tab-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path><path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path><path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path><path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12C2 8.22876 2 6.34315 3.17157 5.17157C4.34315 4 6.22876 4 10 4H14C17.7712 4 19.6569 4 20.8284 5.17157C22 6.34315 22 8.22876 22 12C22 15.7712 22 17.6569 20.8284 18.8284C19.6569 20 17.7712 20 14 20H10C6.22876 20 4.34315 20 3.17157 18.8284C2 17.6569 2 15.7712 2 12Z"></path><path d="M10 16H6"></path><path d="M14 16H12.5"></path><path d="M2 10L22 10"></path></svg>
|
||||
<span data-i18n="card_customization">Card Customization</span>
|
||||
</button>
|
||||
<!-- <button id="card-colors-btn" class="big-button tab-btn">
|
||||
@ -53,8 +53,8 @@
|
||||
<span data-i18n="styles">Styles</span>
|
||||
</button>
|
||||
<button id="gpa-bounds-btn" class="big-button tab-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M22 9l-10 -4l-10 4l10 4l10 -4v6"></path><path d="M6 10.6v5.4a6 3 0 0 0 12 0v-5.4"></path></svg>
|
||||
<span data-i18n="gpa_settings">GPA Settings</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><path d="M17.5 14.0002L17.5 21.0004"></path><path d="M21.0001 17.5001L14 17.5001"></path></svg>
|
||||
<span data-i18n="widgets">Widgets</span>
|
||||
</button>
|
||||
<button id="report-issue-btn" class="big-button tab-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v4"></path><path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.87l-8.106 -13.536a1.914 1.914 0 0 0 -3.274 0z"></path><path d="M12 16h.01"></path></svg>
|
||||
@ -141,31 +141,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="gpa_calc">
|
||||
<input type="radio" id="off" name="gpa_calc">
|
||||
<input type="radio" id="on" name="gpa_calc">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div>
|
||||
<span class="option-name" data-i18n="gpa_calculator">GPA calculator</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_prepend" name="gpa_calc_prepend">
|
||||
<label for="gpa_calc_prepend" class="sub-text" data-i18n="move_top">Move to top</label>
|
||||
</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>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_weighted" name="gpa_calc_weighted">
|
||||
<label for="gpa_calc_weighted" class="sub-text" data-i18n="gpa_calc_weighted">Show weighted GPA</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="dashboard_grades">
|
||||
<input type="radio" id="off" name="dashboard_grades">
|
||||
@ -212,14 +187,6 @@
|
||||
<span class="cr-info-bubble-text" data-i18n="quiz_safe_mode_desc">Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="option" id="dashboard_notes">
|
||||
<input type="radio" id="off" name="dashboard_notes">
|
||||
<input type="radio" id="on" name="dashboard_notes">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div><span class="option-name" data-i18n="dashboard_notes">Dashboard Notes</span>
|
||||
</div>
|
||||
<div class="option" id="global_search">
|
||||
<input type="radio" id="off" name="global_search">
|
||||
<input type="radio" id="on" name="global_search">
|
||||
@ -1143,20 +1110,71 @@
|
||||
|
||||
<div class="gpa-bounds-container tab" style="display:none">
|
||||
<div class="tab-header">
|
||||
<h2 style="margin-top: 0" data-i18n="gpa_settings">GPA Calculator Bounds</h2>
|
||||
<h2 style="margin-top: 0" data-i18n="widgets">Widgets</h2>
|
||||
<button class="big-button back-btn" data-i18n="back">Back</button>
|
||||
</div>
|
||||
<div style="display: flex;gap: 8px;">
|
||||
<div class="gpa-bounds option-container"></div>
|
||||
<div class="gpa-bounds-explanation option-container">
|
||||
The letter grade will be calculated as greater than or equal to the percentage given. <br><br>
|
||||
If your school doesn't use the +/- system, you can enter 101 for those grades to remove them from the
|
||||
calculation. (for example A+ = 101, A = 90, A- = 101)
|
||||
<h3 class="header-small" style="margin-top: 20px;">GPA Scale Presets 0-4</h3>
|
||||
<button class="big-button" id="gpa-plus-minus">+/- GPA Scale</button>
|
||||
<button class="big-button" id="gpa-by-letter">By Letter GPA Scale</button>
|
||||
<!-- Widget ordering: shown only when 2+ widgets share the top/bottom container -->
|
||||
<div class="option-container" id="widget-order-container" style="display:none">
|
||||
<div id="widget-order-top-wrap" style="display:none">
|
||||
<h3 class="header-small" data-i18n="widget_order_top">Top container order</h3>
|
||||
<div class="widget-order-strip" id="widget-order-top"></div>
|
||||
</div>
|
||||
<div id="widget-order-below-wrap" style="display:none">
|
||||
<h3 class="header-small" data-i18n="widget_order_bottom">Bottom container order</h3>
|
||||
<div class="widget-order-strip" id="widget-order-below"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Widgets: add one option-container per widget here -->
|
||||
<div class="option-container">
|
||||
<div class="option" id="dashboard_notes">
|
||||
<input type="radio" id="off" name="dashboard_notes">
|
||||
<input type="radio" id="on" name="dashboard_notes">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</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">
|
||||
<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 class="option-container">
|
||||
<div class="option" id="gpa_calc">
|
||||
<input type="radio" id="off" name="gpa_calc">
|
||||
<input type="radio" id="on" name="gpa_calc">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div>
|
||||
<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">
|
||||
<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: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>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_weighted" name="gpa_calc_weighted">
|
||||
<label for="gpa_calc_weighted" class="sub-text" data-i18n="gpa_calc_weighted">Show weighted GPA</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -40,6 +40,8 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
"dashboard_notes": false,
|
||||
"dashboard_notes_text": "",
|
||||
"dashboard_notes_mode": "edit",
|
||||
"dashboard_notes_position": "top",
|
||||
"widget_order": {},
|
||||
"better_todo": true,
|
||||
"todo_hr24": false,
|
||||
"todo_separate_scrollbar": false,
|
||||
|
||||
503
js/content.js
503
js/content.js
@ -1023,8 +1023,21 @@ function applyOptionsChanges(changes) {
|
||||
case "dashboard_notes":
|
||||
case "dashboard_notes_text":
|
||||
case "dashboard_notes_mode":
|
||||
case "dashboard_notes_position":
|
||||
loadDashboardNotes();
|
||||
break;
|
||||
case "widget_order":
|
||||
["top", "below"].forEach(area => {
|
||||
const row = document.querySelector(".canvasrefined-widget-row[data-area='" + area + "']");
|
||||
if (row) applyWidgetRowLayout(row, area);
|
||||
});
|
||||
// A widget moved between areas (or joined/left a shared row) needs
|
||||
// the OTHER widgets to re-place too: each widget only repositions
|
||||
// itself on its own position key, so without this the widget left
|
||||
// behind would keep the old layout until reload.
|
||||
if (options.dashboard_notes === true) loadDashboardNotes();
|
||||
if (options.gpa_calc === true && current_page === "/") setupGPACalc();
|
||||
break;
|
||||
case "dashboard_grades":
|
||||
case "grade_hover":
|
||||
case "card_letter":
|
||||
@ -1144,6 +1157,7 @@ function applyOptionsChanges(changes) {
|
||||
break;
|
||||
case "gpa_calc":
|
||||
case "gpa_calc_prepend":
|
||||
case "gpa_calc_position":
|
||||
case "gpa_calc_weighted":
|
||||
case "gpa_calc_cumulative":
|
||||
if (!grades) getGrades();
|
||||
@ -1155,6 +1169,10 @@ function applyOptionsChanges(changes) {
|
||||
break;
|
||||
case "gpa_calc_bounds":
|
||||
calculateGPA2();
|
||||
{
|
||||
const modal = document.getElementById("canvasrefined-gpa-modal");
|
||||
if (modal) renderGpaBoundsEditor(modal.querySelector(".cr-gpa-modal-bounds"));
|
||||
}
|
||||
break;
|
||||
case "custom_font":
|
||||
case "custom_font_skip_p":
|
||||
@ -7598,6 +7616,7 @@ GPA calculator
|
||||
function calculateGPA2() {
|
||||
let qualityPoints = 0, numCredits = 0, weightedQualityPoints = 0, cumulativePoints = 0, cumulativeCredits = 0;
|
||||
document.querySelectorAll('.canvasrefined-gpa-course').forEach(course => {
|
||||
course.dataset.gradeLetter = "--";
|
||||
const weight = course.querySelector('.canvasrefined-course-weight').value;
|
||||
const credits = parseFloat(course.querySelector('.canvasrefined-course-credit').value);
|
||||
const grade = parseFloat(course.querySelector('.canvasrefined-course-percent').value);
|
||||
@ -7645,6 +7664,7 @@ function calculateGPA2() {
|
||||
gpa = options.gpa_calc_bounds["F"].gpa;
|
||||
}
|
||||
course.querySelector(".canvasrefined-gpa-letter-grade").textContent = letter;
|
||||
course.dataset.gradeLetter = letter;
|
||||
|
||||
let weightMultiplier = 0;
|
||||
if (weight === "ap") {
|
||||
@ -7659,12 +7679,19 @@ function calculateGPA2() {
|
||||
|
||||
|
||||
});
|
||||
document.querySelector("#canvasrefined-gpa-unweighted").textContent = (qualityPoints / numCredits).toFixed(2);
|
||||
document.querySelector("#canvasrefined-gpa-weighted").textContent = (weightedQualityPoints / numCredits).toFixed(2);
|
||||
document.querySelector("#canvasrefined-gpa-unweighted").textContent = Number.isFinite(qualityPoints / numCredits) ? (qualityPoints / numCredits).toFixed(2) : "--";
|
||||
document.querySelector("#canvasrefined-gpa-weighted").textContent = Number.isFinite(weightedQualityPoints / numCredits) ? (weightedQualityPoints / numCredits).toFixed(2) : "--";
|
||||
const cGPA = document.querySelector("#canvasrefined-cumulative-gpa");
|
||||
const g = parseFloat(cGPA.querySelector(".canvasrefined-course-percent").value);
|
||||
const c = parseInt(cGPA.querySelector(".canvasrefined-course-credit").value);
|
||||
document.querySelector("#canvasrefined-gpa-cumulative").textContent = (((options.gpa_calc_weighted === true ? weightedQualityPoints : qualityPoints) + (g * c)) / (numCredits + c)).toFixed(2);
|
||||
const cumulativeTotal = (options.gpa_calc_weighted === true ? weightedQualityPoints : qualityPoints) + (g * c);
|
||||
document.querySelector("#canvasrefined-gpa-cumulative").textContent = Number.isFinite(cumulativeTotal / (numCredits + c)) ? (cumulativeTotal / (numCredits + c)).toFixed(2) : "--";
|
||||
// Mirror the live results into the open calculator modal's header tiles.
|
||||
const gpaModal = document.getElementById("canvasrefined-gpa-modal");
|
||||
if (gpaModal) gpaModal.querySelectorAll("[data-gpa-mirror]").forEach(tile => {
|
||||
const src = document.getElementById("canvasrefined-gpa-" + tile.dataset.gpaMirror);
|
||||
if (src) tile.querySelector(".cr-gpa-result-value").textContent = src.textContent;
|
||||
});
|
||||
}
|
||||
|
||||
function changeGPASettings(course_id, update) {
|
||||
@ -7768,6 +7795,357 @@ function createGPACalcCourse(location, course) {
|
||||
return courseContainer;
|
||||
}
|
||||
|
||||
// Widgets tab position for the GPA widget. Falls back to the legacy "Move to
|
||||
// top" checkbox for users who set it before the dropdown existed.
|
||||
function gpaCalcPosition() {
|
||||
if (options.gpa_calc_position !== undefined) return options.gpa_calc_position;
|
||||
return options.gpa_calc_prepend === true ? "cards_start" : "cards_end";
|
||||
}
|
||||
|
||||
// Place the GPA card (container2) and its expanded calculator (container) at
|
||||
// the position chosen in the Widgets tab. Guards avoid re-inserting nodes
|
||||
// that are already in place — re-inserting would ping the dashboard mutation
|
||||
// observer and retrigger this whole pass.
|
||||
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;
|
||||
const pos = gpaCalcPosition();
|
||||
const containerMode = pos === "top" || pos === "below";
|
||||
container.classList.toggle("container-mode", containerMode);
|
||||
container2.classList.toggle("container-mode", containerMode);
|
||||
const parent = dashboardContainer.parentNode;
|
||||
if (containerMode) {
|
||||
placeWidgetInRow("gpa_calc", pos, [container2, container], dashboardContainer);
|
||||
return;
|
||||
}
|
||||
releaseWidgetFromRow("gpa_calc", [container2, container], dashboardContainer);
|
||||
if (pos === "cards_start") {
|
||||
if (dashboardContainer.children[0] !== container || dashboardContainer.children[1] !== container2) {
|
||||
dashboardContainer.insertBefore(container, dashboardContainer.firstChild);
|
||||
dashboardContainer.insertBefore(container2, container.nextSibling);
|
||||
}
|
||||
} else if (pos === "cards_end") {
|
||||
if (dashboardContainer.lastElementChild !== container || container2.nextElementSibling !== container) {
|
||||
dashboardContainer.appendChild(container2);
|
||||
dashboardContainer.appendChild(container);
|
||||
}
|
||||
} else if (pos === "top") {
|
||||
if (container2.parentNode !== parent || container2.nextElementSibling !== container || container.nextElementSibling !== dashboardContainer) {
|
||||
if (parent) {
|
||||
parent.insertBefore(container2, dashboardContainer);
|
||||
parent.insertBefore(container, container2.nextSibling);
|
||||
} else {
|
||||
dashboardContainer.prepend(container2);
|
||||
dashboardContainer.prepend(container);
|
||||
}
|
||||
}
|
||||
} else { // below
|
||||
if (container2.parentNode !== parent || dashboardContainer.nextElementSibling !== container2 || container2.nextElementSibling !== container) {
|
||||
if (parent) {
|
||||
parent.insertBefore(container2, dashboardContainer.nextSibling);
|
||||
parent.insertBefore(container, container2.nextSibling);
|
||||
} else {
|
||||
dashboardContainer.appendChild(container2);
|
||||
dashboardContainer.appendChild(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const saved = options.widget_order && options.widget_order[area];
|
||||
if (!Array.isArray(saved)) return [];
|
||||
return saved.map(e => {
|
||||
if (typeof e === "string") return { id: e, size: null };
|
||||
if (e && typeof e === "object" && e.id) {
|
||||
const n = Number(e.size);
|
||||
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"));
|
||||
}
|
||||
|
||||
// Both widgets must share one row: normalize the anchor to the outer
|
||||
// DashboardCard_Container. The GPA's dashboardContainer can be the inner
|
||||
// .ic-DashboardCard__box__container, whose parent is INSIDE the card
|
||||
// container — that's what caused GPA and Notes to build two separate rows.
|
||||
function widgetContainerAnchor(container) {
|
||||
if (container && container.id === "DashboardCard_Container") return container;
|
||||
return document.querySelector("#DashboardCard_Container") || container;
|
||||
}
|
||||
|
||||
function ensureWidgetRow(area, container) {
|
||||
const parent = container.parentNode;
|
||||
if (!parent) return null;
|
||||
let row = parent.querySelector(":scope > .canvasrefined-widget-row[data-area='" + area + "']");
|
||||
if (!row) {
|
||||
row = document.createElement("div");
|
||||
row.className = "canvasrefined-widget-row";
|
||||
row.dataset.area = area;
|
||||
if (area === "top") parent.insertBefore(row, container);
|
||||
else parent.insertBefore(row, container.nextSibling);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// Lay out a widget row: order slots per the saved order, size each slot by
|
||||
// its saved % (normalized across the widgets actually present).
|
||||
function applyWidgetRowLayout(row, area) {
|
||||
const slots = [...row.querySelectorAll(":scope > .canvasrefined-widget-slot")];
|
||||
if (!slots.length) { row.remove(); return; }
|
||||
const entries = containerWidgetOrder(area);
|
||||
const byId = {};
|
||||
slots.forEach(slot => { byId[slot.dataset.widget] = slot; });
|
||||
const order = entries.map(e => e.id).filter(id => byId[id]);
|
||||
slots.forEach(slot => { if (!order.includes(slot.dataset.widget)) order.push(slot.dataset.widget); });
|
||||
|
||||
// Only re-append when the DOM order differs, so our own layout pass
|
||||
// doesn't ping the dashboard mutation observer into another pass.
|
||||
const currentOrder = slots.map(s => s.dataset.widget);
|
||||
if (currentOrder.join("|") !== order.join("|")) {
|
||||
order.forEach(id => row.appendChild(byId[id]));
|
||||
}
|
||||
|
||||
// Stale dividers from a previous layout are rebuilt below.
|
||||
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;
|
||||
});
|
||||
}
|
||||
order.forEach((id, i) => {
|
||||
byId[id].dataset.sizePct = weights[i] * 100;
|
||||
byId[id].style.flex = weights[i] + " 1 0%";
|
||||
byId[id].style.minWidth = "0";
|
||||
});
|
||||
}
|
||||
|
||||
// Put a widget (one or more elements forming one unit) into its slot in the
|
||||
// shared top/below container row.
|
||||
function placeWidgetInRow(id, area, els, container) {
|
||||
const row = ensureWidgetRow(area, widgetContainerAnchor(container));
|
||||
if (!row) return;
|
||||
let slot = row.querySelector(":scope > .canvasrefined-widget-slot[data-widget='" + id + "']");
|
||||
if (!slot) {
|
||||
slot = document.createElement("div");
|
||||
slot.className = "canvasrefined-widget-slot";
|
||||
slot.dataset.widget = id;
|
||||
row.appendChild(slot);
|
||||
}
|
||||
// If the widget sat in a slot of a DIFFERENT row (moving top <-> below),
|
||||
// drop that old slot first: appendChild moves the element but leaves the
|
||||
// empty slot behind, still reserving its old % width.
|
||||
const first = els[0];
|
||||
const oldSlot = first && first.closest && first.closest(".canvasrefined-widget-slot");
|
||||
if (oldSlot && oldSlot !== slot) {
|
||||
const oldRow = oldSlot.parentNode;
|
||||
oldSlot.remove();
|
||||
if (oldRow && oldRow !== row) applyWidgetRowLayout(oldRow, oldRow.dataset.area);
|
||||
}
|
||||
els.forEach(el => { if (el.parentNode !== slot) slot.appendChild(el); });
|
||||
applyWidgetRowLayout(row, area);
|
||||
}
|
||||
|
||||
// Take a widget back out of a container row (e.g. moved into the card grid).
|
||||
function releaseWidgetFromRow(id, els, container) {
|
||||
const first = els[0];
|
||||
const slot = first && first.closest && first.closest(".canvasrefined-widget-slot");
|
||||
if (!slot) return;
|
||||
const row = slot.parentNode;
|
||||
const parent = row.parentNode;
|
||||
const ref = row.dataset.area === "top" ? row.nextSibling : row;
|
||||
els.forEach(el => { if (el.parentNode === slot) parent.insertBefore(el, ref); });
|
||||
slot.remove();
|
||||
if (row && !row.querySelector(":scope > .canvasrefined-widget-slot")) row.remove();
|
||||
else if (row) applyWidgetRowLayout(row, row.dataset.area);
|
||||
}
|
||||
|
||||
// GPA calculator modal: the "Edit Calculator" button opens a centered modal
|
||||
// combining the course calculator with the grade-scale editor. Built fresh
|
||||
// per open (no shared state with the dashboard DOM except the calculator
|
||||
// body, which is moved in/out), and everything syncs through chrome.storage
|
||||
// (gpa_calc_bounds / custom_cards / cumulative_gpa).
|
||||
const GPA_BOUNDS_ORDER = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F"];
|
||||
|
||||
const GPA_PLUS_MINUS_PRESET = {
|
||||
"A+": { "cutoff": 97, "gpa": 4.0 },
|
||||
"A": { "cutoff": 93, "gpa": 4.0 },
|
||||
"A-": { "cutoff": 90, "gpa": 3.7 },
|
||||
"B+": { "cutoff": 87, "gpa": 3.3 },
|
||||
"B": { "cutoff": 83, "gpa": 3.0 },
|
||||
"B-": { "cutoff": 80, "gpa": 2.7 },
|
||||
"C+": { "cutoff": 77, "gpa": 2.3 },
|
||||
"C": { "cutoff": 73, "gpa": 2.0 },
|
||||
"C-": { "cutoff": 70, "gpa": 1.7 },
|
||||
"D+": { "cutoff": 67, "gpa": 1.3 },
|
||||
"D": { "cutoff": 63, "gpa": 1.0 },
|
||||
"D-": { "cutoff": 60, "gpa": 0.7 },
|
||||
"F": { "cutoff": 0, "gpa": 0 }
|
||||
};
|
||||
|
||||
const GPA_BY_LETTER_PRESET = {
|
||||
"A+": { "cutoff": 97, "gpa": 4.0 },
|
||||
"A": { "cutoff": 93, "gpa": 4.0 },
|
||||
"A-": { "cutoff": 90, "gpa": 4.0 },
|
||||
"B+": { "cutoff": 87, "gpa": 3.0 },
|
||||
"B": { "cutoff": 83, "gpa": 3.0 },
|
||||
"B-": { "cutoff": 80, "gpa": 3.0 },
|
||||
"C+": { "cutoff": 77, "gpa": 2.0 },
|
||||
"C": { "cutoff": 73, "gpa": 2.0 },
|
||||
"C-": { "cutoff": 70, "gpa": 2.0 },
|
||||
"D+": { "cutoff": 67, "gpa": 1.0 },
|
||||
"D": { "cutoff": 63, "gpa": 1.0 },
|
||||
"D-": { "cutoff": 60, "gpa": 1.0 },
|
||||
"F": { "cutoff": 0, "gpa": 0 }
|
||||
};
|
||||
|
||||
// Grade-scale editor: rows of letter | min % | GPA points. Writes the
|
||||
// gpa_calc_bounds key; the modal re-renders on storage changes (which also
|
||||
// re-syncs which preset button is highlighted).
|
||||
function renderGpaBoundsEditor(host) {
|
||||
if (!host.isConnected) return;
|
||||
chrome.storage.sync.get(["gpa_calc_bounds"], storage => {
|
||||
const bounds = storage["gpa_calc_bounds"];
|
||||
if (!bounds || !host.isConnected) return;
|
||||
host.textContent = "";
|
||||
GPA_BOUNDS_ORDER.forEach(key => {
|
||||
const row = document.createElement("label");
|
||||
row.className = "cr-gpa-row";
|
||||
row.innerHTML =
|
||||
'<span class="cr-gpa-chip">' + key + '</span>' +
|
||||
'<span class="cr-gpa-field"><input class="cr-gpa-input" data-field="cutoff" type="text" inputmode="decimal"><span class="cr-gpa-suffix">%</span></span>' +
|
||||
'<span class="cr-gpa-field"><input class="cr-gpa-input" data-field="gpa" type="text" inputmode="decimal"><span class="cr-gpa-suffix">pts</span></span>';
|
||||
const [cutoff, gpa] = row.querySelectorAll("input");
|
||||
cutoff.value = bounds[key].cutoff;
|
||||
gpa.value = bounds[key].gpa;
|
||||
const save = field => () => {
|
||||
chrome.storage.sync.get(["gpa_calc_bounds"], existing => {
|
||||
chrome.storage.sync.set({ "gpa_calc_bounds": { ...existing["gpa_calc_bounds"], [key]: { ...existing["gpa_calc_bounds"][key], [field]: parseFloat(field === "cutoff" ? cutoff.value : gpa.value) } } });
|
||||
});
|
||||
};
|
||||
cutoff.addEventListener("change", save("cutoff"));
|
||||
gpa.addEventListener("change", save("gpa"));
|
||||
host.appendChild(row);
|
||||
});
|
||||
// Highlight the preset that currently matches the saved scale, if either does.
|
||||
const modal = host.closest("#canvasrefined-gpa-modal");
|
||||
if (modal) {
|
||||
const json = JSON.stringify(bounds);
|
||||
modal.querySelectorAll(".cr-gpa-preset").forEach(btn => {
|
||||
const preset = btn.dataset.preset === "letter" ? GPA_BY_LETTER_PRESET : GPA_PLUS_MINUS_PRESET;
|
||||
btn.classList.toggle("active", JSON.stringify(preset) === json);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeGpaModal(dashboardContainer, container, container2) {
|
||||
const modal = document.getElementById("canvasrefined-gpa-modal");
|
||||
if (modal) {
|
||||
if (modal.__crKeyHandler) document.removeEventListener("keydown", modal.__crKeyHandler);
|
||||
modal.remove();
|
||||
}
|
||||
// Return the calculator body to its resting spot next to the GPA card;
|
||||
// placeGPAElements corrects the position for the current widget placement.
|
||||
if (container2.parentNode) container2.parentNode.insertBefore(container, container2.nextSibling);
|
||||
container.style.display = "none";
|
||||
placeGPAElements(dashboardContainer, container, container2);
|
||||
}
|
||||
|
||||
function openGpaModal(dashboardContainer, container, container2) {
|
||||
closeGpaModal(dashboardContainer, container, container2);
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "canvasrefined-gpa-modal";
|
||||
modal.innerHTML = `
|
||||
<div class="cr-gpa-modal" role="dialog" aria-modal="true" aria-label="GPA Calculator">
|
||||
<header class="cr-gpa-head">
|
||||
<div class="cr-gpa-head-titles">
|
||||
<span class="cr-gpa-title">GPA Calculator</span>
|
||||
<span class="cr-gpa-subtitle">Changes save automatically</span>
|
||||
</div>
|
||||
<div class="cr-gpa-results">
|
||||
<div class="cr-gpa-result" data-gpa-mirror="unweighted"><span class="cr-gpa-result-value">--</span><span class="cr-gpa-result-label">Current</span></div>
|
||||
<div class="cr-gpa-result" data-gpa-mirror="weighted"><span class="cr-gpa-result-value">--</span><span class="cr-gpa-result-label">Weighted</span></div>
|
||||
<div class="cr-gpa-result" data-gpa-mirror="cumulative"><span class="cr-gpa-result-value">--</span><span class="cr-gpa-result-label">Cumulative</span></div>
|
||||
</div>
|
||||
<button class="cr-gpa-close" type="button" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
<div class="cr-gpa-body">
|
||||
<section class="cr-gpa-pane cr-gpa-pane-courses">
|
||||
<div class="cr-gpa-pane-head">Courses</div>
|
||||
<div class="cr-gpa-colhead" aria-hidden="true"><span></span><span>Course</span><span>Grade</span><span>Credits</span><span>Course type</span><span class="cr-gpa-colhead-custom">Custom grade</span></div>
|
||||
<div class="cr-gpa-courses-host"></div>
|
||||
</section>
|
||||
<aside class="cr-gpa-pane cr-gpa-pane-scale">
|
||||
<div class="cr-gpa-pane-head">Grade scale</div>
|
||||
<div class="cr-gpa-seg">
|
||||
<button class="cr-gpa-preset" data-preset="plusminus" type="button">+/- Scale</button>
|
||||
<button class="cr-gpa-preset" data-preset="letter" type="button">Letter only</button>
|
||||
</div>
|
||||
<div class="cr-gpa-scale-head" aria-hidden="true"><span></span><span>Min %</span><span>GPA</span></div>
|
||||
<div class="cr-gpa-modal-bounds"></div>
|
||||
<p class="cr-gpa-note">A percentage earns the GPA of the first row whose Min % is at or below it. If your school doesn't use +/- grades, set the Min % of the +/- rows to 101 to take them out of the math.</p>
|
||||
</aside>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Only show the result tiles the user actually tracks on the card.
|
||||
if (options.gpa_calc_weighted !== true) modal.querySelector('[data-gpa-mirror="weighted"]').style.display = "none";
|
||||
if (options.gpa_calc_cumulative !== true) modal.querySelector('[data-gpa-mirror="cumulative"]').style.display = "none";
|
||||
|
||||
// Seed the result tiles from the card's current numbers; calculateGPA2
|
||||
// keeps them live from here on (see its mirror block).
|
||||
modal.querySelectorAll("[data-gpa-mirror]").forEach(tile => {
|
||||
const src = document.getElementById("canvasrefined-gpa-" + tile.dataset.gpaMirror);
|
||||
if (src) tile.querySelector(".cr-gpa-result-value").textContent = src.textContent || "--";
|
||||
});
|
||||
|
||||
// The calculator body (with all its wired course rows) MOVES into the
|
||||
// modal while open — one set of course nodes in the DOM, and
|
||||
// calculateGPA2 keeps working unchanged.
|
||||
modal.querySelector(".cr-gpa-courses-host").appendChild(container);
|
||||
container.style.display = "block";
|
||||
|
||||
const boundsHost = modal.querySelector(".cr-gpa-modal-bounds");
|
||||
renderGpaBoundsEditor(boundsHost);
|
||||
modal.querySelector('[data-preset="plusminus"]').addEventListener("click", () => {
|
||||
chrome.storage.sync.set({ "gpa_calc_bounds": GPA_PLUS_MINUS_PRESET }, () => renderGpaBoundsEditor(boundsHost));
|
||||
});
|
||||
modal.querySelector('[data-preset="letter"]').addEventListener("click", () => {
|
||||
chrome.storage.sync.set({ "gpa_calc_bounds": GPA_BY_LETTER_PRESET }, () => renderGpaBoundsEditor(boundsHost));
|
||||
});
|
||||
|
||||
const close = () => closeGpaModal(dashboardContainer, container, container2);
|
||||
// Escape closes from anywhere (the old modal only caught keys while it
|
||||
// itself held focus).
|
||||
modal.__crKeyHandler = (e) => { if (e.key === "Escape") close(); };
|
||||
document.addEventListener("keydown", modal.__crKeyHandler);
|
||||
modal.querySelector(".cr-gpa-close").addEventListener("click", close);
|
||||
modal.addEventListener("mousedown", (e) => { if (e.target === modal) close(); });
|
||||
}
|
||||
|
||||
function setupGPACalc() {
|
||||
if (current_page !== "/" && current_page !== "") return;
|
||||
try {
|
||||
@ -7798,13 +8176,9 @@ function setupGPACalc() {
|
||||
|
||||
container.innerHTML = '<h3 class="canvasrefined-gpa-header">GPA Calculator</h3><div class="canvasrefined-gpa-courses-container"><div class="canvasrefined-gpa-courses"></div></div>';
|
||||
|
||||
if (options.gpa_calc_prepend === true) {
|
||||
dashboardContainer.prepend(container2);
|
||||
dashboardContainer.prepend(container);
|
||||
} else {
|
||||
dashboardContainer.appendChild(container2);
|
||||
dashboardContainer.appendChild(container);
|
||||
}
|
||||
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;
|
||||
@ -7815,20 +8189,6 @@ function setupGPACalc() {
|
||||
|
||||
container.style.display = "none";
|
||||
|
||||
editBtn.addEventListener("click", () => {
|
||||
if (container.style.display === "none") {
|
||||
container.style.display = "inline-block";
|
||||
editBtn.textContent = "Close Calculator";
|
||||
} else {
|
||||
container.style.display = "none";
|
||||
editBtn.textContent = "Edit Calculator";
|
||||
}
|
||||
// Opening/closing changes the card grid's row composition
|
||||
// (the expanded calculator claims a full-width row), so
|
||||
// uneven-row centering has to recompute either way.
|
||||
requestAnimationFrame(centerUnevenGridRows);
|
||||
});
|
||||
|
||||
container2.dataset.canvasrefinedGpaRendered = "true";
|
||||
container.dataset.canvasrefinedGpaRendered = "true";
|
||||
} else {
|
||||
@ -7837,18 +8197,7 @@ function setupGPACalc() {
|
||||
if (weighted) weighted.style.display = options.gpa_calc_weighted ? "block" : "none";
|
||||
if (cumulative) cumulative.style.display = options.gpa_calc_cumulative ? "block" : "none";
|
||||
|
||||
const shouldPrepend = options.gpa_calc_prepend === true;
|
||||
if (shouldPrepend) {
|
||||
if (dashboardContainer.children[0] !== container || dashboardContainer.children[1] !== container2) {
|
||||
dashboardContainer.insertBefore(container, dashboardContainer.firstChild);
|
||||
dashboardContainer.insertBefore(container2, container.nextSibling);
|
||||
}
|
||||
} else {
|
||||
if (dashboardContainer.lastElementChild !== container || container2.nextElementSibling !== container) {
|
||||
dashboardContainer.appendChild(container2);
|
||||
dashboardContainer.appendChild(container);
|
||||
}
|
||||
}
|
||||
placeGPAElements(dashboardContainer, container, container2);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -8142,6 +8491,30 @@ function wireDashboardNotes(notes) {
|
||||
});
|
||||
}
|
||||
|
||||
// Mount the notes at the position chosen in the Widgets tab:
|
||||
// top — full-width container above the card grid
|
||||
// cards_start / cards_end — card-shaped cell inside the grid, before/after cards
|
||||
// below — full-width container below the grid, with some margin
|
||||
function placeDashboardNotes(notes, container) {
|
||||
const pos = options.dashboard_notes_position || "top";
|
||||
if (notes.dataset.crPos === pos) return;
|
||||
notes.classList.toggle("in-cards", pos === "cards_start" || pos === "cards_end");
|
||||
const parent = container.parentNode;
|
||||
if (pos === "top" || pos === "below") {
|
||||
// Full-width container above/below the cards; shares the row with any
|
||||
// other container-mode widget, widths from the Widgets tab.
|
||||
placeWidgetInRow("dashboard_notes", pos, [notes], container);
|
||||
} else {
|
||||
releaseWidgetFromRow("dashboard_notes", [notes], container);
|
||||
if (pos === "cards_start") {
|
||||
container.prepend(notes);
|
||||
} else {
|
||||
container.appendChild(notes);
|
||||
}
|
||||
}
|
||||
notes.dataset.crPos = pos;
|
||||
}
|
||||
|
||||
function loadDashboardNotes() {
|
||||
const container = document.querySelector("#DashboardCard_Container");
|
||||
if (options.dashboard_notes === true) {
|
||||
@ -8156,11 +8529,6 @@ function loadDashboardNotes() {
|
||||
notes = document.createElement("div");
|
||||
notes.classList.add("canvasrefined-dashboard-notes");
|
||||
notes.innerHTML = DASHBOARD_NOTES_HTML;
|
||||
// Mount as a full-width sibling above the card grid. Prepending inside the
|
||||
// DashboardCard_Container makes the notes a masonry/grid cell (narrow & broken).
|
||||
const parent = container.parentNode;
|
||||
if (parent) parent.insertBefore(notes, container);
|
||||
else container.prepend(notes);
|
||||
wireDashboardNotes(notes);
|
||||
} else {
|
||||
notes.style.display = "";
|
||||
@ -8174,6 +8542,7 @@ function loadDashboardNotes() {
|
||||
renderDashboardNotesPreview(rendered, editor ? editor.value : "");
|
||||
}
|
||||
}
|
||||
placeDashboardNotes(notes, container);
|
||||
} else {
|
||||
let notes = document.querySelector('.canvasrefined-dashboard-notes');
|
||||
if (notes) notes.style.display = "none";
|
||||
@ -8339,7 +8708,9 @@ function applyAestheticChanges() {
|
||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-top:0!important;margin-left:0!important}`;
|
||||
const centerGrid = options.center_cards === true;
|
||||
const gpaOn = options.gpa_calc === true;
|
||||
const gpaTop = options.gpa_calc_prepend === true;
|
||||
const gpaPos = gpaCalcPosition();
|
||||
const gpaInsideGrid = gpaPos === "cards_start" || gpaPos === "cards_end";
|
||||
const gpaFirst = gpaPos === "cards_start";
|
||||
const flexGrid = options.card_grid_flex === true;
|
||||
if (flexGrid) {
|
||||
// Flexible grid: fixed-width tracks (cards keep their natural
|
||||
@ -8363,17 +8734,17 @@ function applyAestheticChanges() {
|
||||
// Grid gaps alone control spacing (beats the customCardStyles
|
||||
// margin rules emitted later via higher specificity).
|
||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{margin-right:0!important;margin-bottom:0!important}`;
|
||||
if (gpaOn) {
|
||||
if (gpaTop) {
|
||||
// Top mode: definite first-row placement, one track wide;
|
||||
// the expanded calculator takes the full second row.
|
||||
if (gpaOn && gpaInsideGrid) {
|
||||
if (gpaFirst) {
|
||||
// "Before cards": definite first-row placement, one track
|
||||
// wide; the expanded calculator takes the full second row.
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1!important}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||
} else {
|
||||
// Bottom mode: the GPA card auto-places in DOM order, so it
|
||||
// flows right after the last course card — filling the next
|
||||
// open slot instead of stranding below the grid — and the
|
||||
// expanded calculator follows on a full-width row.
|
||||
// "After cards": the GPA card auto-places in DOM order, so
|
||||
// it flows right after the last course card — filling the
|
||||
// next open slot instead of stranding below the grid — and
|
||||
// the expanded calculator follows on a full-width row.
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-column:1/-1!important}`;
|
||||
}
|
||||
}
|
||||
@ -8385,28 +8756,30 @@ function applyAestheticChanges() {
|
||||
for (let m = gridCols - 1; m >= 1; m--) {
|
||||
const tooWideFor = (m + 1) * cardW + m * gridColGap;
|
||||
if (m === 1) {
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:minmax(0,1fr)!important}.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{width:100%!important;min-width:0!important}}`;
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:minmax(0,1fr)!important}.ic-DashboardCard__box__container > .ic-DashboardCard${gpaInsideGrid ? ",.ic-DashboardCard__box__container > .canvasrefined-gpa-card" : ""}{width:100%!important;min-width:0!important}}`;
|
||||
} else {
|
||||
style.textContent += `@container (max-width:${tooWideFor - 1}px){.ic-DashboardCard__box__container{grid-template-columns:repeat(${m},${cardW}px)!important}}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const totalRows = gridRows + (gpaOn ? 2 : 0);
|
||||
const totalRows = gridRows + (gpaOn && gpaInsideGrid ? 2 : 0);
|
||||
style.textContent += `.ic-DashboardCard__box__container{display:grid!important;grid-template-columns:repeat(${gridCols * 2},minmax(0,max-content))!important;grid-template-rows:repeat(${totalRows},minmax(0,auto))!important;grid-auto-rows:0!important;column-gap:${gridColGap}px!important;row-gap:${gridRowGap}px!important;align-items:start!important;overflow:hidden!important${centerGrid ? ";justify-content:center!important" : ""}}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .ic-DashboardCard,.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-column:span 2!important}`;
|
||||
if (gpaOn) {
|
||||
// "Move to top" (gpa_calc_prepend) decides where the GPA card
|
||||
// sits: on -> pinned to the first slot (the expanded calculator
|
||||
// takes the full second row, and cards auto-place around both);
|
||||
// off -> the GPA card is a regular auto-placed item (one card
|
||||
// slot wide via the span rule above), so it flows into the next
|
||||
// open slot after the course cards — filling the leftover
|
||||
// spaces of a partial last row instead of squatting alone in a
|
||||
// dedicated row below the grid. The expanded calculator always
|
||||
// takes a full-width row: auto-placed right after the GPA card
|
||||
// in bottom mode, and display:none while collapsed so it
|
||||
// consumes no cells.
|
||||
if (gpaTop) {
|
||||
if (gpaOn && gpaInsideGrid) {
|
||||
// The Widgets position dropdown decides where the GPA card
|
||||
// sits: "Start of cards" -> pinned to the first slot (the
|
||||
// expanded calculator takes the full second row, and cards
|
||||
// auto-place around both); "End of cards" -> the GPA card is a
|
||||
// regular auto-placed item (one card slot wide via the span
|
||||
// rule above), so it flows into the next open slot after the
|
||||
// course cards — filling the leftover spaces of a partial last
|
||||
// row instead of squatting alone in a dedicated row below the
|
||||
// grid. The expanded calculator always takes a full-width row:
|
||||
// auto-placed right after the GPA card in end mode, and
|
||||
// display:none while collapsed so it consumes no cells.
|
||||
// "Top of page"/"Below cards" place the GPA elements outside
|
||||
// the grid container entirely, so no grid rules are needed.
|
||||
if (gpaFirst) {
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa-card{grid-row:1!important;grid-column:1/span 2!important}`;
|
||||
style.textContent += `.ic-DashboardCard__box__container > .canvasrefined-gpa{grid-row:2!important;grid-column:1/-1!important}`;
|
||||
} else {
|
||||
|
||||
311
js/popup.js
311
js/popup.js
@ -128,6 +128,8 @@ const defaultOptions = {
|
||||
"dashboard_notes": false,
|
||||
"dashboard_notes_text": "",
|
||||
"dashboard_notes_mode": "edit",
|
||||
"dashboard_notes_position": "top",
|
||||
"gpa_calc_position": "cards_end",
|
||||
"better_todo": true,
|
||||
"better_sidebar": false,
|
||||
"sidebar_scale": 100,
|
||||
@ -1001,9 +1003,219 @@ function toggleAlternateColorsVisibility(darkModeOn) {
|
||||
if (wrap) wrap.style.display = darkModeOn ? "none" : "";
|
||||
}
|
||||
|
||||
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
|
||||
// Widgets tab: position dropdowns for widgets that support placement.
|
||||
const WIDGET_LIST = [
|
||||
{ id: "dashboard_notes", nameKey: "dashboard_notes" },
|
||||
{ id: "gpa_calc", nameKey: "gpa_calculator" },
|
||||
];
|
||||
|
||||
function widgetPosFromSync(id, sync) {
|
||||
if (id === "dashboard_notes") return sync.dashboard_notes_position || "top";
|
||||
if (id === "gpa_calc") {
|
||||
if (sync.gpa_calc_position !== undefined) return sync.gpa_calc_position;
|
||||
return sync.gpa_calc_prepend === true ? "cards_start" : "cards_end";
|
||||
}
|
||||
return "top";
|
||||
}
|
||||
|
||||
const WIDGET_ORDER_MIN_PCT = 15;
|
||||
const WIDGET_ORDER_SNAP_PCT = 4;
|
||||
|
||||
// widget_order entries: [{id, size}] where size is the widget's % width of
|
||||
// the shared container. Legacy plain-id arrays are treated as equal sizes.
|
||||
function parseWidgetOrderEntries(raw) {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map(e => {
|
||||
if (typeof e === "string") return { id: e, size: null };
|
||||
if (e && typeof e === "object" && e.id) {
|
||||
const n = Number(e.size);
|
||||
return { id: e.id, size: Number.isFinite(n) && n > 0 ? n : null };
|
||||
}
|
||||
return null;
|
||||
}).filter(e => e && WIDGET_LIST.some(w => w.id === e.id));
|
||||
}
|
||||
|
||||
function widgetDisplayName(id) {
|
||||
const info = WIDGET_LIST.find(w => w.id === id);
|
||||
return (info && chrome.i18n.getMessage(info.nameKey)) || id;
|
||||
}
|
||||
|
||||
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 render = (sync) => {
|
||||
const areas = { top: [], below: [] };
|
||||
WIDGET_LIST.forEach(w => {
|
||||
if (sync[w.id] !== true) return;
|
||||
const pos = widgetPosFromSync(w.id, sync);
|
||||
if (pos === "top" || pos === "below") areas[pos].push(w.id);
|
||||
});
|
||||
let any = false;
|
||||
["top", "below"].forEach(area => {
|
||||
const strip = document.getElementById("widget-order-" + area);
|
||||
const wrapEl = document.getElementById("widget-order-" + area + "-wrap");
|
||||
if (!strip || !wrapEl) return;
|
||||
strip.dataset.area = area;
|
||||
const ids = areas[area];
|
||||
if (ids.length < 2) {
|
||||
wrapEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
any = true;
|
||||
wrapEl.style.display = "";
|
||||
// Saved entries first (order + widths), then any newcomers.
|
||||
const entries = parseWidgetOrderEntries((sync.widget_order || {})[area]).filter(e => ids.includes(e.id));
|
||||
ids.forEach(id => { if (!entries.some(e => e.id === id)) entries.push({ id, size: null }); });
|
||||
renderWidgetOrderStrip(strip, entries);
|
||||
});
|
||||
wrap.style.display = any ? "" : "none";
|
||||
};
|
||||
|
||||
// Reordering: drag a box horizontally past a neighbor.
|
||||
["top", "below"].forEach(area => {
|
||||
const strip = document.getElementById("widget-order-" + area);
|
||||
if (!strip) return;
|
||||
strip.addEventListener("dragover", e => {
|
||||
e.preventDefault();
|
||||
const dragging = strip.querySelector(".wos-item.dragging");
|
||||
if (!dragging) return;
|
||||
let ref = null;
|
||||
for (const el of strip.querySelectorAll(".wos-item:not(.dragging)")) {
|
||||
const box = el.getBoundingClientRect();
|
||||
if (e.clientX < box.left + box.width / 2) { ref = el; break; }
|
||||
}
|
||||
if (ref) strip.insertBefore(dragging, ref);
|
||||
else strip.appendChild(dragging);
|
||||
});
|
||||
strip.addEventListener("drop", e => e.preventDefault());
|
||||
});
|
||||
|
||||
const refresh = () => chrome.storage.sync.get(KEYS, render);
|
||||
|
||||
refresh();
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== "sync") return;
|
||||
if (KEYS.some(k => changes[k])) refresh();
|
||||
});
|
||||
}
|
||||
|
||||
function renderWidgetOrderStrip(strip, entries) {
|
||||
strip.textContent = "";
|
||||
entries.forEach((entry, i) => {
|
||||
if (i > 0) {
|
||||
const divider = document.createElement("div");
|
||||
divider.className = "wos-divider";
|
||||
wireWidgetOrderDivider(strip, divider);
|
||||
strip.appendChild(divider);
|
||||
}
|
||||
const item = document.createElement("div");
|
||||
item.className = "wos-item";
|
||||
item.dataset.id = entry.id;
|
||||
item.dataset.size = entry.size == null ? (100 / entries.length) : entry.size;
|
||||
item.style.flexBasis = item.dataset.size + "%";
|
||||
item.draggable = true;
|
||||
item.textContent = widgetDisplayName(entry.id);
|
||||
item.addEventListener("dragstart", e => {
|
||||
item.classList.add("dragging");
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
try { e.dataTransfer.setData("text/plain", entry.id); } catch (_) {}
|
||||
});
|
||||
item.addEventListener("dragend", () => {
|
||||
item.classList.remove("dragging");
|
||||
saveWidgetStrip(strip.dataset.area, strip);
|
||||
});
|
||||
strip.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Dragging a divider resizes its two neighboring boxes; the divider position
|
||||
// is exactly the % split between them.
|
||||
function wireWidgetOrderDivider(strip, divider) {
|
||||
divider.addEventListener("pointerdown", e => {
|
||||
e.preventDefault();
|
||||
try { divider.setPointerCapture(e.pointerId); } catch (_) {}
|
||||
const idx = [...strip.children].indexOf(divider);
|
||||
const prev = strip.children[idx - 1];
|
||||
const next = strip.children[idx + 1];
|
||||
if (!prev || !next || !prev.classList.contains("wos-item") || !next.classList.contains("wos-item")) return;
|
||||
const stripWidth = strip.getBoundingClientRect().width || 1;
|
||||
const startX = e.clientX;
|
||||
const p0 = parseFloat(prev.dataset.size);
|
||||
const n0 = parseFloat(next.dataset.size);
|
||||
const onMove = ev => {
|
||||
const dx = (ev.clientX - startX) / stripWidth * 100;
|
||||
let p = p0 + dx;
|
||||
p = Math.max(WIDGET_ORDER_MIN_PCT, Math.min(p, p0 + n0 - WIDGET_ORDER_MIN_PCT));
|
||||
// Snap to even fractions of the widget count: with 2 widgets the
|
||||
// bar snaps to 50/50, with 3 to the 1/3 and 2/3 marks, etc.
|
||||
const count = strip.querySelectorAll(".wos-item").length;
|
||||
for (let k = 1; k < count; k++) {
|
||||
const mark = k / count * 100;
|
||||
if (Math.abs(p - mark) < WIDGET_ORDER_SNAP_PCT) { p = mark; break; }
|
||||
}
|
||||
const n = p0 + n0 - p;
|
||||
prev.dataset.size = p;
|
||||
next.dataset.size = n;
|
||||
prev.style.flexBasis = p + "%";
|
||||
next.style.flexBasis = n + "%";
|
||||
};
|
||||
const onUp = () => {
|
||||
divider.removeEventListener("pointermove", onMove);
|
||||
divider.removeEventListener("pointerup", onUp);
|
||||
divider.removeEventListener("pointercancel", onUp);
|
||||
saveWidgetStrip(strip.dataset.area, strip);
|
||||
};
|
||||
divider.addEventListener("pointermove", onMove);
|
||||
divider.addEventListener("pointerup", onUp);
|
||||
divider.addEventListener("pointercancel", onUp);
|
||||
});
|
||||
}
|
||||
|
||||
function saveWidgetStrip(area, strip) {
|
||||
const items = [...strip.querySelectorAll(".wos-item")];
|
||||
if (items.length < 2) return;
|
||||
let sizes = items.map(el => parseFloat(el.dataset.size) || 100 / items.length);
|
||||
const total = sizes.reduce((a, b) => a + b, 0) || 100;
|
||||
sizes = sizes.map(v => v / total * 100);
|
||||
const rounded = sizes.map(v => Math.round(v));
|
||||
rounded[rounded.length - 1] += 100 - rounded.reduce((a, b) => a + b, 0);
|
||||
const order = items.map((el, i) => ({ id: el.dataset.id, size: rounded[i] }));
|
||||
chrome.storage.sync.get(["widget_order"], sync => {
|
||||
const wo = Object.assign({}, sync.widget_order || {});
|
||||
wo[area] = order;
|
||||
chrome.storage.sync.set({ widget_order: wo });
|
||||
});
|
||||
}
|
||||
|
||||
function setupWidgetPosition() {
|
||||
[
|
||||
{ select: "dashboard_notes_position", key: "dashboard_notes_position" },
|
||||
{ select: "gpa_calc_position", key: "gpa_calc_position" },
|
||||
].forEach(({ select, key }) => {
|
||||
const el = document.getElementById(select);
|
||||
if (!el) return;
|
||||
chrome.storage.sync.get([key, "gpa_calc_prepend"], sync => {
|
||||
let value = sync[key];
|
||||
if (value === undefined && key === "gpa_calc_position") {
|
||||
// Legacy "Move to top" checkbox -> equivalent dropdown position.
|
||||
value = sync["gpa_calc_prepend"] === true ? "cards_start" : "cards_end";
|
||||
chrome.storage.sync.set({ [key]: value });
|
||||
}
|
||||
el.value = value || defaultOptions.sync[key] || "top";
|
||||
});
|
||||
el.addEventListener("change", function () {
|
||||
chrome.storage.sync.set({ [key]: this.value });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Hide a toggle's sub-options when it's off; auto_dark only hides its time
|
||||
// clocks.
|
||||
function toggleSubOptionsVisibility(option, isOn) {
|
||||
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"];
|
||||
const togglesWithSubOptions = ["dashboard_notes", "gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"];
|
||||
if (!togglesWithSubOptions.includes(option)) return;
|
||||
const optionEl = document.getElementById(option);
|
||||
if (!optionEl) return;
|
||||
@ -1162,6 +1374,15 @@ function setupFeatureSearch(menu) {
|
||||
add({ key: "option:" + opt.id, text: name.textContent.trim(), el: opt, action: () => goToMainElement(opt) });
|
||||
});
|
||||
|
||||
// Tabs: option toggles that live inside a tab (e.g. Dashboard Notes in Widgets)
|
||||
document.querySelectorAll(".tab .option").forEach(opt => {
|
||||
if (!opt.id) return;
|
||||
const name = opt.querySelector(".option-name");
|
||||
if (!name) return;
|
||||
if (shouldSkip(opt)) return;
|
||||
add({ key: "option:" + opt.id, text: name.textContent.trim(), el: opt, action: () => goToTabElement(opt) });
|
||||
});
|
||||
|
||||
// Home: sub-options / timesets / labelled rows (e.g. "Use dd/mm", "Start time", max-items)
|
||||
document.querySelectorAll(".options .sub-option, .options .timeset, .options .sub-options > div").forEach(sub => {
|
||||
if (shouldSkip(sub)) return;
|
||||
@ -1230,7 +1451,7 @@ function setupFeatureSearch(menu) {
|
||||
});
|
||||
|
||||
// Tab: standalone action buttons with distinct labels (GPA scale presets, revert colors).
|
||||
["gpa-plus-minus", "gpa-by-letter", "revert-colors", "clearCustomBackground"].forEach(id => {
|
||||
["revert-colors", "clearCustomBackground"].forEach(id => {
|
||||
const btn = document.getElementById(id);
|
||||
if (!btn || shouldSkip(btn)) return;
|
||||
const text = btn.textContent.trim();
|
||||
@ -1378,7 +1599,6 @@ function setup() {
|
||||
"hide_completed_cards",
|
||||
"equal_height_cards",
|
||||
// "todo_overdues",
|
||||
"gpa_calc_prepend",
|
||||
"auto_dark",
|
||||
"assignment_date_format",
|
||||
"custom_font_skip_p",
|
||||
@ -1411,14 +1631,15 @@ function setup() {
|
||||
setup: displayAdvancedCards,
|
||||
tab: ".advanced",
|
||||
},
|
||||
"gpa-bounds-btn": {
|
||||
setup: displayGPABounds,
|
||||
tab: ".gpa-bounds-container",
|
||||
},
|
||||
|
||||
"custom-font-btn": {
|
||||
setup: displayCustomFont,
|
||||
tab: ".custom-font-container",
|
||||
},
|
||||
"gpa-bounds-btn": {
|
||||
setup: null,
|
||||
tab: ".gpa-bounds-container",
|
||||
},
|
||||
"card-colors-btn": { setup: null, tab: ".card-colors-container" },
|
||||
"customize-dark-btn": {
|
||||
setup: displayDarkModeFixUrls,
|
||||
@ -1645,12 +1866,15 @@ function setup() {
|
||||
chrome.storage.sync.get("todo_celebrations_enabled", v => {
|
||||
toggleCelebrationOptionsVisibility(v["todo_celebrations_enabled"] !== false);
|
||||
});
|
||||
["gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => {
|
||||
["dashboard_notes", "gpa_calc", "assignments_due", "better_todo", "auto_dark", "grade_analytics", "hide_personal_details"].forEach(opt => {
|
||||
toggleSubOptionsVisibility(opt, sync[opt] === true);
|
||||
});
|
||||
toggleAlternateColorsVisibility(sync["dark_mode"] === true);
|
||||
});
|
||||
|
||||
setupWidgetPosition();
|
||||
setupWidgetOrder();
|
||||
|
||||
chrome.storage.sync.get(menu.checkboxes, sync => {
|
||||
menu.checkboxes.forEach(option => {
|
||||
const checkbox = document.querySelector("#" + option);
|
||||
@ -2208,41 +2432,6 @@ function setup() {
|
||||
|
||||
document.getElementById("submit-form-btn").addEventListener("click", displayThemeSubmissionForm);
|
||||
|
||||
document.getElementById("gpa-plus-minus").addEventListener("click", () => {
|
||||
applyGPAPreset({
|
||||
"A+": { "cutoff": 97, "gpa": 4.0 },
|
||||
"A": { "cutoff": 93, "gpa": 4.0 },
|
||||
"A-": { "cutoff": 90, "gpa": 3.7 },
|
||||
"B+": { "cutoff": 87, "gpa": 3.3 },
|
||||
"B": { "cutoff": 83, "gpa": 3.0 },
|
||||
"B-": { "cutoff": 80, "gpa": 2.7 },
|
||||
"C+": { "cutoff": 77, "gpa": 2.3 },
|
||||
"C": { "cutoff": 73, "gpa": 2.0 },
|
||||
"C-": { "cutoff": 70, "gpa": 1.7 },
|
||||
"D+": { "cutoff": 67, "gpa": 1.3 },
|
||||
"D": { "cutoff": 63, "gpa": 1.0 },
|
||||
"D-": { "cutoff": 60, "gpa": 0.7 },
|
||||
"F": { "cutoff": 0, "gpa": 0 }
|
||||
});
|
||||
});
|
||||
document.getElementById("gpa-by-letter").addEventListener("click", () => {
|
||||
applyGPAPreset({
|
||||
"A+": { "cutoff": 97, "gpa": 4.0 },
|
||||
"A": { "cutoff": 93, "gpa": 4.0 },
|
||||
"A-": { "cutoff": 90, "gpa": 4.0 },
|
||||
"B+": { "cutoff": 87, "gpa": 3.0 },
|
||||
"B": { "cutoff": 83, "gpa": 3.0 },
|
||||
"B-": { "cutoff": 80, "gpa": 3.0 },
|
||||
"C+": { "cutoff": 77, "gpa": 2.0 },
|
||||
"C": { "cutoff": 73, "gpa": 2.0 },
|
||||
"C-": { "cutoff": 70, "gpa": 2.0 },
|
||||
"D+": { "cutoff": 67, "gpa": 1.0 },
|
||||
"D": { "cutoff": 63, "gpa": 1.0 },
|
||||
"D-": { "cutoff": 60, "gpa": 1.0 },
|
||||
"F": { "cutoff": 0, "gpa": 0 }
|
||||
});
|
||||
});
|
||||
|
||||
// Note: the card-style number inputs (imageSize, cardRoundness,
|
||||
// imageRoundness, cardSpacing, cardWidth, cardHeight, cardPadding) are wired
|
||||
// via their setup*Input
|
||||
@ -2292,13 +2481,6 @@ function setup() {
|
||||
if (skipPToggle) {
|
||||
skipPToggle.closest("span").addEventListener("click", (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function applyGPAPreset(bounds) {
|
||||
chrome.storage.sync.set({ "gpa_calc_bounds": bounds }, () => {
|
||||
displayGPABounds();
|
||||
});
|
||||
}
|
||||
|
||||
function setupCustomStyle(initial) {
|
||||
@ -2727,33 +2909,6 @@ function displayCustomFont() {
|
||||
});
|
||||
}
|
||||
|
||||
function displayGPABounds() {
|
||||
chrome.storage.sync.get(["gpa_calc_bounds"], storage => {
|
||||
const order = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F"];
|
||||
const el = document.querySelector(".gpa-bounds");
|
||||
el.textContent = "";
|
||||
order.forEach(key => {
|
||||
let inputs = makeElement("div", el, { "className": "gpa-bounds-item" });
|
||||
inputs.innerHTML += '<div><span class="gpa-bounds-grade"></span><input class="gpa-bounds-input gpa-bounds-cutoff" type="text"></input><span style="margin-left:6px;margin-right:6px;">%</span><input class="gpa-bounds-input gpa-bounds-gpa" type="text" value=></input><span style="margin-left:6px">GPA</span></div>';
|
||||
inputs.querySelector(".gpa-bounds-grade").textContent = key;
|
||||
inputs.querySelector(".gpa-bounds-cutoff").value = storage["gpa_calc_bounds"][key].cutoff;
|
||||
inputs.querySelector(".gpa-bounds-gpa").value = storage["gpa_calc_bounds"][key].gpa;
|
||||
|
||||
inputs.querySelector(".gpa-bounds-cutoff").addEventListener("change", function (e) {
|
||||
chrome.storage.sync.get(["gpa_calc_bounds"], existing => {
|
||||
chrome.storage.sync.set({ "gpa_calc_bounds": { ...existing["gpa_calc_bounds"], [key]: { ...existing["gpa_calc_bounds"][key], "cutoff": parseFloat(e.target.value) } } });
|
||||
});
|
||||
});
|
||||
|
||||
inputs.querySelector(".gpa-bounds-gpa").addEventListener("change", function (e) {
|
||||
chrome.storage.sync.get(["gpa_calc_bounds"], existing => {
|
||||
chrome.storage.sync.set({ "gpa_calc_bounds": { ...existing["gpa_calc_bounds"], [key]: { ...existing["gpa_calc_bounds"][key], "gpa": parseFloat(e.target.value) } } });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let removeAlert = null;
|
||||
|
||||
function clearAlert() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user