mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 13:57:17 +01:00
more changes
This commit is contained in:
parent
2860aa7c4d
commit
27dba082cb
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} benotete Aufgabe(n) ohne Fälligkeitsdatum werden nicht angezeigt."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Kurse ausschließen"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Gesamtnote im Zeitverlauf"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "Keine Vorschau verfügbar"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Datenschutz"
|
||||
"dev_tools": {
|
||||
"message": "Entwickler-Tools"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} erledigt"
|
||||
|
||||
@ -1292,8 +1292,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "No preview available"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Privacy"
|
||||
"dev_tools": {
|
||||
"message": "Dev tools"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} done"
|
||||
@ -2027,6 +2027,9 @@
|
||||
"heatmap_widget": {
|
||||
"message": "Heatmap"
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Exclude courses"
|
||||
},
|
||||
"widget_short_heatmap": {
|
||||
"message": "Heatmap"
|
||||
},
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} tarea(s) calificada(s) sin fecha de entrega no mostrada(s)."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Excluir cursos"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Nota general a lo largo del tiempo"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "No hay vista previa disponible"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Privacidad"
|
||||
"dev_tools": {
|
||||
"message": "Herramientas de desarrollo"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} hechas"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} devoir(s) noté(s) sans date d'échéance non affiché(s)."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Exclure des cours"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Note globale au fil du temps"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "Aucun aperçu disponible"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Confidentialité"
|
||||
"dev_tools": {
|
||||
"message": "Outils de développement"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} terminés"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} consegna/e valutata/e senza data di scadenza non mostrata/e."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Escludi corsi"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Voto complessivo nel tempo"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "Nessuna anteprima disponibile"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Privacy"
|
||||
"dev_tools": {
|
||||
"message": "Strumenti per sviluppatori"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} completati"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "期限のない評価済み課題{{COUNT}}件は表示されていません。"
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "除外するコース"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "総合評価の推移"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "プレビューを利用できません"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "プライバシー"
|
||||
"dev_tools": {
|
||||
"message": "開発者ツール"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} 完了"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} trabalho(s) avaliado(s) sem data de entrega não apresentado(s)."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Excluir disciplinas"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Nota global ao longo do tempo"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "Pré-visualização indisponível"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Privacidade"
|
||||
"dev_tools": {
|
||||
"message": "Ferramentas de programador"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} concluídos"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} оценённых заданий без срока не показано."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Исключить курсы"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Итоговая оценка во времени"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "Предпросмотр недоступен"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Конфиденциальность"
|
||||
"dev_tools": {
|
||||
"message": "Инструменты разработчика"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} выполнено"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} betygsatt(a) uppgift(er) utan förfallodatum visas inte."
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "Exkludera kurser"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "Totalt betyg över tid"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "Ingen förhandsvisning tillgänglig"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "Integritet"
|
||||
"dev_tools": {
|
||||
"message": "Utvecklarverktyg"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} klara"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "{{COUNT}} 项有评分但无截止日期的作业未显示。"
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "排除课程"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "总评成绩变化"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "没有可用的预览"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "隐私"
|
||||
"dev_tools": {
|
||||
"message": "开发者工具"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "{{DONE}}/{{TOTAL}} 已完成"
|
||||
|
||||
@ -611,6 +611,9 @@
|
||||
"ga_heatmap_undated": {
|
||||
"message": "有 {{COUNT}} 項已評分作業沒有到期日,因此未顯示。"
|
||||
},
|
||||
"heatmap_exclude_courses": {
|
||||
"message": "排除課程"
|
||||
},
|
||||
"ga_line_title": {
|
||||
"message": "總成績變化"
|
||||
},
|
||||
@ -1283,8 +1286,8 @@
|
||||
"preview_unavailable": {
|
||||
"message": "無法提供預覽"
|
||||
},
|
||||
"privacy": {
|
||||
"message": "隱私"
|
||||
"dev_tools": {
|
||||
"message": "開發人員工具"
|
||||
},
|
||||
"progress_done": {
|
||||
"message": "已完成 {{DONE}}/{{TOTAL}}"
|
||||
|
||||
@ -91,16 +91,22 @@ h2 {margin-top: 20px;font-weight:600}
|
||||
.custom-tf-row input {flex:none;width:58px;font-size:12px;background:var(--inputbg);border:none;padding:6px;color:#e2e2e2;border-radius:6px;font-family:inherit;text-align:right;box-sizing:border-box;color-scheme:dark;}
|
||||
.custom-tf-row input::-webkit-outer-spin-button, .custom-tf-row input::-webkit-inner-spin-button {opacity:1;}
|
||||
.todo-more-options {margin-top: 7px;}
|
||||
.celebration-multiselect {position:relative;}
|
||||
.celebration-multiselect-btn {display:flex;align-items:center;justify-content:space-between;gap:8px;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;cursor:pointer;text-align:left;}
|
||||
.celebration-multiselect-btn svg {flex:none;transition:transform .15s ease;}
|
||||
.celebration-multiselect.open .celebration-multiselect-btn svg {transform:rotate(-180deg);}
|
||||
.celebration-multiselect-menu {display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:60;background:#1e1e1e;border-radius:7px;padding:5px 0;max-height:180px;overflow-y:auto;box-shadow:0 6px 18px rgba(0,0,0,.45);}
|
||||
.celebration-multiselect.open .celebration-multiselect-menu {display:block;}
|
||||
.celebration-multiselect.flip-up .celebration-multiselect-menu {top:auto;bottom:calc(100% + 4px);}
|
||||
.celebration-option {display:flex;align-items:center;gap:7px;padding:5px 10px;font-size:12px;color:#e2e2e2;cursor:pointer;white-space:nowrap;user-select:none;}
|
||||
.celebration-option:hover {background:#2e2e2e;color:#fff;}
|
||||
.celebration-option input {accent-color:#56caf0;cursor:pointer;margin:0;}
|
||||
/* Checkbox dropdown shared by todo celebrations and the heatmap's course filter. */
|
||||
.cr-multiselect {position:relative;}
|
||||
.cr-multiselect-btn {display:flex;align-items:center;justify-content:space-between;gap:8px;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;cursor:pointer;text-align:left;}
|
||||
/* the current selection stays on one line; the menu is where long names wrap */
|
||||
.cr-multiselect-btn > span {min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.cr-multiselect-btn svg {flex:none;transition:transform .15s ease;}
|
||||
.cr-multiselect.open .cr-multiselect-btn svg {transform:rotate(-180deg);}
|
||||
/* grows past a narrow trigger (widget cards) instead of clipping the labels */
|
||||
.cr-multiselect-menu {display:none;position:absolute;top:calc(100% + 4px);left:0;right:auto;z-index:60;background:#1e1e1e;border-radius:7px;padding:5px 0;max-height:200px;overflow-y:auto;overflow-x:hidden;box-shadow:0 6px 18px rgba(0,0,0,.45);box-sizing:border-box;width:max-content;min-width:100%;max-width:min(320px, calc(100vw - 32px));}
|
||||
.cr-multiselect.open .cr-multiselect-menu {display:block;}
|
||||
.cr-multiselect.flip-up .cr-multiselect-menu {top:auto;bottom:calc(100% + 4px);}
|
||||
.cr-multiselect-option {display:flex;align-items:flex-start;gap:7px;padding:5px 10px;font-size:12px;line-height:1.3;color:#e2e2e2;cursor:pointer;user-select:none;}
|
||||
.cr-multiselect-option:hover {background:#2e2e2e;color:#fff;}
|
||||
.cr-multiselect-option span {overflow-wrap:anywhere;}
|
||||
.cr-multiselect-option input {accent-color:#56caf0;cursor:pointer;margin:1px 0 0;}
|
||||
.cr-multiselect-empty {padding:6px 10px;font-size:12px;line-height:1.3;color:#9aa3ab;}
|
||||
#todo_celebration_amount {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;}
|
||||
#todo_celebration_amount option {background:#1e1e1e;color:#e2e2e2;}
|
||||
#todo_celebration_amount option:checked,
|
||||
|
||||
@ -349,12 +349,12 @@
|
||||
</div>
|
||||
<div class="sub-option celebration-sub" id="celebration-type-row" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<span class="sub-text" data-i18n="celebration_type">Celebration type</span>
|
||||
<div class="celebration-multiselect" id="todo_celebrations">
|
||||
<button type="button" class="celebration-multiselect-btn" id="todo_celebrations_btn" aria-haspopup="listbox" aria-expanded="false">
|
||||
<div class="cr-multiselect" id="todo_celebrations">
|
||||
<button type="button" class="cr-multiselect-btn" id="todo_celebrations_btn" aria-haspopup="listbox" aria-expanded="false">
|
||||
<span id="todo_celebrations_label" data-i18n="celebration_confetti">Confetti</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6 6-6" /></svg>
|
||||
</button>
|
||||
<div class="celebration-multiselect-menu" id="todo_celebrations_menu" role="listbox" aria-multiselectable="true"></div>
|
||||
<div class="cr-multiselect-menu" id="todo_celebrations_menu" role="listbox" aria-multiselectable="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-option celebration-sub" id="celebration-amount-row" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
@ -1417,6 +1417,17 @@
|
||||
<input type="number" id="heatmap_height" class="card-input" min="0" max="2000" data-i18n-placeholder="auto" placeholder="auto" style="width:92px;margin:0;">
|
||||
<span class="sub-text">px</span>
|
||||
</div>
|
||||
<!-- Course filter: ticked courses are left out of the grid. -->
|
||||
<div class="sub-option" style="margin-top:6px;flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<span class="sub-text" data-i18n="heatmap_exclude_courses">Exclude courses</span>
|
||||
<div class="cr-multiselect" id="heatmap_excluded">
|
||||
<button type="button" class="cr-multiselect-btn" id="heatmap_excluded_btn" aria-haspopup="listbox" aria-expanded="false">
|
||||
<span id="heatmap_excluded_label" data-i18n="celebration_none">None</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6 6-6" /></svg>
|
||||
</button>
|
||||
<div class="cr-multiselect-menu" id="heatmap_excluded_menu" role="listbox" aria-multiselectable="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1571,7 +1582,7 @@
|
||||
<p class="sub-text" style="margin:0;font-size:11px;opacity:0.85;">Email is still available at <strong>sandlerguy5@gmail.com</strong> for private matters.</p>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<h3 class="header-small" data-i18n="privacy">Privacy</h3>
|
||||
<h3 class="header-small" data-i18n="dev_tools">Dev tools</h3>
|
||||
<div class="option" id="hide_personal_details">
|
||||
<input type="radio" id="off" name="hide_personal_details">
|
||||
<input type="radio" id="on" name="hide_personal_details">
|
||||
|
||||
@ -58,6 +58,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
"quick_links_position": "cards_end",
|
||||
"heatmap": false,
|
||||
"heatmap_position": "below",
|
||||
"heatmap_excluded_courses": [],
|
||||
"widget_top_height": 0,
|
||||
"widget_bottom_height": 0,
|
||||
"widget_row_height": 0,
|
||||
|
||||
@ -1128,6 +1128,13 @@ function applyOptionsChanges(changes) {
|
||||
loadHeatmapWidget();
|
||||
refreshCardGridLayout();
|
||||
break;
|
||||
case "heatmap_excluded_courses":
|
||||
// Rebuild from the cache (courses drop out immediately) and refetch the rest.
|
||||
crHeatmapData = null;
|
||||
crHeatmapLoadedAt = 0;
|
||||
crHeatmapStarted = false;
|
||||
crHeatmapEnsureData();
|
||||
break;
|
||||
case "quick_links_font_size":
|
||||
case "quick_links_icon_size":
|
||||
document.querySelectorAll(".canvasrefined-quick-links").forEach(quickLinksApplySizes);
|
||||
@ -8682,17 +8689,26 @@ async function crHeatmapFetchCourse(course) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Courses ticked in the widget's course filter (stored as ids; may be numbers).
|
||||
function crHeatmapExcludedIds() {
|
||||
const raw = options.heatmap_excluded_courses;
|
||||
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
||||
}
|
||||
|
||||
async function crHeatmapFetchAll() {
|
||||
if (crHeatmapLoading) return;
|
||||
crHeatmapLoading = true;
|
||||
crHeatmapRenderAll();
|
||||
try {
|
||||
const courses = await gsFetchCourseList();
|
||||
// Excluded courses are skipped here, so they cost no requests either.
|
||||
const excluded = crHeatmapExcludedIds();
|
||||
const items = [];
|
||||
let next = 0;
|
||||
const worker = async () => {
|
||||
while (next < courses.length) {
|
||||
const course = courses[next++];
|
||||
if (excluded.has(String(course.id))) continue;
|
||||
try {
|
||||
const list = await crHeatmapFetchCourse(course);
|
||||
for (const item of list) items.push(item);
|
||||
@ -8742,6 +8758,9 @@ function crHeatmapEnsureData() {
|
||||
function crHeatmapBuild(items) {
|
||||
const byDay = new Map();
|
||||
const range = { grades: { min: null, max: null }, subs: { min: null, max: null } };
|
||||
// Cached items keep their course id, so a course excluded from the widget drops out
|
||||
// of the grid right away instead of waiting for the next fetch.
|
||||
const excluded = crHeatmapExcludedIds();
|
||||
let maxCount = 0, subTotal = 0, undated = 0;
|
||||
const cellFor = (d, kind) => {
|
||||
const key = d.getTime();
|
||||
@ -8756,6 +8775,7 @@ function crHeatmapBuild(items) {
|
||||
return cell;
|
||||
};
|
||||
for (const item of items) {
|
||||
if (item.courseId != null && excluded.has(String(item.courseId))) continue;
|
||||
if (item.score != null && !item.excused && item.points > 0) {
|
||||
const d = crHeatmapDayStart(item.due);
|
||||
if (d) {
|
||||
|
||||
145
js/popup.js
145
js/popup.js
@ -41,6 +41,7 @@ const syncedSubOptions = [
|
||||
"daily_quotes_height",
|
||||
"quick_links_height",
|
||||
"heatmap_height",
|
||||
"heatmap_excluded_courses",
|
||||
"quick_links_font_size",
|
||||
"quick_links_icon_size",
|
||||
"auto_dark",
|
||||
@ -292,6 +293,8 @@ const defaultOptions = {
|
||||
"daily_quotes_height": 0,
|
||||
"quick_links_height": 0,
|
||||
"heatmap_height": 0,
|
||||
// Courses left out of the heatmap widget (course ids as strings).
|
||||
"heatmap_excluded_courses": [],
|
||||
// Quick Links tile label font size in px.
|
||||
"quick_links_font_size": 12,
|
||||
// Quick Links tile icon box size in px.
|
||||
@ -587,19 +590,44 @@ function resolveCelebrations(raw, legacyConfetti) {
|
||||
return legacyConfetti === false ? [] : ["confetti"];
|
||||
}
|
||||
|
||||
function setupCelebrationsSelect(initial) {
|
||||
const wrap = document.getElementById("todo_celebrations");
|
||||
const btn = document.getElementById("todo_celebrations_btn");
|
||||
const menu = document.getElementById("todo_celebrations_menu");
|
||||
const label = document.getElementById("todo_celebrations_label");
|
||||
if (!wrap || !btn || !menu || !label) return;
|
||||
// Labels are rendered through innerHTML, so escape user-provided text (course names).
|
||||
function escapePopupHtml(value) {
|
||||
return String(value === null || value === undefined ? "" : value)
|
||||
.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
}
|
||||
|
||||
// Checkbox dropdown ("select all that apply") shared by the todo celebrations and the
|
||||
// heatmap's course filter. Options are { id, label } pairs; every change is handed back
|
||||
// through onChange so the caller decides what to persist.
|
||||
function setupPopupMultiSelect({ wrapId, btnId, menuId, labelId, emptyText, onChange }) {
|
||||
const wrap = document.getElementById(wrapId);
|
||||
const btn = document.getElementById(btnId);
|
||||
const menu = document.getElementById(menuId);
|
||||
const label = document.getElementById(labelId);
|
||||
if (!wrap || !btn || !menu || !label) return null;
|
||||
|
||||
const noneLabel = getMsg("celebration_none") || "None";
|
||||
let items = [];
|
||||
let selected = [];
|
||||
|
||||
const updateLabel = () => {
|
||||
if (selected.length === 0) label.textContent = noneLabel;
|
||||
else if (selected.length === 1) {
|
||||
const item = items.find(i => i.id === selected[0]);
|
||||
label.textContent = item ? item.label : noneLabel;
|
||||
} else {
|
||||
label.textContent = replaceCountPlaceholder(getMsg("selected_count") || "{{COUNT}} selected", selected.length);
|
||||
}
|
||||
};
|
||||
const renderMenu = () => {
|
||||
menu.innerHTML = CELEBRATION_TYPES.map(t => `
|
||||
<label class="celebration-option">
|
||||
<input type="checkbox" value="${t.id}" ${selected.includes(t.id) ? "checked" : ""}>
|
||||
<span>${t.label}</span>
|
||||
if (!items.length) {
|
||||
menu.innerHTML = `<p class="cr-multiselect-empty">${escapePopupHtml(emptyText || noneLabel)}</p>`;
|
||||
return;
|
||||
}
|
||||
menu.innerHTML = items.map(item => `
|
||||
<label class="cr-multiselect-option">
|
||||
<input type="checkbox" value="${escapePopupHtml(item.id)}" ${selected.includes(item.id) ? "checked" : ""}>
|
||||
<span>${escapePopupHtml(item.label)}</span>
|
||||
</label>
|
||||
`).join("");
|
||||
menu.querySelectorAll("input").forEach(input => {
|
||||
@ -610,22 +638,10 @@ function setupCelebrationsSelect(initial) {
|
||||
selected = selected.filter(id => id !== input.value);
|
||||
}
|
||||
updateLabel();
|
||||
chrome.storage.sync.set({ "todo_celebrations": selected });
|
||||
if (onChange) onChange(selected.slice());
|
||||
});
|
||||
});
|
||||
};
|
||||
const updateLabel = () => {
|
||||
if (selected.length === 0) label.textContent = getMsg("celebration_none") || "None";
|
||||
else if (selected.length === 1) label.textContent = CELEBRATION_TYPES.find(t => t.id === selected[0]) ? (getMsg("celebration_" + selected[0]) || CELEBRATION_TYPES.find(t => t.id === selected[0]).label) : (getMsg("celebration_none") || "None");
|
||||
else label.textContent = replaceCountPlaceholder((getMsg("selected_count") || "{{COUNT}} selected"), selected.length);
|
||||
};
|
||||
|
||||
// Legacy key: the special handler only passes the new one.
|
||||
chrome.storage.sync.get("todo_confetti", legacy => {
|
||||
selected = resolveCelebrations(initial, legacy && legacy["todo_confetti"]);
|
||||
renderMenu();
|
||||
updateLabel();
|
||||
});
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
@ -645,6 +661,44 @@ function setupCelebrationsSelect(initial) {
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// Replacing the option list keeps any still-valid selection.
|
||||
setItems(list) {
|
||||
items = Array.isArray(list) ? list : [];
|
||||
const ids = new Set(items.map(i => i.id));
|
||||
selected = selected.filter(id => ids.has(id));
|
||||
renderMenu();
|
||||
updateLabel();
|
||||
},
|
||||
setSelected(ids) {
|
||||
const valid = new Set(items.map(i => i.id));
|
||||
selected = (Array.isArray(ids) ? ids : []).map(String).filter(id => !valid.size || valid.has(id));
|
||||
renderMenu();
|
||||
updateLabel();
|
||||
},
|
||||
getSelected() { return selected.slice(); },
|
||||
close() {
|
||||
wrap.classList.remove("open");
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setupCelebrationsSelect(initial) {
|
||||
const control = setupPopupMultiSelect({
|
||||
wrapId: "todo_celebrations",
|
||||
btnId: "todo_celebrations_btn",
|
||||
menuId: "todo_celebrations_menu",
|
||||
labelId: "todo_celebrations_label",
|
||||
onChange: (selected) => chrome.storage.sync.set({ "todo_celebrations": selected }),
|
||||
});
|
||||
if (!control) return;
|
||||
control.setItems(CELEBRATION_TYPES.map(t => ({ id: t.id, label: (getMsg("celebration_" + t.id) || t.label) })));
|
||||
// Legacy key: the special handler only passes the new one.
|
||||
chrome.storage.sync.get("todo_confetti", legacy => {
|
||||
control.setSelected(resolveCelebrations(initial, legacy && legacy["todo_confetti"]));
|
||||
});
|
||||
}
|
||||
|
||||
function setupCelebrationAmount(initial) {
|
||||
@ -1442,6 +1496,47 @@ function setupWidgetHeightInput(id, initial, legacyKey) {
|
||||
});
|
||||
}
|
||||
|
||||
// Heatmap course filter: ticked courses are left out of the widget completely.
|
||||
function setupHeatmapExcludedCourses(initial) {
|
||||
const control = setupPopupMultiSelect({
|
||||
wrapId: "heatmap_excluded",
|
||||
btnId: "heatmap_excluded_btn",
|
||||
menuId: "heatmap_excluded_menu",
|
||||
labelId: "heatmap_excluded_label",
|
||||
emptyText: getMsg("no_course_cards", "Visit your Canvas dashboard to load courses."),
|
||||
onChange: (selected) => chrome.storage.sync.set({ "heatmap_excluded_courses": selected }),
|
||||
});
|
||||
if (!control) return;
|
||||
|
||||
// Only the courses still on the dashboard, labelled the way the user knows them
|
||||
// (custom name, then the Canvas nickname) — same list as the Cards tab.
|
||||
const renderList = (cards) => {
|
||||
const entries = Object.entries(cards || {})
|
||||
.filter(([, card]) => card && card.hidden !== true && card.hide !== true)
|
||||
.map(([id, card]) => ({
|
||||
id: String(id),
|
||||
label: card.name || card.fullName || card.default || card.code || `Course ${id}`,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
control.setItems(entries);
|
||||
};
|
||||
|
||||
chrome.storage.sync.get("custom_cards", sync => {
|
||||
renderList(sync.custom_cards);
|
||||
// The stored ids only survive setItems if their course is still listed.
|
||||
control.setSelected(Array.isArray(initial) ? initial : []);
|
||||
});
|
||||
|
||||
// A dashboard tab writes the card list; keep the dropdown in sync when it lands.
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== "sync" || !changes["custom_cards"]) return;
|
||||
if (!document.getElementById("heatmap_excluded")) return;
|
||||
const selected = control.getSelected();
|
||||
renderList(changes["custom_cards"].newValue);
|
||||
control.setSelected(selected);
|
||||
});
|
||||
}
|
||||
|
||||
// h:mm:ss fields: click to edit a segment, a second digit auto-advances, and
|
||||
// the value is saved zero-padded on blur. The hours field is optional.
|
||||
function setupWidgetTimeInput(prefix, initial) {
|
||||
@ -2103,6 +2198,10 @@ async function setup() {
|
||||
identifier: "heatmap_height",
|
||||
setup: (initial) => setupWidgetHeightInput("heatmap_height", initial),
|
||||
},
|
||||
{
|
||||
identifier: "heatmap_excluded_courses",
|
||||
setup: (initial) => setupHeatmapExcludedCourses(initial),
|
||||
},
|
||||
{
|
||||
identifier: "quick_links_font_size",
|
||||
setup: (initial) => setupRangeSlider("quick_links_font_size", "quickLinksFontSizeSlider", "quickLinksFontSizeValue", null, 12, 24, "px", initial, 8),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user