mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
Merge branch 'main' into asessment-average-manual-input
This commit is contained in:
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 page’s
|
||||
* 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 Search’s `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 });
|
||||
|
||||
Reference in New Issue
Block a user