feat: adaptive themeing

This commit is contained in:
2026-03-16 15:40:16 +10:30
parent 577287b8a8
commit 3a2c438223
9 changed files with 197 additions and 21 deletions
+30 -8
View File
@@ -1,8 +1,10 @@
import browser from "webextension-polyfill";
import Color from "color";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { lightenAndPaleColor } from "./lightenAndPaleColor";
import ColorLuminance from "./ColorLuminance";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
@@ -13,13 +15,7 @@ const setCSSVar = (varName: any, value: any) =>
const applyProperties = (props: any) =>
Object.entries(props).forEach(([key, value]) => setCSSVar(key, value));
export function updateAllColors() {
// Determine the color to use
const selectedColor =
settingsState.selectedColor !== ""
? settingsState.selectedColor
: "#007bff";
function applyColorsWith(selectedColor: string) {
if (settingsState.transparencyEffects) {
document.documentElement.classList.add("transparencyEffects");
}
@@ -28,7 +24,7 @@ export function updateAllColors() {
const commonProps = {
"--better-sub": "#161616",
"--better-alert-highlight": "#c61851",
"--better-main": settingsState.selectedColor,
"--better-main": selectedColor,
};
// Mode-based properties, applied if storedSetting is provided
@@ -79,3 +75,29 @@ export function updateAllColors() {
}
}
}
function toSoftGradient(hex: string): string {
const base = Color(hex);
const analogous = base.rotate(30).lighten(0.25).saturate(0.15);
const mid = base.mix(analogous, 0.5).hex();
return `linear-gradient(135deg, ${hex} 0%, ${mid} 50%, ${analogous.hex()} 100%)`;
}
export async function updateAllColors() {
let effectiveColor =
settingsState.selectedColor !== ""
? settingsState.selectedColor
: "#007bff";
if (settingsState.adaptiveThemeColour) {
const adaptiveColor = await getAdaptiveColour();
if (adaptiveColor) {
effectiveColor =
settingsState.adaptiveThemeGradient
? toSoftGradient(adaptiveColor)
: adaptiveColor;
}
}
applyColorsWith(effectiveColor);
}
+114
View File
@@ -0,0 +1,114 @@
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
/**
* Parses the current page from window.location.hash.
* Returns { programme, metaclass } for /courses/SEMESTER/X:Y or /assessments/SEMESTER/X:Y, or null.
* e.g. #?page=/courses/2023S/4804:11066 or #?page=/assessments/2023S/4621:10772
*/
function parsePageContext(): { programme: number; metaclass: number } | null {
const hash = window.location.hash || "";
const match = hash.match(/[?&]page=\/(courses|assessments)\/[^/]+\/(\d+):(\d+)/);
if (!match) return null;
const programme = parseInt(match[2], 10);
const metaclass = parseInt(match[3], 10);
if (isNaN(programme) || isNaN(metaclass)) return null;
return { programme, metaclass };
}
/**
* Fetches subjects and finds the subject matching programme:metaclass.
*/
async function getSubjectCode(
programme: number,
metaclass: number
): Promise<string | null> {
try {
const res = await fetch(`${location.origin}/seqta/student/load/subjects?`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({}),
});
const data = await res.json();
const payload = data?.payload;
if (!Array.isArray(payload)) return null;
for (const semester of payload) {
const subjects = semester?.subjects;
if (!Array.isArray(subjects)) continue;
const subject = subjects.find(
(s: any) =>
s &&
Number(s.programme) === programme &&
Number(s.metaclass) === metaclass
);
if (subject?.code) return subject.code;
}
return null;
} catch (error) {
console.warn("[BetterSEQTA+] Adaptive theme: failed to load subjects:", error);
return null;
}
}
/**
* Fetches user prefs and returns the colour for the given subject code.
*/
async function getSubjectColour(
subjectCode: string,
userId: number
): Promise<string | null> {
try {
const res = await fetch(`${location.origin}/seqta/student/load/prefs?`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({
request: "userPrefs",
asArray: true,
user: userId,
}),
});
const data = await res.json();
const payload = data?.payload;
if (!Array.isArray(payload)) return null;
const pref = payload.find(
(p: { name: string; value: string }) =>
p.name === `timetable.subject.colour.${subjectCode}`
);
return pref?.value ?? null;
} catch (error) {
console.warn("[BetterSEQTA+] Adaptive theme: failed to load prefs:", error);
return null;
}
}
/**
* Returns the adaptive theme colour for the current page context, or null.
* When viewing a course or assessments page, returns the subject's assigned colour.
*/
export async function getAdaptiveColour(): Promise<string | null> {
const context = parsePageContext();
if (!context) return null;
const subjectCode = await getSubjectCode(context.programme, context.metaclass);
if (!subjectCode) return null;
let userId: number;
try {
const userInfo = await getUserInfo();
userId = userInfo?.id;
if (typeof userId !== "number") return null;
} catch {
return null;
}
const colour = await getSubjectColour(subjectCode, userId);
if (!colour || typeof colour !== "string") return null;
// Basic hex validation
if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(colour)) return colour;
if (/^[0-9A-Fa-f]{6}$/.test(colour)) return `#${colour}`;
return null;
}
+4 -2
View File
@@ -14,7 +14,9 @@ export class StorageChangeHandler {
}
private registerHandlers() {
settingsState.register("selectedColor", updateAllColors.bind(this));
settingsState.register("selectedColor", () => void updateAllColors());
settingsState.register("adaptiveThemeColour", () => void updateAllColors());
settingsState.register("adaptiveThemeGradient", () => void updateAllColors());
settingsState.register("DarkMode", this.handleDarkModeChange.bind(this));
settingsState.register("onoff", this.handleOnOffChange.bind(this));
settingsState.register("shortcuts", this.handleShortcutsChange.bind(this));
@@ -45,7 +47,7 @@ export class StorageChangeHandler {
}
private handleDarkModeChange() {
updateAllColors();
void updateAllColors();
}
private handleOnOffChange(newValue: boolean) {