Merge branch 'main' into asessment-average-manual-input

This commit is contained in:
Jaxx7594
2026-05-04 22:46:29 +08:00
committed by GitHub
24 changed files with 2889 additions and 251 deletions
+45 -3
View File
@@ -5,7 +5,7 @@ import { lightenAndPaleColor } from "./lightenAndPaleColor";
import ColorLuminance from "./ColorLuminance";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
import { getCustomThemeAdaptiveCssVariables } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
@@ -84,6 +84,21 @@ function cancelColorTransition() {
}
}
function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null {
const parsedHex = parseRepresentativeHex(s);
if (!parsedHex) return null;
try {
const [r, g, b] = Color(parsedHex).rgb().array();
return {
r: Math.round(r),
g: Math.round(g),
b: Math.round(b),
};
} catch {
return null;
}
}
function applyColorsWith(selectedColor: string) {
if (settingsState.transparencyEffects) {
document.documentElement.classList.add("transparencyEffects");
@@ -129,8 +144,35 @@ function applyColorsWith(selectedColor: string) {
applyProperties({ ...commonProps, ...modeProps, ...dynamicProps });
if (settingsState.selectedTheme) {
for (const name of getCustomThemeAdaptiveCssVariables()) {
setCSSVar(name, selectedColor);
const channels = getRepresentativeRgbChannels(selectedColor);
for (const binding of getCustomThemeAdaptiveCssVariableBindings()) {
if (!binding.channel) {
setCSSVar(binding.cssVarName, selectedColor);
continue;
}
if (!channels) {
continue;
}
if (binding.channel === "r") {
setCSSVar(binding.cssVarName, String(channels.r));
} else if (binding.channel === "g") {
setCSSVar(binding.cssVarName, String(channels.g));
} else {
setCSSVar(binding.cssVarName, String(channels.b));
}
}
}
// Let themes opt-in to overriding only adaptive accent output.
// A theme can define `--adaptive-better-main` from adaptive channel bindings.
if (settingsState.selectedTheme && settingsState.adaptiveThemeColour) {
const adaptiveOverride = getComputedStyle(document.documentElement)
.getPropertyValue("--adaptive-better-main")
.trim();
if (adaptiveOverride) {
setCSSVar("--better-main", adaptiveOverride);
}
}
@@ -1,20 +1,49 @@
/** Tracks which author-declared CSS variables mirror the effective accent; not persisted in settings storage. */
const VALID_CUSTOM_PROP = /^--[a-zA-Z0-9_-]{1,120}$/;
const VALID_CHANNEL = /^(r|g|b)$/;
let boundNames: string[] = [];
export type AdaptiveChannel = "r" | "g" | "b";
export function normalizeAdaptiveCssVariableNames(
export type AdaptiveCssVariableBinding = {
cssVarName: string;
channel?: AdaptiveChannel;
};
let boundBindings: AdaptiveCssVariableBinding[] = [];
function parseAdaptiveBinding(
rawBinding: string,
): AdaptiveCssVariableBinding | null {
const trimmed = rawBinding.trim();
if (!trimmed) return null;
const [rawName, rawChannel] = trimmed.split(":", 2);
const cssVarName = rawName?.trim() ?? "";
if (!VALID_CUSTOM_PROP.test(cssVarName)) return null;
if (!rawChannel) return { cssVarName };
const channel = rawChannel.trim().toLowerCase();
if (!VALID_CHANNEL.test(channel)) return null;
return { cssVarName, channel: channel as AdaptiveChannel };
}
export function normalizeAdaptiveCssVariableBindings(
names: string[] | undefined,
): string[] {
): AdaptiveCssVariableBinding[] {
if (!names?.length) return [];
const out: string[] = [];
const out: AdaptiveCssVariableBinding[] = [];
const seen = new Set<string>();
for (const raw of names) {
const s = raw.trim();
if (!VALID_CUSTOM_PROP.test(s) || seen.has(s)) continue;
seen.add(s);
out.push(s);
const parsed = parseAdaptiveBinding(raw);
if (!parsed) continue;
const key = `${parsed.cssVarName}:${parsed.channel ?? "full"}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(parsed);
}
return out;
}
@@ -22,19 +51,24 @@ export function normalizeAdaptiveCssVariableNames(
export function setCustomThemeAdaptiveCssVariables(
names: string[] | undefined,
): void {
for (const n of boundNames) {
document.documentElement.style.removeProperty(n);
for (const binding of boundBindings) {
document.documentElement.style.removeProperty(binding.cssVarName);
}
boundNames = normalizeAdaptiveCssVariableNames(names);
boundBindings = normalizeAdaptiveCssVariableBindings(names);
}
export function getCustomThemeAdaptiveCssVariableBindings(): AdaptiveCssVariableBinding[] {
return boundBindings;
}
// Backward-compatible helper for existing callsites.
export function getCustomThemeAdaptiveCssVariables(): string[] {
return boundNames;
return boundBindings.map((b) => b.cssVarName);
}
export function clearCustomThemeAdaptiveCssVariables(): void {
for (const n of boundNames) {
document.documentElement.style.removeProperty(n);
for (const binding of boundBindings) {
document.documentElement.style.removeProperty(binding.cssVarName);
}
boundNames = [];
boundBindings = [];
}
+9 -2
View File
@@ -34,14 +34,20 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
<h1>3.6.4 - Assessment weighting override & fixes</h1>
<h1>3.6.5 - Assessment weighting override & fixes</h1>
<li>Added the ability to override/add weightings to assessments (on assessment page).</li>
<li>Fixed the display of weightings that could not automatically be discovered.</li>
<li>Fixed the formatting of the weighting tag that was broken due to a SEQTA update.</li>
<h1>3.6.4 - Theme flavours and fixes, Upcoming Assements improvement</h1>
<li>Added advanced colour adjustments variables for theme customisation.</li>
<li>Improved logic for upcoming assements dashlet to improve compatibility.</li>
<li>BS Cloud can now automatically download themes from other devices.</li>
<li>Added theme flavours for multiple colour variations of the same theme.</li>
<h1>3.6.3 - Assessment overview fix</h1>
<li>Fixed assessments overview failing to load.</li>
<h1>3.6.2 - Cloud backup, various fixes & SEQTA Engage support</h1>
<li>BetterSEQTA Cloud: back up and restore extension settings from your account (General settings).</li>
<li>Optional automatic cloud sync if signed in (on by default).</li>
@@ -55,6 +61,7 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
<li>Updated outdated in-app links and update some under the hood code (Vite 8).</li>
<li>Added a notifications panel animation to work like settings.</li>
<li>Fix timetable edit plugin not working correctly.</li>
<h1>3.5.3 - Adaptive theme updates</h1>
<li>Fixed adaptive theming on current-year course and assessment pages.</li>
+56 -1
View File
@@ -10,6 +10,13 @@ export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
export const BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY =
"bsplus_cloud_settings_known_remote_updated_at";
/**
* Written by the service worker after applying a cloud settings envelope; the SEQTA pages
* ThemeManager reads and clears it (SW cannot share localforage/IndexedDB with the page).
*/
export const BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY =
"bsplus_pending_theme_ensure_after_cloud";
/**
* Never uploaded to the cloud backup (OAuth and legacy keys).
* IndexedDB (e.g. Global Searchs `betterseqta-index` database) is not part of
@@ -39,6 +46,7 @@ export const SENSITIVE_DEVICE_STORAGE_KEY_PREFIXES = ["plugin.global-search.stor
const CLIENT_ONLY_CLOUD_KEYS_EXACT = [
BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY,
"bsplus_lastCloudPoll",
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
] as const;
/** After restoring from cloud, keep local session so the user stays signed in. */
@@ -101,8 +109,15 @@ function stripExcludedKeysFromRemoteData(remote: Record<string, unknown>): Recor
return out;
}
/** Stored theme id (`selectedTheme`); trims whitespace; empty string clears. */
export function normalizeThemeIdForSync(raw: unknown): string {
if (typeof raw !== "string") return "";
return raw.trim();
}
export function buildUploadPayload(all: Record<string, unknown>): {
schemaVersion: number;
themeId: string;
data: Record<string, unknown>;
} {
const filtered: Record<string, unknown> = {};
@@ -111,17 +126,57 @@ export function buildUploadPayload(all: Record<string, unknown>): {
filtered[k] = v;
}
const data = migrateLegacyToPluginSettings(filtered);
return { schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION, data };
const themeId = normalizeThemeIdForSync(all.selectedTheme);
return {
schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
themeId,
data,
};
}
export async function getSnapshotForUpload(): Promise<{
schemaVersion: number;
themeId: string;
data: Record<string, unknown>;
}> {
const all = await browser.storage.local.get();
return buildUploadPayload(all as Record<string, unknown>);
}
/**
* Theme to ensure is installed locally after a downloaded envelope (explicit `themeId` overrides `data.selectedTheme`).
* Works for any store-backed id, including **flavour (slave) variants** nested under masters in the catalogue.
*/
export function resolveThemeIdForPostSyncDownload(envelope: unknown): string | undefined {
if (envelope && typeof envelope === "object" && "themeId" in envelope) {
const top = normalizeThemeIdForSync(
(envelope as Record<string, unknown>).themeId,
);
if (top) return top;
}
let remoteFlat: Record<string, unknown>;
if (
envelope &&
typeof envelope === "object" &&
"data" in envelope &&
(envelope as { data?: unknown }).data !== undefined &&
typeof (envelope as { data?: unknown }).data === "object" &&
(envelope as { data?: unknown }).data !== null &&
!Array.isArray((envelope as { data?: unknown }).data)
) {
remoteFlat = (envelope as { data: Record<string, unknown> }).data;
} else if (envelope && typeof envelope === "object" && !Array.isArray(envelope)) {
remoteFlat = envelope as Record<string, unknown>;
} else {
return undefined;
}
const migrated = migrateLegacyToPluginSettings(remoteFlat);
const fromData = normalizeThemeIdForSync(migrated.selectedTheme);
return fromData === "" ? undefined : fromData;
}
export async function setKnownRemoteUpdatedAt(iso: string | undefined): Promise<void> {
if (!iso || typeof iso !== "string") return;
await browser.storage.local.set({ [BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY]: iso });