From a4a4db7fb9dbfde83913bf76e5dc8a33a0b97469 Mon Sep 17 00:00:00 2001 From: Guy Sandler Date: Tue, 29 Sep 2026 20:46:26 -0700 Subject: [PATCH] Better multi-canvas, but failed to make split settings --- _locales/en/messages.json | 16 +++- css/content.css | 14 ++++ css/popup.css | 9 +++ html/popup.html | 12 ++- js/background.js | 2 + js/content.js | 159 ++++++++++++++++++++++++++++---------- js/popup.js | 108 +++++++++++++++++++++----- 7 files changed, 257 insertions(+), 63 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b8239fe..99c03d3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -24,7 +24,19 @@ "message": "Dark Mode" }, "enter_url": { - "message": "Your Canvas url:" + "message": "Your Canvas URLs:" + }, + "add_canvas": { + "message": "+ Add canvas" + }, + "no_auto_detect": { + "message": "Don't auto detect canvases" + }, + "add_to_list": { + "message": "Add to list" + }, + "no_thanks": { + "message": "No thanks" }, "card_grades": { "message": "Card Grades" @@ -326,4 +338,4 @@ "grade_analytics_zones": { "message": "Colored grade zones" } -} \ No newline at end of file +} diff --git a/css/content.css b/css/content.css index 7abc507..9e43bdd 100644 --- a/css/content.css +++ b/css/content.css @@ -158,6 +158,20 @@ .canvasrefined-todo-item-header {max-height:80px;overflow:hidden;} .canvasrefined-todo-svg {fill:#2d3b45!important} #canvasrefined-update-msg {margin-top: 5px; display: flex; justify-content: space-between; font-size:15px;background: linear-gradient(115deg, rgb(255, 53, 88, .9), rgb(255, 131, 73, .9), rgb(255, 103, 187, .9)); color: #fff; border-radius:4px; padding: 20px;white-space: pre-line;} + +/* Detected-canvas prompt — centered modal with backdrop so it can't be missed */ +#canvasrefined-domain-prompt-backdrop {position: fixed!important;inset: 0!important;width: 100%!important;height: 100%!important;background: rgb(0 0 0 / 60%)!important;z-index: 2147483647!important;display: flex!important;align-items: center!important;justify-content: center!important;} +#canvasrefined-domain-prompt {background: #fff!important;border-radius: 14px!important;padding: 24px 28px!important;max-width: 460px!important;width: 90%!important;box-sizing: border-box!important;box-shadow: 0 12px 48px rgb(0 0 0 / 45%)!important;animation: .25s ease canvasrefined-pop;} +@keyframes canvasrefined-pop {0% {transform: scale(.9);opacity: 0} 100% {transform: scale(1);opacity: 1}} +#canvasrefined-domain-prompt h3 {margin: 0 0 10px!important;font-size: 19px!important;font-weight: 700!important;color: #2d3b45!important;font-family: inherit!important;} +#canvasrefined-domain-prompt p {margin: 0 0 18px!important;font-size: 14px!important;color: #4a5460!important;line-height: 1.5!important;font-family: inherit!important;} +.canvasrefined-domain-host {font-weight: 700!important;color: #2d3b45!important;word-break: break-all!important;} +.canvasrefined-domain-prompt-actions {display: flex!important;gap: 10px!important;} +.canvasrefined-domain-prompt-actions button {flex: 1!important;border: none!important;border-radius: 8px!important;padding: 11px 16px!important;font-size: 14px!important;font-weight: 600!important;cursor: pointer!important;font-family: inherit!important;} +.canvasrefined-domain-yes {background: #2d3b45!important;color: #fff!important;} +.canvasrefined-domain-yes:hover {background: #3b4d61!important;} +.canvasrefined-domain-no {background: #eceef0!important;color: #2d3b45!important;} +.canvasrefined-domain-no:hover {background: #dfe2e5!important;} #canvasrefined-update-msg p {margin: 0;color:#fff!important;} #canvasrefined-update-close { background: #2f2f2f59; padding: 3px 6px; border-radius: 3px; height:100%; border:none; color: #fff!important;} #canvasrefined-reminders {animation: .4s ease slide; margin:20px!important;position: fixed!important; bottom: 0!important;right: 0;z-index:10000000000!important;flex-direction: column!important; gap:10px!important;} diff --git a/css/popup.css b/css/popup.css index de94fb4..952a86d 100644 --- a/css/popup.css +++ b/css/popup.css @@ -34,6 +34,15 @@ a.option-container:hover {background: #333} .options-left {margin-right: 8px;} .options-left, .options-right {width: 50%;} .customDomain {font-weight:600} + +/* Canvas URL list — one text box per school */ +#custom-domain-list {display: flex;flex-direction: column;gap: 6px;margin-top: 8px} +.domain-row {display: flex;gap: 6px;align-items: center} +.domain-row input {flex: 1;min-width: 0;box-sizing: border-box;font-size: 12px;background: var(--inputbg);border: none;padding: 8px;color: #e2e2e2;border-radius: 7px;font-family: inherit} +.domain-row .domain-remove {flex: none;width: 24px;height: 31px;padding: 0;border: none;border-radius: 6px;background: #2f2f2f;color: #a7a7a7;font-size: 13px;line-height: 1;cursor: pointer} +.domain-row .domain-remove:hover {background: #3d3d3d;color: #fff} +#add-domain-btn {margin-top: 8px;font-size: 12px;padding: 5px 12px} +.domain-toggles {margin-top: 10px;display: flex;flex-direction: column} .changer {margin:0;margin-bottom: 0px;width:100%} .option-background {color: #fff} .color-changer { font-family: inherit;font-size: 20px;background: none;border: none;color: #000;height: 30px;width:100%;padding:10px 0px 10px 10px;} diff --git a/html/popup.html b/html/popup.html index 78ba84f..b83a6eb 100644 --- a/html/popup.html +++ b/html/popup.html @@ -440,9 +440,15 @@
- Your Canvas URL: -

(Enter multiple with a comma)

- + Your Canvas URLs: +
+ +
+
+ + +
+
diff --git a/js/background.js b/js/background.js index 6e86982..9af703a 100644 --- a/js/background.js +++ b/js/background.js @@ -32,6 +32,8 @@ chrome.runtime.onInstalled.addListener(function () { "auto_dark_end": { "hour": "08", "minute": "00" }, "num_assignments": 4, "custom_domain": [""], + "custom_domain_denied": [], + "auto_detect_disabled": false, "assignments_done": [], "dashboard_grades": true, "assignment_date_format": false, diff --git a/js/content.js b/js/content.js index 433cb90..f46196a 100644 --- a/js/content.js +++ b/js/content.js @@ -757,31 +757,131 @@ function showExampleReminder() { } +// Canvas detection: if the domain is in the user's list, start normally. +// Otherwise probe it (unless detection was declined for it); an empty list +// auto-adds the first canvas, and with canvases already in the list the user +// is asked once per domain. isDomainCanvasPage(); function isDomainCanvasPage() { - chrome.storage.sync.get(['custom_domain', 'dark_mode', 'dark_preset', 'device_dark', 'remind'], result => { + chrome.storage.sync.get(['custom_domain', 'custom_domain_denied', 'auto_detect_disabled', 'dark_mode', 'dark_preset', 'device_dark', 'remind'], result => { options = result; - if (result.custom_domain.length && result.custom_domain[0] !== "") { - for (let i = 0; i < result.custom_domain.length; i++) { - if (domain.includes(result.custom_domain[i])) { - startExtension(); - return; - } - } - - // if the code reaches this point, its not a canvas page so run the reminders - setTimeout(reminderWatch, 2000); - setInterval(reminderWatch, 60000); - // turn the reminders on/off if the option is changed - chrome.storage.onChanged.addListener((changes) => { - Object.keys(changes).forEach(key => { - if (key === "remind") reminderWatch(); - }) - }) - } else { - setupCustomURL(); + const domains = (result.custom_domain || []).filter(d => d && d !== ""); + const denied = result.custom_domain_denied || []; + if (domains.some(d => domain.includes(d))) { + startExtension(); + return; } + + // not in the list — no prompting/probing if the user turned auto detect + // off, or already said no here; just run reminders + if (result.auto_detect_disabled === true || denied.some(d => domain.includes(d))) { + startReminderMode(); + return; + } + + detectCanvasPage().then(({ isCanvas, courses }) => { + if (!isCanvas) { + startReminderMode(); + return; + } + if (domains.length === 0) { + // first canvas ever — add it automatically (original first-run + // behavior) and reload so the extension starts + addCanvasDomain(courses).then(() => location.reload()); + } else { + // at least one canvas already in the list: ask the first time + // this page is seen. Saying no means we never ask again here. + promptAddCanvasDomain(courses); + } + }); + }); +} + +function startReminderMode() { + setTimeout(reminderWatch, 2000); + setInterval(reminderWatch, 60000); + // turn the reminders on/off if the option is changed + chrome.storage.onChanged.addListener((changes) => { + Object.keys(changes).forEach(key => { + if (key === "remind") reminderWatch(); + }) + }) +} + +// The courses API only answers on real Canvas installs. Resolves { isCanvas, courses }. +async function detectCanvasPage() { + try { + const courses = await getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}per_page=100`); + if (Array.isArray(courses) && courses.length) { + return { isCanvas: true, courses }; + } + console.log("Canvas Refined - this url doesn't seem to be a canvas url (1)"); + return { isCanvas: false, courses: null }; + } catch (err) { + console.log("Canvas Refined - this url doesn't seem to be a canvas url (2)"); + return { isCanvas: false, courses: null }; + } +} + +// Register the domain in the canvas list. `courses` (from detectCanvasPage) is +// passed to getCards so this school's courses are indexed before the reload, +// exactly like the original first-run flow did. +function addCanvasDomain(courses) { + return getCards(courses).then(() => new Promise(resolve => { + setTimeout(() => { + console.log("Canvas Refined - setting custom domain to " + domain); + chrome.storage.sync.get(["custom_domain"], storage => { + const domains = (storage.custom_domain || []).filter(d => d && d !== ""); + domains.push(domain); + // the domain was just accepted — make sure it isn't on the + // "don't ask again" list + chrome.storage.sync.get(["custom_domain_denied"], deniedStorage => { + const denied = (deniedStorage.custom_domain_denied || []).filter(d => !domain.includes(d)); + chrome.storage.sync.set({ custom_domain: domains, custom_domain_denied: denied }).then(resolve); + }); + }); + }, 100); + })); +} + +function promptAddCanvasDomain(courses) { + // content scripts run at document_start, so the body may not exist yet + const show = () => showCanvasPrompt(courses); + if (document.body) show(); + else document.addEventListener("DOMContentLoaded", show, { once: true }); +} + +function showCanvasPrompt(courses) { + if (document.getElementById("canvasrefined-domain-prompt-backdrop")) return; + + const host = domain.replace(/^https?:\/\//, ""); + // modal with a backdrop so the ask is impossible to miss + const backdrop = makeElement("div", document.body, { "id": "canvasrefined-domain-prompt-backdrop" }); + const prompt = makeElement("div", backdrop, { "id": "canvasrefined-domain-prompt" }); + makeElement("h3", prompt, { "textContent": "New Canvas detected" }); + const text = makeElement("p", prompt, {}); + text.append("Canvas Refined detected a Canvas page at "); + makeElement("span", text, { "className": "canvasrefined-domain-host", "textContent": host }); + text.append(". Add it to your canvas list?"); + const actions = makeElement("div", prompt, { "className": "canvasrefined-domain-prompt-actions" }); + const yes = makeElement("button", actions, { "textContent": chrome.i18n.getMessage("add_to_list") || "Add to list", "className": "canvasrefined-domain-yes" }); + const no = makeElement("button", actions, { "textContent": chrome.i18n.getMessage("no_thanks") || "No thanks", "className": "canvasrefined-domain-no" }); + + const dismiss = () => backdrop.remove(); + + yes.addEventListener("click", () => { + dismiss(); + addCanvasDomain(courses).then(() => location.reload()); + }); + no.addEventListener("click", () => { + dismiss(); + chrome.storage.sync.get(["custom_domain_denied"], storage => { + const denied = storage.custom_domain_denied || []; + if (!denied.some(d => domain.includes(d))) denied.push(domain); + chrome.storage.sync.set({ custom_domain_denied: denied }); + }); + startReminderMode(); }); } @@ -7634,25 +7734,6 @@ function cleanCustomAssignments() { }); } -function setupCustomURL() { - //let test = getData(`${domain}/api/v1/dashboard/dashboard_cards?include[]=concluded&include[]=term`); - let test = getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}per_page=100`); - test.then(res => { - if (res.length) { - getCards(res).then(() => { - setTimeout(() => { - console.log("Canvas Refined - setting custom domain to " + domain); - chrome.storage.sync.set({ custom_domain: [domain] }).then(location.reload()); - }, 100); - }); - } else { - console.log("Canvas Refined - this url doesn't seem to be a canvas url (1)"); - } - }).catch(err => { - console.log("Canvas Refined - this url doesn't seem to be a canvas url (2)"); - }); -} - function getGrades() { if (options.gpa_calc === true || options.dashboard_grades === true) { grades = getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}include[]=concluded&include[]=total_scores&include[]=computed_current_score&include[]=current_grading_period_scores&per_page=100`); diff --git a/js/popup.js b/js/popup.js index 7cbe587..9ee1415 100644 --- a/js/popup.js +++ b/js/popup.js @@ -29,6 +29,7 @@ const syncedSubOptions = [ "todo_show_scores", "grade_hover", "card_letter", + "auto_detect_disabled", // "hide_completed", "num_todo_items", "hover_preview", @@ -111,6 +112,8 @@ const defaultOptions = { "auto_dark_end": { "hour": "08", "minute": "00" }, "num_assignments": 4, "custom_domain": [""], + "custom_domain_denied": [], + "auto_detect_disabled": false, "assignments_done": [], "dashboard_grades": false, "assignment_date_format": false, @@ -876,8 +879,8 @@ function setupFeatureSearch(menu) { add({ key: "sub:" + keyIdOf(sub), text, el: sub, action: () => goToMainElement(sub) }); }); - // Home: custom Canvas URL - const customDomain = document.querySelector("#customDomain"); + // Home: custom Canvas URLs + const customDomain = document.querySelector(".customDomain"); if (customDomain && !shouldSkip(customDomain)) { const wrap = customDomain.closest(".customDomain"); const label = wrap ? wrap.querySelector("[data-i18n='enter_url']") : null; @@ -1094,6 +1097,7 @@ function setup() { "todo_show_scores", "grade_hover", "card_letter", + "auto_detect_disabled", // "hide_completed", "hover_preview", "customBackgroundDaily", @@ -1491,28 +1495,94 @@ function setup() { } updateStorageUsage(); - // activate custom url input - document.querySelector('#customDomain').addEventListener('input', function () { - let domains = this.value.split(","); - domains.forEach((domain, index) => { - let val = domain.replace(" ", ""); - if (val === "") return; - //if (!val.includes("https://") && !val.includes("http://")) val = "https://" + val; - try { - let url = new URL(val); - domains[index] = url.hostname; + // Canvas URL list — each school gets its own text box + const domainListEl = document.querySelector("#custom-domain-list"); + const addDomainBtn = document.querySelector("#add-domain-btn"); + + // Normalize a single entry: a full URL becomes its hostname, anything else + // is kept as typed. Idempotent — rows store bare hostnames + // ("canvas.school.edu"), which fail new URL() without a scheme, so they're + // validated as hostnames instead of re-flagged as invalid on every save. + function sanitizeDomainEntry(val, showAlert = true) { + let trimmed = val.trim().replace(/ /g, ""); + if (trimmed === "") return ""; + try { + const host = new URL(trimmed).hostname; + if (host) { clearAlert(); - } catch (e) { - domains[index] = val; - displayAlert(true, "The URL you entered appears to be invalid, so it might not work."); + return host; + } + } catch (e) { /* not a full URL — may still be a bare hostname */ } + // strip scheme/path if new URL couldn't parse it, then validate as a hostname + const host = trimmed.replace(/^https?:\/\//i, "").replace(/\/.*$/, ""); + if (host && /^[a-z0-9][a-z0-9.-]*$/i.test(host)) { + clearAlert(); + return host; + } + if (showAlert) displayAlert(true, "The URL you entered appears to be invalid, so it might not work."); + return trimmed; + } + + function saveDomainList() { + const domains = []; + domainListEl.querySelectorAll(".domain-row input").forEach(input => { + const val = sanitizeDomainEntry(input.value); + if (val !== "") domains.push(val); + }); + // keep the legacy [""]-when-empty shape the rest of the code expects + chrome.storage.sync.set({ custom_domain: domains.length ? domains : [""] }); + } + + function makeDomainRow(value) { + const row = document.createElement("div"); + row.className = "domain-row"; + const input = document.createElement("input"); + input.type = "text"; + input.placeholder = "canvas.myschool.edu"; + input.value = value || ""; + input.spellcheck = false; + input.addEventListener("input", saveDomainList); + input.addEventListener("blur", () => { + // normalize what's shown and drop rows left empty + if (input.value.trim() === "") { + if (domainListEl.querySelectorAll(".domain-row").length > 1) row.remove(); + } else { + input.value = sanitizeDomainEntry(input.value); + } + saveDomainList(); + }); + const remove = document.createElement("button"); + remove.className = "domain-remove"; + remove.type = "button"; + remove.textContent = "\u2715"; + remove.title = "Remove"; + remove.addEventListener("click", () => { + const val = sanitizeDomainEntry(input.value, false); + row.remove(); + saveDomainList(); + // clear any past "no thanks" for this canvas so the detection + // prompt can ask again if it's visited later + if (val) { + chrome.storage.sync.get(["custom_domain_denied"], storage => { + const denied = (storage.custom_domain_denied || []).filter(d => d !== val); + chrome.storage.sync.set({ custom_domain_denied: denied }); + }); } }); - chrome.storage.sync.set({ custom_domain: domains }); + row.appendChild(input); + row.appendChild(remove); + return row; + } + + chrome.storage.sync.get(["custom_domain"], storage => { + const domains = (storage.custom_domain || []).filter(d => d && d.trim() !== ""); + domains.forEach(d => domainListEl.appendChild(makeDomainRow(d))); }); - // setup custom url - chrome.storage.sync.get(["custom_domain"], storage => { - document.querySelector("#customDomain").value = storage.custom_domain ? storage.custom_domain : ""; + addDomainBtn.addEventListener("click", () => { + domainListEl.appendChild(makeDomainRow("")); + const rows = domainListEl.querySelectorAll(".domain-row input"); + rows[rows.length - 1].focus(); }); // activate import input box