mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-22 04:54:53 +02:00
Compare commits
2 Commits
765ecbf2fa
...
759deeed75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
759deeed75 | ||
|
|
8b9a1ded43 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
*.zip
|
||||
.vscode
|
||||
.vscode
|
||||
.pi
|
||||
@ -56,6 +56,12 @@
|
||||
"better_sidebar": {
|
||||
"message": "Better Sidebar"
|
||||
},
|
||||
"quiz_safe_mode": {
|
||||
"message": "Quiz Safe Mode"
|
||||
},
|
||||
"quiz_safe_mode_desc": {
|
||||
"message": "Turns off most Canvas Refined features on quiz pages so you get the default Canvas quiz experience."
|
||||
},
|
||||
"opacity_options": {
|
||||
"message": "Opacity & Blur"
|
||||
},
|
||||
|
||||
@ -217,4 +217,52 @@
|
||||
.canvasrefined-notes-editor::placeholder {color:var(--bctext-2,#9aa3ab);}
|
||||
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-toolbar {display:none;}
|
||||
.canvasrefined-dashboard-notes.is-editing .canvasrefined-notes-rendered {display:none;}
|
||||
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-editor {display:none;}
|
||||
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-editor {display:none;}
|
||||
/* Quiz safe mode banner (pre-quiz pages) */
|
||||
.canvasrefined-quiz-safe-banner {
|
||||
box-sizing: border-box;
|
||||
margin: 16px 0;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #d4a017;
|
||||
border-left: 4px solid #d4a017;
|
||||
border-radius: 6px;
|
||||
background: #fff8e1;
|
||||
color: #333;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
max-width: 760px;
|
||||
}
|
||||
.canvasrefined-quiz-safe-title {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
color: #7a5a00;
|
||||
}
|
||||
.canvasrefined-quiz-safe-info { margin: 0 0 10px; }
|
||||
.canvasrefined-quiz-safe-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.canvasrefined-quiz-safe-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.canvasrefined-quiz-safe-toggle input { width: 16px; height: 16px; cursor: pointer; }
|
||||
.canvasrefined-quiz-safe-toggle-label { font-weight: 600; }
|
||||
.canvasrefined-quiz-safe-dismiss {
|
||||
margin-left: auto;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #c7cdd1;
|
||||
border-radius: 6px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #000 !important;
|
||||
}
|
||||
.canvasrefined-quiz-safe-dismiss:hover { background: #ececec; }
|
||||
|
||||
@ -287,4 +287,44 @@ input[type="checkbox"]:checked {background: #8dd28d;}
|
||||
.search-result.empty { text-align: center; color: #8d8d8d; font-size: 12px; cursor: default; padding: 12px; }
|
||||
.search-result.empty:hover { background: none; }
|
||||
.search-highlight { outline: 2px solid #56Caf0; outline-offset: 2px; border-radius: 8px; animation: search-pulse 1.8s ease-out; }
|
||||
@keyframes search-pulse { 0% { box-shadow: 0 0 0 0 #56Caf099; } 40% { box-shadow: 0 0 0 6px #56Caf000; } 100% { box-shadow: 0 0 0 0 #56Caf000; } }
|
||||
@keyframes search-pulse { 0% { box-shadow: 0 0 0 0 #56Caf099; } 40% { box-shadow: 0 0 0 6px #56Caf000; } 100% { box-shadow: 0 0 0 0 #56Caf000; } }
|
||||
/* ===== Quiz Safe Mode info bubble ===== */
|
||||
.cr-info-bubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #8d8d8d;
|
||||
cursor: help;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cr-info-bubble:hover,
|
||||
.cr-info-bubble:focus { color: #56Caf0; }
|
||||
.cr-info-bubble-text {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: max-content;
|
||||
max-width: 220px;
|
||||
background: #0e0e0ef0;
|
||||
color: #e2e2e2;
|
||||
border: 1px solid #ffffff1a;
|
||||
border-radius: 8px;
|
||||
padding: 8px 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.45);
|
||||
transition: opacity .15s ease, visibility .15s ease;
|
||||
z-index: 1000;
|
||||
text-align: left;
|
||||
}
|
||||
.cr-info-bubble:hover .cr-info-bubble-text,
|
||||
.cr-info-bubble:focus .cr-info-bubble-text { visibility: visible; opacity: 1; }
|
||||
|
||||
@ -190,6 +190,20 @@
|
||||
<span class="option-name" data-i18n="todo_remind">Todo Reminders</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="quiz_safe_mode">
|
||||
<input type="radio" id="off" name="quiz_safe_mode">
|
||||
<input type="radio" id="on" name="quiz_safe_mode">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div><span class="option-name" data-i18n="quiz_safe_mode">Quiz Safe Mode</span>
|
||||
<span class="cr-info-bubble" tabindex="0" role="button" aria-label="Quiz Safe Mode info">
|
||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>
|
||||
<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>
|
||||
<div class="social-buttons">
|
||||
<a class="discord-button" href="https://discord.gg/GjFh4JA3wh" target="_blank" rel="noopener noreferrer" title="Join our Discord">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.369a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.331c-1.182 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
|
||||
@ -76,6 +76,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
"equal_height_cards": false,
|
||||
"hide_feedback": false,
|
||||
"hide_new_canvas": true,
|
||||
"quiz_safe_mode": false,
|
||||
"dark_mode_fix": [],
|
||||
"assignment_states": {},
|
||||
"tab_icons": false,
|
||||
|
||||
269
js/content.js
269
js/content.js
@ -38,6 +38,23 @@ function isProfilePage() {
|
||||
return /^\/profile(?:\/|$)/.test(current_page);
|
||||
}
|
||||
|
||||
// Quiz pages: /courses/123/quizzes/456 (pre-take/intro) and
|
||||
// /courses/123/quizzes/456/take (the actual quiz).
|
||||
function isQuizPage() {
|
||||
return /^\/courses\/\d+\/quizzes\/\d+(?:\/|$)/.test(current_page);
|
||||
}
|
||||
function isQuizTakePage() {
|
||||
return /^\/courses\/\d+\/quizzes\/\d+\/take(?:\/|$)/.test(current_page);
|
||||
}
|
||||
function isQuizPreTakePage() {
|
||||
return isQuizPage() && !isQuizTakePage();
|
||||
}
|
||||
// "Quiz safe mode" disables features that interfere with the default Canvas
|
||||
// quiz experience. Only active on quiz pages when the user has opted in.
|
||||
function quizSafeModeActive() {
|
||||
return isQuizPage() && options.quiz_safe_mode === true;
|
||||
}
|
||||
|
||||
function getSubmissionAssignmentLink() {
|
||||
const match = current_page.match(/^\/courses\/(\d+)\/assignments\/(\d+)\/submissions\/(\d+)(?:\/|$)/);
|
||||
if (!match) return null;
|
||||
@ -558,7 +575,10 @@ function startExtension() {
|
||||
|
||||
toggleDarkMode();
|
||||
|
||||
chrome.storage.sync.get(["better_sidebar", "sidebar_scale"], result => {
|
||||
// Include bg_opacity/bg_blur so setupBetterSidebar (called below) tints the
|
||||
// course-content panel with the user's slider values on first load, instead
|
||||
// of falling back to the defaults until a slider is touched.
|
||||
chrome.storage.sync.get(["better_sidebar", "sidebar_scale", "bg_opacity", "bg_blur"], result => {
|
||||
options = { ...options, ...result };
|
||||
ensureBetterSidebar();
|
||||
});
|
||||
@ -581,6 +601,8 @@ function startExtension() {
|
||||
watchSubmissionPageButton();
|
||||
watchProfileLogoutPageButton();
|
||||
|
||||
setupQuizSafeModeBanner();
|
||||
|
||||
|
||||
setTimeout(() => runDarkModeFixer(false), 800);
|
||||
setTimeout(() => runDarkModeFixer(false), 4500);
|
||||
@ -777,6 +799,11 @@ function applyOptionsChanges(changes) {
|
||||
resetBetterSidebarLayout();
|
||||
}
|
||||
break;
|
||||
case "quiz_safe_mode":
|
||||
// Toggling safe mode changes which features run on quiz pages; reload
|
||||
// so the gating is applied cleanly.
|
||||
if (isQuizPage()) window.location.reload();
|
||||
break;
|
||||
case "sidebar_scale": {
|
||||
const existingSidebar = document.getElementById("better-sidebar-container");
|
||||
if (existingSidebar) {
|
||||
@ -823,12 +850,16 @@ function resetBetterSidebarLayout() {
|
||||
|
||||
function ensureBetterSidebar() {
|
||||
if (!options.better_sidebar) return;
|
||||
// Quiz safe mode: don't replace the Canvas sidebar on quiz pages.
|
||||
if (quizSafeModeActive()) return;
|
||||
if (document.querySelector("#better-sidebar-container")) return;
|
||||
if (!document.querySelector("#wrapper") || !document.querySelector(".ic-Layout-contentWrapper")) return;
|
||||
setupBetterSidebar(getSidebarLayoutMode());
|
||||
}
|
||||
|
||||
async function applyCustomBackground() {
|
||||
// Quiz safe mode: leave the quiz page background untouched.
|
||||
if (quizSafeModeActive()) return;
|
||||
// let style = document.querySelector("#DashboardCard_Container")
|
||||
let style = document.querySelector("#canvasrefined-background") || document.createElement('style');
|
||||
style.id = "canvasrefined-background";
|
||||
@ -1017,6 +1048,23 @@ async function applyCustomBackground() {
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
` : ""}
|
||||
/* Course content pages (sidebar layout "course": /courses/:id/* and
|
||||
/profile): tint the #content.ic-Layout-contentMain panel so the
|
||||
bg_opacity/bg_blur sliders have a surface to control even without Better
|
||||
Sidebar. setupBetterSidebar only adds this panel when Better Sidebar is
|
||||
on; this mirrors its inline values so the panel shows regardless. When
|
||||
Better Sidebar is on, its inline !important overrides these (same values),
|
||||
so this rule is inert in that case. */
|
||||
${getSidebarLayoutMode() === "course" ? `
|
||||
.ic-Layout-contentMain {
|
||||
margin: 26px 38px 38px !important;
|
||||
padding: 10px !important;
|
||||
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important;
|
||||
backdrop-filter: blur(${bgBlur}px) !important;
|
||||
-webkit-backdrop-filter: blur(${bgBlur}px) !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
` : ""}
|
||||
${isConversationsPage() ? `
|
||||
.css-1nh4pc4-view-flexItem {
|
||||
background-color: color-mix(in srgb, var(--bcbackground-0), transparent ${bgTransparent}%) !important;
|
||||
@ -1690,7 +1738,7 @@ function renderProgressRainbow(wrapper, shown, totalAll, completedAll, percent)
|
||||
// sweep-flag 1 => arc bulges UPWARD (top semicircle), drawn left -> right
|
||||
const arcPath = `M ${cx - radius} ${baseY} A ${radius} ${radius} 0 0 1 ${cx + radius} ${baseY}`;
|
||||
const prog = entry.total === 0 ? 0 : entry.completed / entry.total;
|
||||
const color = `hsl(${Math.round((idx * 360) / Math.max(1, ringCount))},70%,55%)`;
|
||||
const color = courseRingColor(entry.courseId, idx);
|
||||
|
||||
// track: full semicircle, faded class color
|
||||
let track = svg.querySelector(`path[data-idx='${idx}'][data-role='bg']`);
|
||||
@ -3194,66 +3242,85 @@ function createConfettiBurst(targetElement, opts = {}) {
|
||||
function markAs(item, element) {
|
||||
const csrfToken = CSRFtoken();
|
||||
const completeState = item.planner_override ? !item.planner_override.marked_complete : true;
|
||||
fetch(domain + "/api/v1/planner/overrides" + (item.planner_override ? "/" + item.planner_override.id : ""), {
|
||||
method: item.planner_override ? "PUT" : "POST",
|
||||
headers: {
|
||||
"content-type":"application/json",
|
||||
"accept":"application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: item.planner_override ? item.planner_override.id : null,
|
||||
marked_complete: completeState,
|
||||
plannable_id: item.plannable_id,
|
||||
plannable_type: item.plannable_type
|
||||
})
|
||||
})
|
||||
.then(resp => {
|
||||
if (resp.status == 200 || resp.status == 201 || resp.status == 204) {
|
||||
console.log("marked as complete");
|
||||
item.planner_override = item.planner_override || {};
|
||||
item.planner_override.marked_complete = completeState;
|
||||
element.style.transform = "translate(100%)";
|
||||
element.style.opacity = "0";
|
||||
|
||||
// fire confetti only when marking complete (not when unmarking)
|
||||
if (completeState) {
|
||||
try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
|
||||
// --- Optimistic UI ---
|
||||
// Canvas's /planner/overrides endpoint occasionally returns 400 (Bad
|
||||
// Request) for both custom tasks and normal tasks, even though the action
|
||||
// is actually applied server-side shortly after. When that happens the
|
||||
// item would neither visually mark nor animate, and would later appear
|
||||
// "secretly complete" on reload. To avoid that confusing UX, we update the
|
||||
// UI as if the request succeeded immediately, and fire the actual API
|
||||
// call in the background for persistence.
|
||||
item.planner_override = item.planner_override || {};
|
||||
item.planner_override.marked_complete = completeState;
|
||||
element.style.transform = "translate(100%)";
|
||||
element.style.opacity = "0";
|
||||
|
||||
// fire confetti only when marking complete (not when unmarking)
|
||||
if (completeState) {
|
||||
try { createConfettiBurst(element); } catch (e) { console.error('confetti trigger error', e); }
|
||||
}
|
||||
|
||||
// update progress rings immediately so they animate while the item slides/fades
|
||||
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
|
||||
if (progressPlaceholder && typeof assignments?.then === 'function' && progressRingsEnabled()) {
|
||||
assignments.then(data => {
|
||||
const courseId = getCurrentCourseId();
|
||||
const scopedData = courseId
|
||||
? data.map(d => Object.assign({}, d)) // shallow copy
|
||||
.filter(d => {
|
||||
const itemCourseId = parseInt(d.course_id || d.context_id || d?.plannable?.course_id);
|
||||
return itemCourseId === courseId;
|
||||
})
|
||||
: data.map(d => Object.assign({}, d));
|
||||
|
||||
// reflect the updated state for this item in the snapshot
|
||||
for (let i = 0; i < scopedData.length; i++) {
|
||||
if (scopedData[i].plannable_id === item.plannable_id && scopedData[i].plannable_type === item.plannable_type) {
|
||||
scopedData[i].planner_override = scopedData[i].planner_override || {};
|
||||
scopedData[i].planner_override.marked_complete = item.planner_override.marked_complete;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update progress rings immediately so they animate while the item slides/fades
|
||||
const progressPlaceholder = document.getElementById("better-todo-progress-placeholder");
|
||||
if (progressPlaceholder && typeof assignments?.then === 'function' && progressRingsEnabled()) {
|
||||
assignments.then(data => {
|
||||
const courseId = getCurrentCourseId();
|
||||
const scopedData = courseId
|
||||
? data.map(d => Object.assign({}, d)) // shallow copy
|
||||
.filter(d => {
|
||||
const itemCourseId = parseInt(d.course_id || d.context_id || d?.plannable?.course_id);
|
||||
return itemCourseId === courseId;
|
||||
})
|
||||
: data.map(d => Object.assign({}, d));
|
||||
renderProgressRings(progressPlaceholder, scopedData);
|
||||
});
|
||||
}
|
||||
|
||||
// reflect the updated state for this item in the snapshot
|
||||
for (let i = 0; i < scopedData.length; i++) {
|
||||
if (scopedData[i].plannable_id === item.plannable_id && scopedData[i].plannable_type === item.plannable_type) {
|
||||
scopedData[i].planner_override = scopedData[i].planner_override || {};
|
||||
scopedData[i].planner_override.marked_complete = item.planner_override.marked_complete;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
clearTodoList();
|
||||
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
||||
}, 400);
|
||||
|
||||
renderProgressRings(progressPlaceholder, scopedData);
|
||||
});
|
||||
// --- Persistence (background, best-effort) ---
|
||||
// A 400/non-OK response is treated as a soft success because Canvas often
|
||||
// completes the override server-side anyway; we already reflected the
|
||||
// change in the UI, so just log it.
|
||||
fetch(domain + "/api/v1/planner/overrides" + (item.planner_override && item.planner_override.id ? "/" + item.planner_override.id : ""), {
|
||||
method: item.planner_override && item.planner_override.id ? "PUT" : "POST",
|
||||
headers: {
|
||||
"content-type":"application/json",
|
||||
"accept":"application/json",
|
||||
"X-CSRF-Token": csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: item.planner_override && item.planner_override.id ? item.planner_override.id : null,
|
||||
marked_complete: completeState,
|
||||
plannable_id: item.plannable_id,
|
||||
plannable_type: item.plannable_type
|
||||
})
|
||||
})
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
console.log("marked as complete");
|
||||
} else {
|
||||
// Non-OK (e.g. 400) is logged but not surfaced: the UI already
|
||||
// reflects the intended state, and Canvas typically applies the
|
||||
// override server-side regardless.
|
||||
console.warn("planner override request returned", resp.status, "— UI already updated optimistically");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
clearTodoList();
|
||||
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
||||
}, 400);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error("error marking as complete", err));
|
||||
})
|
||||
.catch(err => console.error("error marking as complete", err));
|
||||
|
||||
}
|
||||
|
||||
@ -3273,6 +3340,8 @@ function createTodoViewMore(location, type) {
|
||||
|
||||
// better todo init
|
||||
function setupBetterTodo() {
|
||||
// Better Todo list is removed from quizzes (it interferes with the quiz page).
|
||||
if (isQuizPage()) return;
|
||||
if (options.better_todo !== true || isGradesPage()) return;
|
||||
if (document.querySelector('#canvasrefined-todo-list')) return;
|
||||
let list = document.querySelector("#right-side");
|
||||
@ -4056,6 +4125,8 @@ function toggleDarkMode() {
|
||||
}
|
||||
|
||||
function runDarkModeFixer(override = false) {
|
||||
// Quiz safe mode: never auto-run the dark mode fixer on quiz pages.
|
||||
if (quizSafeModeActive()) return { "path": "canvasrefined-none", "time": "" };
|
||||
if (options.dark_mode !== true) return { "path": "canvasrefined-darkmode_off", "time": "" };
|
||||
if (override === false && !options["dark_mode_fix"].includes(window.location.pathname)) return { "path": "canvasrefined-none", "time": "" };
|
||||
let output = inspectDarkMode();
|
||||
@ -5029,6 +5100,8 @@ Custom font
|
||||
*/
|
||||
|
||||
function loadCustomFont() {
|
||||
// Quiz safe mode: don't override fonts on quiz pages.
|
||||
if (quizSafeModeActive()) return;
|
||||
let link = document.querySelector("#custom_font_link");
|
||||
let style = document.querySelector("#custom_font");
|
||||
|
||||
@ -5069,6 +5142,8 @@ Smaller features
|
||||
*/
|
||||
|
||||
function applyAestheticChanges() {
|
||||
// Quiz safe mode: don't inject custom layout/aesthetic CSS on quiz pages.
|
||||
if (quizSafeModeActive()) return;
|
||||
let style = document.querySelector("#canvasrefined-aesthetics") || document.createElement('style');
|
||||
style.id = "canvasrefined-aesthetics";
|
||||
style.textContent = "";
|
||||
@ -5091,6 +5166,88 @@ function applyAestheticChanges() {
|
||||
document.documentElement.appendChild(style);
|
||||
}
|
||||
|
||||
/*
|
||||
Quiz safe mode banner (pre-quiz pages only).
|
||||
Shows an info box explaining the extension hasn't been approved by all teachers,
|
||||
with a toggle for Quiz Safe Mode and a "Don't show again" button.
|
||||
*/
|
||||
function setupQuizSafeModeBanner() {
|
||||
if (!isQuizPreTakePage()) return;
|
||||
if (document.getElementById("canvasrefined-quiz-safe-banner")) return;
|
||||
|
||||
chrome.storage.local.get("quiz_safe_mode_reminder_dismissed", local => {
|
||||
if (local && local.quiz_safe_mode_reminder_dismissed === true) return;
|
||||
chrome.storage.sync.get("quiz_safe_mode", sync => {
|
||||
const safeModeOn = sync && sync.quiz_safe_mode === true;
|
||||
injectQuizSafeModeBanner(safeModeOn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function injectQuizSafeModeBanner(safeModeOn) {
|
||||
// Only inject into a real Canvas content container — never <body>, which
|
||||
// would place the banner outside the content area if it renders too early.
|
||||
const findContainer = () =>
|
||||
document.querySelector(".ic-Layout-contentMain") ||
|
||||
document.querySelector("#content") ||
|
||||
document.querySelector("#main");
|
||||
|
||||
const insertInto = (container) => {
|
||||
if (!container) return false;
|
||||
if (document.getElementById("canvasrefined-quiz-safe-banner")) return true;
|
||||
|
||||
const banner = makeElement("div", container, {
|
||||
id: "canvasrefined-quiz-safe-banner",
|
||||
className: "canvasrefined-quiz-safe-banner",
|
||||
}, true);
|
||||
|
||||
makeElement("div", banner, {
|
||||
className: "canvasrefined-quiz-safe-title",
|
||||
textContent: "Canvas Refined — Quiz Safe Mode",
|
||||
});
|
||||
|
||||
makeElement("p", banner, {
|
||||
className: "canvasrefined-quiz-safe-info",
|
||||
textContent: "This extension hasn't been 100% approved by all teachers. Quiz Safe Mode turns off most Canvas Refined features that could interfere with this quiz page, giving you the default Canvas quiz experience.",
|
||||
});
|
||||
|
||||
const toggleRow = makeElement("div", banner, { className: "canvasrefined-quiz-safe-row" });
|
||||
const toggleWrap = makeElement("label", toggleRow, { className: "canvasrefined-quiz-safe-toggle" });
|
||||
const checkbox = makeElement("input", toggleWrap, { type: "checkbox" });
|
||||
checkbox.checked = !!safeModeOn;
|
||||
checkbox.addEventListener("change", () => {
|
||||
chrome.storage.sync.set({ quiz_safe_mode: checkbox.checked });
|
||||
// The storage.onChanged listener (applyOptionsChanges) reloads quiz pages.
|
||||
});
|
||||
makeElement("span", toggleWrap, {
|
||||
className: "canvasrefined-quiz-safe-toggle-label",
|
||||
textContent: "Enable Quiz Safe Mode",
|
||||
});
|
||||
|
||||
const dismissBtn = makeElement("button", toggleRow, {
|
||||
className: "canvasrefined-quiz-safe-dismiss",
|
||||
type: "button",
|
||||
textContent: "Don't show again",
|
||||
title: "Hides this reminder permanently. You can still toggle Quiz Safe Mode in the extension popup.",
|
||||
});
|
||||
dismissBtn.addEventListener("click", () => {
|
||||
chrome.storage.local.set({ quiz_safe_mode_reminder_dismissed: true });
|
||||
banner.remove();
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (insertInto(findContainer())) return;
|
||||
|
||||
// Content container not ready yet; wait for it (never fall back to <body>).
|
||||
const obs = new MutationObserver(() => {
|
||||
if (insertInto(findContainer())) obs.disconnect();
|
||||
});
|
||||
obs.observe(document.documentElement, { childList: true, subtree: true });
|
||||
setTimeout(() => obs.disconnect(), 15000);
|
||||
}
|
||||
|
||||
|
||||
function changeGradientCards() {
|
||||
if (options.gradient_cards === true) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const syncedSwitches = ['remind', 'tab_icons', 'hide_feedback', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards'];
|
||||
const syncedSwitches = ['remind', 'tab_icons', 'hide_feedback', 'dark_mode', 'remlogo', 'full_width', 'auto_dark', 'assignments_due', 'gpa_calc', 'gradient_cards', 'disable_color_overlay', 'dashboard_grades', 'dashboard_notes', 'better_todo', 'better_sidebar', 'condensed_cards', 'hide_new_canvas', 'center_cards', 'quiz_safe_mode'];
|
||||
const syncedSubOptions = [
|
||||
"todo_hide_feedback",
|
||||
"todo_full_height",
|
||||
@ -54,7 +54,7 @@ const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_c
|
||||
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur"];
|
||||
// Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,
|
||||
// no personal productivity features).
|
||||
const exportToggles = ["dark_mode"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
|
||||
const exportToggles = ["dark_mode", "quiz_safe_mode"].concat(exportCardColorToggles, exportLayout, exportSidebar, exportTodo);
|
||||
const fontsDropdownStateKey = "fonts_dropdown_open";
|
||||
|
||||
const apiurl = "none";
|
||||
@ -140,6 +140,7 @@ const defaultOptions = {
|
||||
"equal_height_cards": false,
|
||||
"hide_feedback": false,
|
||||
"hide_new_canvas": true,
|
||||
"quiz_safe_mode": false,
|
||||
"dark_mode_fix": [],
|
||||
"assignment_states": {},
|
||||
"tab_icons": false,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user