Better multi-canvas, but failed to make split settings

This commit is contained in:
Guy Sandler 2026-09-29 20:46:26 -07:00
parent 902eb5b128
commit a4a4db7fb9
7 changed files with 257 additions and 63 deletions

View File

@ -24,7 +24,19 @@
"message": "Dark Mode" "message": "Dark Mode"
}, },
"enter_url": { "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": { "card_grades": {
"message": "Card Grades" "message": "Card Grades"

View File

@ -158,6 +158,20 @@
.canvasrefined-todo-item-header {max-height:80px;overflow:hidden;} .canvasrefined-todo-item-header {max-height:80px;overflow:hidden;}
.canvasrefined-todo-svg {fill:#2d3b45!important} .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;} #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-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-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;} #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;}

View File

@ -34,6 +34,15 @@ a.option-container:hover {background: #333}
.options-left {margin-right: 8px;} .options-left {margin-right: 8px;}
.options-left, .options-right {width: 50%;} .options-left, .options-right {width: 50%;}
.customDomain {font-weight:600} .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%} .changer {margin:0;margin-bottom: 0px;width:100%}
.option-background {color: #fff} .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;} .color-changer { font-family: inherit;font-size: 20px;background: none;border: none;color: #000;height: 30px;width:100%;padding:10px 0px 10px 10px;}

View File

@ -440,9 +440,15 @@
</div> </div>
<div class="option-container"> <div class="option-container">
<div class="customDomain"> <div class="customDomain">
<span data-i18n="enter_url">Your Canvas URL:</span> <span data-i18n="enter_url">Your Canvas URLs:</span>
<p style="font-size: 11px;color:#8d8d8d" data-i18n="enter_multiple">(Enter multiple with a comma)</p> <div id="custom-domain-list"></div>
<input type="text" id="customDomain" placeholder="Or just refresh your Canvas page!"></input> <button id="add-domain-btn" class="customization-button" data-i18n="add_canvas">+ Add canvas</button>
<div class="domain-toggles">
<div class="sub-option">
<input type="checkbox" id="auto_detect_disabled" name="auto_detect_disabled">
<label for="auto_detect_disabled" class="sub-text" data-i18n="no_auto_detect">Don't auto detect canvases</label>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -32,6 +32,8 @@ chrome.runtime.onInstalled.addListener(function () {
"auto_dark_end": { "hour": "08", "minute": "00" }, "auto_dark_end": { "hour": "08", "minute": "00" },
"num_assignments": 4, "num_assignments": 4,
"custom_domain": [""], "custom_domain": [""],
"custom_domain_denied": [],
"auto_detect_disabled": false,
"assignments_done": [], "assignments_done": [],
"dashboard_grades": true, "dashboard_grades": true,
"assignment_date_format": false, "assignment_date_format": false,

View File

@ -757,20 +757,48 @@ 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(); isDomainCanvasPage();
function 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; options = result;
if (result.custom_domain.length && result.custom_domain[0] !== "") { const domains = (result.custom_domain || []).filter(d => d && d !== "");
for (let i = 0; i < result.custom_domain.length; i++) { const denied = result.custom_domain_denied || [];
if (domain.includes(result.custom_domain[i])) { if (domains.some(d => domain.includes(d))) {
startExtension(); startExtension();
return; 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;
} }
// if the code reaches this point, its not a canvas page so run the reminders 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); setTimeout(reminderWatch, 2000);
setInterval(reminderWatch, 60000); setInterval(reminderWatch, 60000);
// turn the reminders on/off if the option is changed // turn the reminders on/off if the option is changed
@ -779,9 +807,81 @@ function isDomainCanvasPage() {
if (key === "remind") reminderWatch(); if (key === "remind") reminderWatch();
}) })
}) })
} else {
setupCustomURL();
} }
// 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() { function getGrades() {
if (options.gpa_calc === true || options.dashboard_grades === true) { 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`); 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`);

View File

@ -29,6 +29,7 @@ const syncedSubOptions = [
"todo_show_scores", "todo_show_scores",
"grade_hover", "grade_hover",
"card_letter", "card_letter",
"auto_detect_disabled",
// "hide_completed", // "hide_completed",
"num_todo_items", "num_todo_items",
"hover_preview", "hover_preview",
@ -111,6 +112,8 @@ const defaultOptions = {
"auto_dark_end": { "hour": "08", "minute": "00" }, "auto_dark_end": { "hour": "08", "minute": "00" },
"num_assignments": 4, "num_assignments": 4,
"custom_domain": [""], "custom_domain": [""],
"custom_domain_denied": [],
"auto_detect_disabled": false,
"assignments_done": [], "assignments_done": [],
"dashboard_grades": false, "dashboard_grades": false,
"assignment_date_format": false, "assignment_date_format": false,
@ -876,8 +879,8 @@ function setupFeatureSearch(menu) {
add({ key: "sub:" + keyIdOf(sub), text, el: sub, action: () => goToMainElement(sub) }); add({ key: "sub:" + keyIdOf(sub), text, el: sub, action: () => goToMainElement(sub) });
}); });
// Home: custom Canvas URL // Home: custom Canvas URLs
const customDomain = document.querySelector("#customDomain"); const customDomain = document.querySelector(".customDomain");
if (customDomain && !shouldSkip(customDomain)) { if (customDomain && !shouldSkip(customDomain)) {
const wrap = customDomain.closest(".customDomain"); const wrap = customDomain.closest(".customDomain");
const label = wrap ? wrap.querySelector("[data-i18n='enter_url']") : null; const label = wrap ? wrap.querySelector("[data-i18n='enter_url']") : null;
@ -1094,6 +1097,7 @@ function setup() {
"todo_show_scores", "todo_show_scores",
"grade_hover", "grade_hover",
"card_letter", "card_letter",
"auto_detect_disabled",
// "hide_completed", // "hide_completed",
"hover_preview", "hover_preview",
"customBackgroundDaily", "customBackgroundDaily",
@ -1491,28 +1495,94 @@ function setup() {
} }
updateStorageUsage(); updateStorageUsage();
// activate custom url input // Canvas URL list — each school gets its own text box
document.querySelector('#customDomain').addEventListener('input', function () { const domainListEl = document.querySelector("#custom-domain-list");
let domains = this.value.split(","); const addDomainBtn = document.querySelector("#add-domain-btn");
domains.forEach((domain, index) => {
let val = domain.replace(" ", ""); // Normalize a single entry: a full URL becomes its hostname, anything else
if (val === "") return; // is kept as typed. Idempotent — rows store bare hostnames
//if (!val.includes("https://") && !val.includes("http://")) val = "https://" + val; // ("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 { try {
let url = new URL(val); const host = new URL(trimmed).hostname;
domains[index] = url.hostname; if (host) {
clearAlert(); clearAlert();
} catch (e) { return host;
domains[index] = val; }
displayAlert(true, "The URL you entered appears to be invalid, so it might not work."); } 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 addDomainBtn.addEventListener("click", () => {
chrome.storage.sync.get(["custom_domain"], storage => { domainListEl.appendChild(makeDomainRow(""));
document.querySelector("#customDomain").value = storage.custom_domain ? storage.custom_domain : ""; const rows = domainListEl.querySelectorAll(".domain-row input");
rows[rows.length - 1].focus();
}); });
// activate import input box // activate import input box