diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 80ea274..07f1099 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -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" diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0218944..effa7cc 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" }, diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 68fedba..32c2ab7 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -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" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 21bfdc3..da79f23 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -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" diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 4d2b2f0..d12eae1 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -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" diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 886d26a..6cdab9b 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -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}} 完了" diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index 737b2ba..d53dcfb 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -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" diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index f3944f8..a525ec3 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -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}} выполнено" diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 7db89cb..92045c6 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -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" diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index d805ba5..49c5d60 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -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}} 已完成" diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index bcac043..7750b26 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -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}}" diff --git a/css/popup.css b/css/popup.css index 0f26ae6..c5e808f 100644 --- a/css/popup.css +++ b/css/popup.css @@ -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, diff --git a/html/popup.html b/html/popup.html index 41b730e..e303d15 100644 --- a/html/popup.html +++ b/html/popup.html @@ -349,12 +349,12 @@
Celebration type -
- -
+
@@ -1417,6 +1417,17 @@ px
+ +
+ Exclude courses +
+ +
+
+
@@ -1571,7 +1582,7 @@

Email is still available at sandlerguy5@gmail.com for private matters.

-

Privacy

+

Dev tools

diff --git a/js/background.js b/js/background.js index 8614cb3..806c09c 100644 --- a/js/background.js +++ b/js/background.js @@ -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, diff --git a/js/content.js b/js/content.js index 56a2c98..6a25495 100644 --- a/js/content.js +++ b/js/content.js @@ -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) { diff --git a/js/popup.js b/js/popup.js index a30bf61..416f60d 100644 --- a/js/popup.js +++ b/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 => ` -