mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-24 05:45:10 +02:00
Compare commits
5 Commits
47ce60bc0d
...
c63b44a0b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c63b44a0b3 | ||
|
|
6b9653cc15 | ||
|
|
d715feb410 | ||
|
|
b859108ac8 | ||
|
|
6aa50259e4 |
@ -389,3 +389,19 @@
|
||||
#grades_summary td.details { white-space: normal; }
|
||||
#assignments { overflow-x: auto; }
|
||||
|
||||
|
||||
/* Big strikethrough on checked-off tasks (dashboard list view). Canvas keeps
|
||||
completed planner items looking identical to unfinished ones — the only cue
|
||||
is the checkbox itself — so when a task's checkbox is checked, strike its
|
||||
title through with a thick line and dim the row. :has() matches the
|
||||
checkbox state reactively, so this also applies live at the moment a task
|
||||
is checked, before Canvas collapses it into the completed-items section. */
|
||||
.planner-item:has(input[data-testid="planner-item-completed-checkbox"]:checked) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.planner-item:has(input[data-testid="planner-item-completed-checkbox"]:checked) .PlannerItem-styles__details a,
|
||||
.planner-item:has(input[data-testid="planner-item-completed-checkbox"]:checked) .PlannerItem-styles__title a,
|
||||
.planner-item:has(input[data-testid="planner-item-completed-checkbox"]:checked) .PlannerItem-styles__title button {
|
||||
text-decoration: line-through !important;
|
||||
text-decoration-thickness: 2.5px !important;
|
||||
}
|
||||
|
||||
@ -1207,4 +1207,232 @@ hr {
|
||||
color-scheme: dark !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* Dashboard list view header (Today / Add To Do / Show My Grades /
|
||||
opportunities): Canvas paints the icon glyphs a dark ink color that
|
||||
disappears on the dark glass header bar. Recolor the header text and
|
||||
its icon SVGs to the theme text color. */
|
||||
.PlannerHeader-styles__root {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
.PlannerHeader-styles__root svg {
|
||||
fill: var(--bctext-0) !important;
|
||||
}
|
||||
/* The "Today" button's label span carries Canvas's dark ink color from its
|
||||
emotion class, which beats the inherited root color above — dark text on
|
||||
the dark header bar. Force the whole button chain to the theme text color. */
|
||||
.PlannerHeader-styles__root button,
|
||||
.PlannerHeader-styles__root button span {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* Solid themed surfaces for the header buttons. Canvas leaves the icon
|
||||
buttons (Add To Do / Show My Grades / opportunities) fully transparent
|
||||
and paints the Today button an unthemed color — on the dark header bar
|
||||
they need solid backgrounds to read as buttons. #planner-today-btn:hover
|
||||
is listed separately because the ID selector on the base background rule
|
||||
above would otherwise out-specify the class-only :hover rule. */
|
||||
.PlannerHeader-styles__root button {
|
||||
background: var(--bcbackground-1) !important;
|
||||
border: 1px solid var(--bcborders) !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
.PlannerHeader-styles__root button:hover,
|
||||
#planner-today-btn:hover {
|
||||
background: var(--bcbackground-2) !important;
|
||||
}
|
||||
/* The "Today" button keeps its filled surface (from the rule above) but
|
||||
gets no outline. Two borders were boxing it in: the themed border the
|
||||
rule above paints on the button itself, and — the sneaky one —
|
||||
Instructure's Button variant draws its own light-gray border
|
||||
(rgb(232,234,236)) on the inner [class$="-baseButton__content"] span,
|
||||
which reads as a bright 1px ring on the dark chip. The icon buttons'
|
||||
content spans carry no border, so only Today needs this. Transparent
|
||||
(instead of border: none) keeps the button's exact dimensions. */
|
||||
#planner-today-btn,
|
||||
#planner-today-btn [class$="-baseButton__content"] {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
/* Instructure's Button variant paints its inner content span white (and a
|
||||
light gray on hover / white + inset shadow on active). That span covers
|
||||
the themed button background painted above, so the "Today" button still
|
||||
rendered as a white box. Flatten the content span in every state so the
|
||||
button's own surface shows through. */
|
||||
.PlannerHeader-styles__root button [class$="-baseButton__content"],
|
||||
.PlannerHeader-styles__root button:hover [class$="-baseButton__content"],
|
||||
.PlannerHeader-styles__root button:active [class$="-baseButton__content"] {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
/* Dashboard list-view trays ("Add To Do" / "My Grades" opened from the
|
||||
header buttons): Instructure UI renders Tray panels as body-level
|
||||
portals — body > span > span[...-tray] — with a hardcoded white
|
||||
background. Paint the tray panel with the theme background; the tray
|
||||
contents already pick up the theme text color. */
|
||||
body > span > span[class*="-tray"] {
|
||||
background: var(--bcbackground-0) !important;
|
||||
}
|
||||
/* The Add To Do form ships its own <style> tag hardcoding background
|
||||
#FFFFFF on its root. */
|
||||
.UpdateItemTray-styles__root {
|
||||
background: var(--bcbackground-0) !important;
|
||||
}
|
||||
/* InstUI TextInput / Select facades (Title, Date, Time, Course fields):
|
||||
white surface, dark ink, gray border. Repaint with the theme surface,
|
||||
text, and border colors — the calendar / arrow icons inside use
|
||||
currentColor, so they follow. Attribute-contains matching because
|
||||
emotion appends animation-state classes (e.g. transition--*) after the
|
||||
component class. */
|
||||
[class*="-textInput__facade"] {
|
||||
background: var(--bcbackground-1) !important;
|
||||
border-color: var(--bcborders) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* Field labels (Title / Date / Time / Course / Details) and the date-time
|
||||
summary message keep Canvas's dark ink. */
|
||||
[class$="-formFieldLayout__label"],
|
||||
[class$="-formFieldMessage"] {
|
||||
color: var(--bctext-1) !important;
|
||||
}
|
||||
/* InstUI ContextView popovers — the opportunities popup behind "Show My
|
||||
Grades" and the date-picker calendar behind the Date field: white card
|
||||
with dark text. Theme the card; descendants without their own ink
|
||||
color (weekday headers, month label) inherit from here. */
|
||||
[class*="-contextView__content"] {
|
||||
background: var(--bcbackground-0) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* The opportunities popup's tab labels carry their own dark ink. */
|
||||
[class*="-contextView__content"] [class$="-view-tab"] {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
/* Flatten the white InstUI View surfaces nested inside those popovers
|
||||
(the calendar body, the opportunities tab strip, the panel content) so
|
||||
the themed card shows through. */
|
||||
[class*="-contextView__content"] [class*="-view--inlineBlock"],
|
||||
[class*="-contextView__content"] [class*="-view--block"],
|
||||
[class*="-contextView__content"] [class*="-view-tabs__container"],
|
||||
[class*="-contextView__content"] [class*="-view-panel__content"],
|
||||
[class*="-contextView__content"] [class*="-calendar__navigation"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
/* Calendar day chips: white squares with dark numbers. Flatten them and
|
||||
recolor; the selected day keeps a filled chip (matched structurally via
|
||||
aria-selected, since the chip's emotion class is a content hash). */
|
||||
[class*="-calendarDay__day"] {
|
||||
background: transparent !important;
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
button[aria-selected="true"] > [class*="-calendarDay__day"] {
|
||||
background: var(--bclinks) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
/* InstUI Select dropdowns (Time / Course) open body-level popover portals
|
||||
with white View wrappers and white option rows around the options list.
|
||||
Theme the list, flatten the wrappers and rows; the wrappers' emotion
|
||||
classes carry no semantic suffix, so they are matched structurally with
|
||||
:has() on the options list they contain (portal pattern: body > span). */
|
||||
[class*="-options__list"] {
|
||||
background: var(--bcbackground-0) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
border-color: var(--bcborders) !important;
|
||||
}
|
||||
[class*="-options__list"] [class$="-optionItem__container"] {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
[class$="-optionItem"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
[class$="-optionItem"]:hover,
|
||||
[class$="-optionItem"][aria-selected="true"] {
|
||||
background: var(--bcbackground-2) !important;
|
||||
}
|
||||
body > span span:has([class*="-options__list"]) {
|
||||
background: transparent !important;
|
||||
}
|
||||
/* Planner "Submitted" pill in the completed-items row: InstUI renders it
|
||||
as a white chip with gray text. */
|
||||
.BadgeList-styles__item [class*="-pill"] {
|
||||
background: var(--bcbackground-2) !important;
|
||||
color: var(--bctext-1) !important;
|
||||
}
|
||||
/* Flash alert toasts (.flashalert-message, e.g. "Nothing planned today.
|
||||
Selecting next item."): Canvas renders them as white cards with dark
|
||||
text, unthemed in dark mode. Paint them with the theme background and
|
||||
text color; the inner div[open] is the alert card itself. The close X
|
||||
glyph inherits currentColor from the button. */
|
||||
.flashalert-message > div {
|
||||
background: var(--bcbackground-0) !important;
|
||||
color: var(--bctext-0) !important;
|
||||
border-color: var(--bcborders) !important;
|
||||
}
|
||||
.flashalert-message > div p {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
.flashalert-message > div button {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
.flashalert-message > div button svg {
|
||||
fill: var(--bctext-0) !important;
|
||||
}
|
||||
/* Global Announcements page (…/account_notifications): the Current/Recent
|
||||
tabs come from Instructure UI. Canvas paints the tab labels ("Current" /
|
||||
"Recent") and the panel caption ("Announcements from the past four
|
||||
months") with its dark ink, and the active tab panel's content wrapper
|
||||
(the direct div child of #currentTab/#pastTab) gets a white surface —
|
||||
all unreadable in dark mode. The panel ids and aria-controls values are
|
||||
stable; the emotion class hashes are not, so they are not used.
|
||||
The white surface actually comes from the outer tabs container
|
||||
(.css-gpxu0l-view-tabs__container, style background:#fff) that wraps
|
||||
both the tab strip (.css-1baf0tq-view-tabs) and the panels — it is
|
||||
themed via the stable "view-tabs__container" class fragment, scoped
|
||||
to this page's panels with :has() so other Instructure UI tabs
|
||||
elsewhere in Canvas are untouched.
|
||||
.notification_account_content is the account notification card used on
|
||||
this page and in the dashboard announcement banner: Canvas sets dark
|
||||
ink on the card chain (.ic-notification down through
|
||||
.notification_message and .notification_account_content_text), leaving
|
||||
the announcement body and the "This is an announcement from…" line
|
||||
invisible on the themed dark card background. Recolor the body text to
|
||||
the theme text color, keep the h2 title at the brighter heading color
|
||||
(an ancestor rule below would otherwise dim it), and mute the meta
|
||||
line. These class rules also fix the same markup in the dashboard
|
||||
banner. */
|
||||
div[class*='view-tabs__container']:has(#currentTab, #pastTab),
|
||||
#currentTab>div,
|
||||
#pastTab>div {
|
||||
background: var(--bcbackground-0) !important;
|
||||
}
|
||||
[aria-controls=currentTab],
|
||||
[aria-controls=pastTab] {
|
||||
color: var(--bctext-1) !important;
|
||||
}
|
||||
#currentTab>div>span,
|
||||
#pastTab>div>span {
|
||||
color: var(--bctext-2) !important;
|
||||
}
|
||||
.notification_account_content,
|
||||
.notification_account_content .ic-notification__content,
|
||||
.notification_account_content .ic-notification__message,
|
||||
.notification_message,
|
||||
.notification_message p,
|
||||
.notification_message span,
|
||||
.notification_message strong,
|
||||
.notification_message b,
|
||||
.notification_message em,
|
||||
.notification_message li,
|
||||
.notification_message td,
|
||||
.notification_message th {
|
||||
color: var(--bctext-1) !important;
|
||||
}
|
||||
.notification_account_content .ic-notification__title {
|
||||
color: var(--bctext-0) !important;
|
||||
}
|
||||
.notification_account_content_text,
|
||||
.notification_account_content_text b,
|
||||
.notification_account_content_text strong {
|
||||
color: var(--bctext-2) !important;
|
||||
}
|
||||
.notification_message a,
|
||||
.notification_account_content a {
|
||||
color: var(--bclinks) !important;
|
||||
}
|
||||
`;
|
||||
366
js/content.js
366
js/content.js
@ -105,10 +105,44 @@ function findContentContainer() {
|
||||
|
||||
let submissionPageButtonObserver = null;
|
||||
let submissionButtonScheduled = false;
|
||||
let assignmentButtonScheduled = false;
|
||||
let profileLogoutButtonObserver = null;
|
||||
let newCanvasButtonObserver = null;
|
||||
let sequenceFooterObserver = null;
|
||||
|
||||
// Current user id, needed to build "Go to Grades" links on assignment pages.
|
||||
// The page's ENV global isn't visible to content scripts (isolated world), so
|
||||
// ask the Canvas API once and cache the result.
|
||||
// undefined = not fetched yet, null = fetch failed, number = ok.
|
||||
let currentUserIdCache;
|
||||
let currentUserIdPromise = null;
|
||||
function ensureCurrentUserId() {
|
||||
if (currentUserIdCache !== undefined) return Promise.resolve(currentUserIdCache);
|
||||
if (!currentUserIdPromise) {
|
||||
currentUserIdPromise = getData(`${domain}/api/v1/users/self`)
|
||||
.then(user => {
|
||||
currentUserIdCache = (user && user.id) || null;
|
||||
})
|
||||
.catch(() => {
|
||||
currentUserIdCache = null;
|
||||
})
|
||||
.then(() => {
|
||||
currentUserIdPromise = null;
|
||||
return currentUserIdCache;
|
||||
});
|
||||
}
|
||||
return currentUserIdPromise;
|
||||
}
|
||||
|
||||
// Assignment pages (/courses/123/assignments/456) link to the current user's
|
||||
// submission ("grades") page for that assignment. The lookahead keeps this
|
||||
// from matching the submission pages themselves (/.../submissions/678).
|
||||
function getAssignmentGradesLink() {
|
||||
const match = window.location.pathname.match(/^\/courses\/(\d+)\/assignments\/(\d+)(?!\/submissions)(?:\/|$)/);
|
||||
if (!match || currentUserIdCache == null) return null;
|
||||
return `${domain}/courses/${match[1]}/assignments/${match[2]}/submissions/${currentUserIdCache}`;
|
||||
}
|
||||
|
||||
function addSubmissionPageButton() {
|
||||
const assignmentLink = getSubmissionAssignmentLink();
|
||||
if (!assignmentLink) return;
|
||||
@ -139,6 +173,31 @@ function addSubmissionPageButton() {
|
||||
}
|
||||
}
|
||||
|
||||
// Assignment pages: /courses/123/assignments/456 — add a "Go to Grades" button
|
||||
// to the right edge of the title row.
|
||||
function addAssignmentPageButton() {
|
||||
const link = getAssignmentGradesLink();
|
||||
if (!link) return;
|
||||
// Place the button inside the assignment header's .title-content block,
|
||||
// pinned to its right edge on the title's line. .title-content is a plain
|
||||
// block wrapping the <h1>, so switch it to a flex row (h1 left, button
|
||||
// right); the h1 still wraps its text when long.
|
||||
const titleContent = document.querySelector(".assignment-title .title-content")
|
||||
|| document.querySelector(".title-content");
|
||||
if (!titleContent || titleContent.querySelector("#canvasrefined-assignment-grades")) return;
|
||||
|
||||
titleContent.style.display = "flex";
|
||||
titleContent.style.alignItems = "center";
|
||||
titleContent.style.gap = "12px";
|
||||
makeElement("a", titleContent, {
|
||||
id: "canvasrefined-assignment-grades",
|
||||
className: "canvasrefined-custom-btn",
|
||||
href: link,
|
||||
textContent: "Go to Grades",
|
||||
style: "display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:auto;padding:6px 12px;text-decoration:none;font-weight:700;font-size:16px;color:inherit!important;white-space:nowrap;",
|
||||
});
|
||||
}
|
||||
|
||||
function addProfileLogoutPageButton() {
|
||||
if (!isProfilePage()) return;
|
||||
const content = document.getElementById("content");
|
||||
@ -208,6 +267,44 @@ function maintainSubmissionPageButton() {
|
||||
});
|
||||
}
|
||||
|
||||
// Same reconciliation pattern as maintainSubmissionPageButton, but for the
|
||||
// "Go to Grades" button on assignment pages. The grades link needs the
|
||||
// current user id, so on the first assignment page visit we kick off the API
|
||||
// fetch and re-run once it resolves.
|
||||
function maintainAssignmentPageButton() {
|
||||
if (assignmentButtonScheduled) return;
|
||||
assignmentButtonScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
assignmentButtonScheduled = false;
|
||||
const isAssignmentPage = /^\/courses\/\d+\/assignments\/\d+(?!\/submissions)(?:\/|$)/.test(window.location.pathname);
|
||||
const existing = document.getElementById("canvasrefined-assignment-grades");
|
||||
if (!isAssignmentPage) {
|
||||
if (existing) {
|
||||
const titleContent = existing.closest(".title-content");
|
||||
existing.remove();
|
||||
// Undo the flex-row layout we applied to the title block.
|
||||
if (titleContent) {
|
||||
titleContent.style.display = "";
|
||||
titleContent.style.alignItems = "";
|
||||
titleContent.style.gap = "";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentUserIdCache === undefined) {
|
||||
ensureCurrentUserId().then(() => maintainAssignmentPageButton());
|
||||
return;
|
||||
}
|
||||
const link = getAssignmentGradesLink();
|
||||
if (!link) return;
|
||||
if (existing) {
|
||||
if (existing.href !== link) existing.href = link;
|
||||
return;
|
||||
}
|
||||
addAssignmentPageButton();
|
||||
});
|
||||
}
|
||||
|
||||
function isAssignmentPage() {
|
||||
return /^\/courses\/\d+\/assignments(?:\/\d+)?(?:\/|$)/.test(current_page);
|
||||
}
|
||||
@ -270,18 +367,25 @@ function watchSequenceFooter() {
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// One persistent, rAF-throttled observer that keeps the button present whenever
|
||||
// we're on a submission page. Unlike the old 10s-disconnecting observer, this
|
||||
// survives Canvas' post-navigation re-renders that remove injected nodes. The
|
||||
// extra delayed checks cover React hydration that wipes the button after our
|
||||
// first add without emitting any later mutation for the observer to catch.
|
||||
// One persistent, rAF-throttled observer that keeps both assignment-page
|
||||
// navigation buttons present: the "Back to Assignment" button on submission
|
||||
// pages and the "Go to Grades" button on assignment pages. Unlike the old
|
||||
// 10s-disconnecting observer, this survives Canvas' post-navigation re-renders
|
||||
// that remove injected nodes. The extra delayed checks cover React hydration
|
||||
// that wipes the button after our first add without emitting any later mutation
|
||||
// for the observer to catch.
|
||||
function maintainAssignmentNavButtons() {
|
||||
maintainSubmissionPageButton();
|
||||
maintainAssignmentPageButton();
|
||||
}
|
||||
|
||||
function watchSubmissionPageButton() {
|
||||
if (submissionPageButtonObserver) return;
|
||||
maintainSubmissionPageButton();
|
||||
submissionPageButtonObserver = new MutationObserver(maintainSubmissionPageButton);
|
||||
maintainAssignmentNavButtons();
|
||||
submissionPageButtonObserver = new MutationObserver(maintainAssignmentNavButtons);
|
||||
submissionPageButtonObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||||
for (const ms of [300, 800, 1600, 3000, 5000]) {
|
||||
setTimeout(maintainSubmissionPageButton, ms);
|
||||
setTimeout(maintainAssignmentNavButtons, ms);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1087,6 +1191,46 @@ async function applyCustomBackground() {
|
||||
backdrop-filter: blur(${bgBlur}px) saturate(120%) !important;
|
||||
-webkit-backdrop-filter: blur(${bgBlur}px) saturate(120%) !important;
|
||||
}
|
||||
/* Dashboard list view (planner). Canvas paints each day block an
|
||||
opaque theme color, so with a background image the whole list reads
|
||||
as one solid slab that hides the image — unlike card view, where
|
||||
the glass header and (optionally) translucent cards let it show
|
||||
through. Give each day group the same glass treatment as the module
|
||||
panels (color-mix tint + slider blur + rounded border) so the
|
||||
background peeks through between the day cards. */
|
||||
#dashboard-planner .planner-day,
|
||||
#dashboard-planner .planner-empty-days {
|
||||
background: 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: 12px !important;
|
||||
border: 1px solid color-mix(in srgb, var(--bcborders) 75%, transparent) !important;
|
||||
padding: 8px 12px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
/* Inner surfaces Canvas keeps opaque (the "Show N completed
|
||||
item" facade, "Nothing Planned" filler, and the Today/Add To Do
|
||||
header cluster that sits on the glass dashboard header bar):
|
||||
flatten them so the glass behind shows through. The course-
|
||||
grouping label instead gets a subtle chip behind the course name —
|
||||
it sits over the course hero image, so without a backdrop the
|
||||
text can be hard to read on busy images. */
|
||||
#dashboard-planner .CompletedItemsFacade-styles__root,
|
||||
#dashboard-planner .EmptyDays-styles__nothingPlanned,
|
||||
#dashboard-planner-header .PlannerHeader-styles__root {
|
||||
background: transparent !important;
|
||||
}
|
||||
#dashboard-planner .Grouping-styles__title {
|
||||
background: var(--bcbackground-1) !important;
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
/* Item-row hover: subtle tint on the glass instead of Canvas's flat
|
||||
gray, so rows feel alive on the translucent day cards. */
|
||||
#dashboard-planner .planner-item:hover,
|
||||
#dashboard-planner .Grouping-styles__heroHover:hover {
|
||||
background: color-mix(in srgb, var(--bctext-0) 5%, transparent) !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
#right-side-wrapper {
|
||||
backdrop-filter: blur(${bgBlur}px) !important;
|
||||
-webkit-backdrop-filter: blur(${bgBlur}px) !important;
|
||||
@ -1471,7 +1615,7 @@ function recieveMessage(request, sender, sendResponse) {
|
||||
}
|
||||
return true; // keep the message channel open for async sendResponse
|
||||
case ("setcolors"): changeColorPreset(request.options); sendResponse(true); break;
|
||||
case ("getcolors"): sendResponse(getCardColors()); break;
|
||||
case ("getcolors"): getCardColors().then(colors => sendResponse(colors)); return true; // keep the message channel open for async sendResponse
|
||||
case ("inspect"): sendResponse(inspectDarkMode(true)); break;
|
||||
case ("fixdm"): sendResponse(runDarkModeFixer(true)); break;
|
||||
case ("updateBackground"): applyCustomBackground(); sendResponse(true); break;
|
||||
@ -1543,16 +1687,15 @@ function inspectDarkMode(withOutput = false) {
|
||||
return { "selectors": output === "" ? "no gaps determined" : output, "time": performance.now() - time };
|
||||
}
|
||||
|
||||
function getCardColors() {
|
||||
let cards = document.querySelectorAll(".ic-DashboardCard__header");
|
||||
let colors = [];
|
||||
cards.forEach(card => {
|
||||
let rgbColor = card.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor;
|
||||
colors.push({ "href": card.querySelector(".ic-DashboardCard__link").href, "color": rgbToHex(rgbColor) });
|
||||
});
|
||||
colors.sort((a, b) => a.href > b.href ? 1 : -1);
|
||||
colors = colors.map(x => x.color);
|
||||
return colors;
|
||||
async function getCardColors() {
|
||||
// Same display order changeColorPreset uses to APPLY palettes, so an
|
||||
// exported theme's color list maps back onto the same courses when
|
||||
// applied. Works in list mode too (API fallback inside getPaletteCards).
|
||||
const { cards, apiColors } = await getPaletteCards();
|
||||
if (cards.length === 0) return [];
|
||||
return cards.map(card => card.el
|
||||
? rgbToHex(card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor)
|
||||
: (apiColors["course_" + card.href.split("courses/")[1]] || "#ffffff"));
|
||||
}
|
||||
|
||||
function getCardsFromDashboard() {
|
||||
@ -1868,6 +2011,49 @@ function getDashboardCourseOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
// Keep the centered % / count text clear of the progress graphics (the rings'
|
||||
// center hole and the rainbow's bowl). The text block is measured after each
|
||||
// render; if it would cross into the strokes its fonts are scaled down, and
|
||||
// when the hole is really tight the count line is dropped before the % is
|
||||
// allowed to shrink below readable size. Font sizes reset to the defaults on
|
||||
// every render so the text grows back when there is room again.
|
||||
// `neededRadius(hw, hh)` returns the distance from the hole's center to the
|
||||
// farthest text corner; the text fits when that is <= availableRadius.
|
||||
function fitProgressOverlayText(overlay, neededRadius, availableRadius) {
|
||||
const textWrap = overlay?.firstElementChild;
|
||||
const pct = overlay?.querySelector('.canvasrefined-progress-percent');
|
||||
const cnt = overlay?.querySelector('.canvasrefined-progress-count');
|
||||
if (!textWrap || !pct || !cnt || textWrap === pct || textWrap === cnt) return;
|
||||
// Undo any shrink applied by a previous render before measuring (these
|
||||
// are the default sizes the overlays are created with).
|
||||
pct.style.fontSize = '20px';
|
||||
cnt.style.fontSize = '12px';
|
||||
cnt.style.display = '';
|
||||
if (!availableRadius || availableRadius <= 0) return;
|
||||
let w = textWrap.offsetWidth;
|
||||
let h = textWrap.offsetHeight;
|
||||
if (!w || !h) return;
|
||||
let needed = neededRadius(w / 2, h / 2);
|
||||
if (needed <= availableRadius) return;
|
||||
let scale = availableRadius / needed;
|
||||
if (20 * scale < 11) {
|
||||
// Too tight for both lines: drop the count and re-fit the % alone.
|
||||
cnt.style.display = 'none';
|
||||
w = textWrap.offsetWidth;
|
||||
h = textWrap.offsetHeight;
|
||||
needed = neededRadius(w / 2, h / 2);
|
||||
if (needed <= availableRadius) return;
|
||||
scale = availableRadius / needed;
|
||||
}
|
||||
pct.style.fontSize = `${Math.max(10, Math.round(20 * scale))}px`;
|
||||
if (cnt.style.display !== 'none') cnt.style.fontSize = `${Math.max(9, Math.round(12 * scale))}px`;
|
||||
// Final safety: if the readable-size floors above still don't fit, drop
|
||||
// the count line so the % is guaranteed to clear the strokes.
|
||||
if (neededRadius(textWrap.offsetWidth / 2, textWrap.offsetHeight / 2) > availableRadius) {
|
||||
cnt.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Mode "rings": concentric rings, one per course, each filled by completion.
|
||||
function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent) {
|
||||
const containerWidth = wrapper.clientWidth || 240;
|
||||
@ -1907,16 +2093,21 @@ function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent
|
||||
const decrement = stroke + gap;
|
||||
const ringCount = shown.length;
|
||||
const startRadius = outerRadius - stroke / 2;
|
||||
const minCenterRadius = 28;
|
||||
// Keep the center hole big enough for the % / count text so the numbers
|
||||
// never sit on top of the ring strokes (fitProgressOverlayText shrinks the
|
||||
// text as a safety net for unusually wide labels).
|
||||
const minCenterRadius = 44;
|
||||
const requiredSpace = (ringCount - 1) * decrement + stroke / 2 + minCenterRadius;
|
||||
let adjustFactor = 1;
|
||||
if (requiredSpace > startRadius) {
|
||||
adjustFactor = (startRadius - minCenterRadius - stroke / 2) / Math.max(1, (ringCount - 1) * decrement);
|
||||
}
|
||||
|
||||
let innerEdge = startRadius - stroke / 2;
|
||||
shown.forEach((entry, idx) => {
|
||||
const radius = startRadius - idx * Math.max(1, Math.floor(decrement * adjustFactor));
|
||||
if (radius <= 0) return;
|
||||
innerEdge = Math.min(innerEdge, radius - stroke / 2);
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const prog = entry.total === 0 ? 0 : entry.completed / entry.total;
|
||||
const color = courseRingColor(entry.courseId, idx);
|
||||
@ -1993,6 +2184,9 @@ function renderProgressRingsMode(wrapper, shown, totalAll, completedAll, percent
|
||||
};
|
||||
});
|
||||
|
||||
// Shrink the % / count text if it would reach the innermost ring.
|
||||
fitProgressOverlayText(overlay, (hw, hh) => Math.hypot(hw, hh), Math.max(0, innerEdge - 2));
|
||||
|
||||
const maxIdx = shown.length - 1;
|
||||
svg.querySelectorAll('circle').forEach(c => {
|
||||
const idx = parseInt(c.getAttribute('data-idx'));
|
||||
@ -2027,8 +2221,11 @@ function renderProgressRainbow(wrapper, shown, totalAll, completedAll, percent)
|
||||
svg.setAttribute('height', String(svgHeight));
|
||||
svg.setAttribute('viewBox', `0 0 ${size} ${svgHeight}`);
|
||||
|
||||
// shrink spacing if too many classes would overflow the inner radius
|
||||
const minInnerRadius = 14;
|
||||
// shrink spacing if too many classes would overflow the inner radius;
|
||||
// keep the inner bowl big enough for the % / count text so the numbers
|
||||
// never sit on top of the arcs (fitProgressOverlayText shrinks the text
|
||||
// as a safety net for unusually wide labels).
|
||||
const minInnerRadius = 56;
|
||||
const requiredSpace = (ringCount - 1) * decrement;
|
||||
let adjustFactor = 1;
|
||||
if (requiredSpace > outerRadius - minInnerRadius) {
|
||||
@ -2131,13 +2328,31 @@ function renderProgressRainbow(wrapper, shown, totalAll, completedAll, percent)
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'canvasrefined-progress-overlay';
|
||||
overlay.style.cssText = `position:absolute;left:0;top:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;transform:translateY(${nudge}px);`;
|
||||
overlay.innerHTML = `<div class='canvasrefined-progress-percent' style='font-weight:700;font-size:20px;line-height:1;color:var(--bctext-0);'></div><div class='canvasrefined-progress-count' style='font-size:12px;margin-top:3px;color:var(--bctext-0);'></div>`;
|
||||
// Same textWrap structure as rings mode so fitProgressOverlayText
|
||||
// measures the whole percent+count block, not just one line.
|
||||
const textWrap = document.createElement('div');
|
||||
textWrap.style.cssText = 'text-align:center;color:var(--bctext-0);';
|
||||
textWrap.innerHTML = `<div class='canvasrefined-progress-percent' style='font-weight:700;font-size:20px;line-height:1;'></div><div class='canvasrefined-progress-count' style='font-size:12px;margin-top:3px;'></div>`;
|
||||
overlay.appendChild(textWrap);
|
||||
wrapper.appendChild(overlay);
|
||||
} else {
|
||||
overlay.style.transform = `translateY(${nudge}px)`;
|
||||
}
|
||||
overlay.querySelector('.canvasrefined-progress-percent').textContent = `${percent}%`;
|
||||
overlay.querySelector('.canvasrefined-progress-count').textContent = `${completedAll}/${totalAll} done`;
|
||||
|
||||
// Shrink the % / count text if any corner would cross the innermost arc.
|
||||
// The text is centered at (cx, holeCenterY); the bowl is the semicircle
|
||||
// of innerRadius around (cx, baseY), so check the farthest text corner
|
||||
// against the bowl's inner edge.
|
||||
const bowlRadius = Math.max(0, innerRadius - stroke / 2 - 2);
|
||||
fitProgressOverlayText(overlay, (hw, hh) => {
|
||||
const dv = Math.max(
|
||||
Math.abs(baseY - holeCenterY + hh),
|
||||
Math.abs(baseY - holeCenterY - hh)
|
||||
);
|
||||
return Math.hypot(hw, dv);
|
||||
}, bowlRadius);
|
||||
}
|
||||
|
||||
// Mode "lines": one horizontal bar per course, each with its own %.
|
||||
@ -4305,6 +4520,48 @@ Card color palettes
|
||||
|
||||
let changeColorInterval = null;
|
||||
let colorChanges = [];
|
||||
|
||||
// Course list for palette operations, in DISPLAY order (first shown to
|
||||
// last) so palette colors land on courses in the order the user sees them.
|
||||
// Card view: dashboard cards are already in the DOM in display order.
|
||||
// List mode: there are no .ic-DashboardCard elements (which used to make the
|
||||
// palette silently do nothing), so fall back to the dashboard_cards API —
|
||||
// ordered by where each course's planner grouping first appears top-to-
|
||||
// bottom, with any courses not currently displayed (no items in the loaded
|
||||
// date range) at the end in API order. Also returns the user's current
|
||||
// course colors from the users/self/colors API (used for "revert colors"
|
||||
// when no DOM cards exist to read inline styles from).
|
||||
async function getPaletteCards() {
|
||||
let cards = [];
|
||||
let apiColors = {};
|
||||
document.querySelectorAll(".ic-DashboardCard__header").forEach(card => {
|
||||
cards.push({ "href": card.querySelector(".ic-DashboardCard__link").href, "el": card });
|
||||
});
|
||||
if (cards.length > 0) return { cards, apiColors };
|
||||
try {
|
||||
const [cardsRes, colorsRes] = await Promise.all([
|
||||
fetch(domain + "/api/v1/dashboard/dashboard_cards", { headers: { "accept": "application/json" } }),
|
||||
fetch(domain + "/api/v1/users/self/colors", { headers: { "accept": "application/json" } })
|
||||
]);
|
||||
const apiCards = await cardsRes.json();
|
||||
apiColors = (await colorsRes.json())?.custom_colors || {};
|
||||
const seen = new Set();
|
||||
const orderedIds = [];
|
||||
document.querySelectorAll("a.Grouping-styles__hero").forEach(hero => {
|
||||
const m = (hero.getAttribute("href") || "").match(/\/courses\/(\d+)/);
|
||||
if (m && !seen.has(m[1])) { seen.add(m[1]); orderedIds.push(m[1]); }
|
||||
});
|
||||
apiCards.forEach(card => {
|
||||
const id = String(card.id);
|
||||
if (!seen.has(id)) { seen.add(id); orderedIds.push(id); }
|
||||
});
|
||||
orderedIds.forEach(id => cards.push({ "href": domain + "/courses/" + id, "el": null }));
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
return { cards, apiColors };
|
||||
}
|
||||
|
||||
async function changeColorPreset(colors) {
|
||||
|
||||
if (colors.length === 0) return;
|
||||
@ -4318,26 +4575,45 @@ async function changeColorPreset(colors) {
|
||||
colorChanges = [];
|
||||
|
||||
// sort cards
|
||||
let cards = document.querySelectorAll(".ic-DashboardCard__header");
|
||||
let sortedCards = [];
|
||||
cards.forEach(card => {
|
||||
sortedCards.push({ "href": card.querySelector(".ic-DashboardCard__link").href, "el": card });
|
||||
});
|
||||
sortedCards.sort((a, b) => a.href > b.href ? 1 : -1);
|
||||
// (display order — see getPaletteCards; no re-sorting here so palette
|
||||
// colors apply from the first course on screen to the last)
|
||||
const { cards: sortedCards, apiColors } = await getPaletteCards();
|
||||
|
||||
// push each color change into a queue
|
||||
try {
|
||||
sortedCards.forEach((card, i) => {
|
||||
let previousColor = rgbToHex(card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor);
|
||||
let course_id = card.href.split("courses/")[1];
|
||||
let previousColor = card.el
|
||||
? rgbToHex(card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor)
|
||||
: (apiColors["course_" + course_id] || "#ffffff");
|
||||
previous.push(previousColor);
|
||||
|
||||
// Object.keys(res.custom_colors).forEach(item => {
|
||||
//let item_id = item.split("_")[1];
|
||||
let course_id = card.href.split("courses/")[1];
|
||||
|
||||
//if (card.href.includes(item_id)) {
|
||||
let cnum = i % colors.length;
|
||||
|
||||
// Apply the new color to whatever surface is rendered: dashboard
|
||||
// card elements (card view) or planner item avatars (list view),
|
||||
// so the change is visible immediately instead of only after a
|
||||
// reload.
|
||||
let applyColor = () => {
|
||||
if (card.el) {
|
||||
card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor = colors[cnum];
|
||||
card.el.querySelector(".ic-DashboardCard__header-title span").style.color = colors[cnum];
|
||||
card.el.querySelector(".ic-DashboardCard__header-button-bg").style.backgroundColor = colors[cnum];
|
||||
} else {
|
||||
const coursePrefix = "/courses/" + course_id;
|
||||
document.querySelectorAll(".planner-item").forEach(item => {
|
||||
const titleLink = item.querySelector(".PlannerItem-styles__title a");
|
||||
const heroLink = item.closest(".Grouping-styles__root")?.querySelector("a.Grouping-styles__hero");
|
||||
const inCourse = (titleLink && (titleLink.getAttribute("href") || "").startsWith(coursePrefix)) ||
|
||||
(heroLink && (heroLink.getAttribute("href") || "").startsWith(coursePrefix));
|
||||
if (inCourse) {
|
||||
const avatar = item.querySelector(".PlannerItem-styles__avatar, .PlannerItem-styles__icon");
|
||||
if (avatar) avatar.style.color = colors[cnum];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let changeCardColor = () => {
|
||||
fetch(domain + "/api/v1/users/self/colors/courses_" + course_id,
|
||||
{
|
||||
@ -4348,20 +4624,12 @@ async function changeColorPreset(colors) {
|
||||
'X-CSRF-Token': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ "hexcode": colors[cnum] })
|
||||
}).then(() => {
|
||||
card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor = colors[cnum];
|
||||
card.el.querySelector(".ic-DashboardCard__header-title span").style.color = colors[cnum];
|
||||
card.el.querySelector(".ic-DashboardCard__header-button-bg").style.backgroundColor = colors[cnum];
|
||||
});
|
||||
}).then(() => applyColor());
|
||||
}
|
||||
|
||||
colorChanges.push(changeCardColor);
|
||||
|
||||
card.el.querySelector(".ic-DashboardCard__header_hero").style.backgroundColor = colors[cnum];
|
||||
card.el.querySelector(".ic-DashboardCard__header-title span").style.color = colors[cnum];
|
||||
card.el.querySelector(".ic-DashboardCard__header-button-bg").style.backgroundColor = colors[cnum];
|
||||
//}
|
||||
// });
|
||||
applyColor();
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
@ -4383,7 +4651,13 @@ async function changeColorPreset(colors) {
|
||||
// set colors to revert back to
|
||||
chrome.storage.local.get("previous_colors", local => {
|
||||
const now = Date.now();
|
||||
if (local["previous_colors"] === null || now >= local["previous_colors"].expire) {
|
||||
const prev = local["previous_colors"];
|
||||
// Overwrite when missing or expired — and when an old list-mode run
|
||||
// (which found no dashboard cards) stored an empty list, which made
|
||||
// revert a silent no-op. Never store an empty capture (nothing to
|
||||
// revert to). chrome.storage.local.get yields undefined (not null)
|
||||
// for an unset key, so the old `=== null` check never matched it.
|
||||
if (previous.length > 0 && (!prev || now >= prev.expire || !Array.isArray(prev.colors) || prev.colors.length === 0)) {
|
||||
chrome.storage.local.set({ "previous_colors": { "colors": previous, "expire": now + 86400000 } });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1388,8 +1388,11 @@ function setup() {
|
||||
// activate revert to original card colors button
|
||||
document.querySelector("#revert-colors").addEventListener("click", () => {
|
||||
chrome.storage.local.get("previous_colors", local => {
|
||||
if (local["previous_colors"] !== null) {
|
||||
sendFromPopup("setcolors", local["previous_colors"].colors);
|
||||
const prev = local["previous_colors"];
|
||||
// Guard against unset/expired-shape entries and the empty lists
|
||||
// old list-mode runs stored — sending [] would be a silent no-op.
|
||||
if (prev && Array.isArray(prev.colors) && prev.colors.length > 0) {
|
||||
sendFromPopup("setcolors", prev.colors);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user