2022-05-11 14:58:14 +05:30
|
|
|
/*
|
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2022-05-18 18:56:28 +05:30
|
|
|
import type {ILogItem} from "../../logging/types.js";
|
2022-05-11 14:58:14 +05:30
|
|
|
import type {Platform} from "./Platform.js";
|
|
|
|
|
2022-05-26 23:35:09 +05:30
|
|
|
type NormalVariant = {
|
|
|
|
id: string;
|
|
|
|
cssLocation: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type DefaultVariant = {
|
|
|
|
dark: {
|
|
|
|
id: string;
|
|
|
|
cssLocation: string;
|
2022-05-30 11:48:02 +05:30
|
|
|
variantName: string;
|
2022-05-26 23:35:09 +05:30
|
|
|
};
|
|
|
|
light: {
|
|
|
|
id: string;
|
|
|
|
cssLocation: string;
|
2022-05-30 11:48:02 +05:30
|
|
|
variantName: string;
|
2022-05-26 23:35:09 +05:30
|
|
|
};
|
2022-06-05 20:49:05 +05:30
|
|
|
default: {
|
|
|
|
id: string;
|
|
|
|
cssLocation: string;
|
|
|
|
variantName: string;
|
|
|
|
};
|
2022-05-26 23:35:09 +05:30
|
|
|
}
|
2022-05-30 11:49:45 +05:30
|
|
|
|
2022-05-26 23:35:09 +05:30
|
|
|
type ThemeInformation = NormalVariant | DefaultVariant;
|
|
|
|
|
2022-06-02 15:05:43 +05:30
|
|
|
export enum ColorSchemePreference {
|
|
|
|
Dark,
|
|
|
|
Light
|
|
|
|
};
|
|
|
|
|
2022-05-11 14:58:14 +05:30
|
|
|
export class ThemeLoader {
|
|
|
|
private _platform: Platform;
|
2022-05-26 23:35:09 +05:30
|
|
|
private _themeMapping: Record<string, ThemeInformation>;
|
2022-05-11 14:58:14 +05:30
|
|
|
|
|
|
|
constructor(platform: Platform) {
|
|
|
|
this._platform = platform;
|
|
|
|
}
|
|
|
|
|
2022-05-11 15:03:32 +05:30
|
|
|
async init(manifestLocations: string[]): Promise<void> {
|
2022-05-26 23:35:09 +05:30
|
|
|
this._themeMapping = {};
|
2022-05-18 18:56:28 +05:30
|
|
|
for (const manifestLocation of manifestLocations) {
|
|
|
|
const { body } = await this._platform
|
|
|
|
.request(manifestLocation, {
|
|
|
|
method: "GET",
|
|
|
|
format: "json",
|
|
|
|
cache: true,
|
|
|
|
})
|
|
|
|
.response();
|
2022-05-31 20:21:18 +05:30
|
|
|
this._populateThemeMap(body);
|
2022-05-18 18:56:28 +05:30
|
|
|
}
|
2022-05-11 14:58:14 +05:30
|
|
|
}
|
|
|
|
|
2022-05-31 20:21:18 +05:30
|
|
|
private _populateThemeMap(manifest) {
|
2022-06-06 11:53:13 +05:30
|
|
|
/*
|
|
|
|
After build has finished, the source section of each theme manifest
|
|
|
|
contains `built-assets` which is a mapping from the theme-id to
|
|
|
|
cssLocation of theme
|
|
|
|
*/
|
2022-05-31 20:21:18 +05:30
|
|
|
const builtAssets: Record<string, string> = manifest.source?.["built-assets"];
|
|
|
|
const themeName = manifest.name;
|
|
|
|
let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
|
|
|
|
for (const [themeId, cssLocation] of Object.entries(builtAssets)) {
|
|
|
|
const variant = themeId.match(/.+-(.+)/)?.[1];
|
|
|
|
const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!];
|
|
|
|
const themeDisplayName = `${themeName} ${variantName}`;
|
|
|
|
if (isDefault) {
|
|
|
|
/**
|
|
|
|
* This is a default variant!
|
|
|
|
* We'll add these to the themeMapping (separately) keyed with just the
|
|
|
|
* theme-name (i.e "Element" instead of "Element Dark").
|
|
|
|
* We need to be able to distinguish them from other variants!
|
|
|
|
*
|
|
|
|
* This allows us to render radio-buttons with "dark" and
|
|
|
|
* "light" options.
|
|
|
|
*/
|
|
|
|
const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant;
|
|
|
|
defaultVariant.variantName = variantName;
|
|
|
|
defaultVariant.id = themeId
|
|
|
|
defaultVariant.cssLocation = cssLocation;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Non-default variants are keyed in themeMapping with "theme_name variant_name"
|
|
|
|
// eg: "Element Dark"
|
|
|
|
this._themeMapping[themeDisplayName] = {
|
|
|
|
cssLocation,
|
|
|
|
id: themeId
|
|
|
|
};
|
2022-06-02 15:05:43 +05:30
|
|
|
}
|
2022-05-31 20:21:18 +05:30
|
|
|
if (defaultDarkVariant.id && defaultLightVariant.id) {
|
|
|
|
/**
|
|
|
|
* As mentioned above, if there's both a default dark and a default light variant,
|
|
|
|
* add them to themeMapping separately.
|
|
|
|
*/
|
2022-06-05 20:49:05 +05:30
|
|
|
const defaultVariant = this.preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant;
|
|
|
|
this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant };
|
2022-05-31 20:21:18 +05:30
|
|
|
}
|
|
|
|
else {
|
|
|
|
/**
|
|
|
|
* If only one default variant is found (i.e only dark default or light default but not both),
|
|
|
|
* treat it like any other variant.
|
|
|
|
*/
|
|
|
|
const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant;
|
|
|
|
this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation };
|
|
|
|
}
|
2022-06-06 11:53:13 +05:30
|
|
|
//Add the default-theme as an additional option to the mapping
|
|
|
|
const defaultThemeId = this.getDefaultTheme();
|
|
|
|
if (defaultThemeId) {
|
|
|
|
const themeDetails = this._findThemeDetailsFromId(defaultThemeId);
|
|
|
|
if (themeDetails) {
|
|
|
|
this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation };
|
|
|
|
}
|
|
|
|
}
|
2022-05-31 20:21:18 +05:30
|
|
|
}
|
|
|
|
|
2022-06-05 20:49:05 +05:30
|
|
|
setTheme(themeName: string, themeVariant: "light" | "dark" | "default", log?: ILogItem) {
|
|
|
|
this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => {
|
|
|
|
let cssLocation: string;
|
|
|
|
let themeDetails = this._themeMapping[themeName];
|
|
|
|
if ("id" in themeDetails) {
|
|
|
|
cssLocation = themeDetails.cssLocation;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
cssLocation = themeDetails[themeVariant ?? "default"].cssLocation;
|
|
|
|
}
|
|
|
|
this._platform.replaceStylesheet(cssLocation);
|
|
|
|
this._platform.settingsStorage.setString("theme-name", themeName);
|
|
|
|
if (themeVariant) {
|
|
|
|
this._platform.settingsStorage.setString("theme-variant", themeVariant);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this._platform.settingsStorage.remove("theme-variant");
|
2022-05-18 16:09:09 +05:30
|
|
|
}
|
2022-06-02 15:05:43 +05:30
|
|
|
});
|
2022-05-11 14:58:14 +05:30
|
|
|
}
|
|
|
|
|
2022-05-26 23:35:09 +05:30
|
|
|
get themeMapping(): Record<string, ThemeInformation> {
|
|
|
|
return this._themeMapping;
|
2022-05-11 14:58:14 +05:30
|
|
|
}
|
|
|
|
|
2022-06-05 20:49:05 +05:30
|
|
|
async getActiveTheme(): Promise<{themeName: string, themeVariant?: string}> {
|
|
|
|
const themeName = await this._platform.settingsStorage.getString("theme-name") ?? "Default";
|
|
|
|
const themeVariant = await this._platform.settingsStorage.getString("theme-variant");
|
|
|
|
return { themeName, themeVariant };
|
2022-05-27 14:23:38 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
getDefaultTheme(): string | undefined {
|
2022-06-02 15:05:43 +05:30
|
|
|
switch (this.preferredColorScheme) {
|
|
|
|
case ColorSchemePreference.Dark:
|
|
|
|
return this._platform.config["defaultTheme"].dark;
|
|
|
|
case ColorSchemePreference.Light:
|
|
|
|
return this._platform.config["defaultTheme"].light;
|
2022-05-18 18:56:28 +05:30
|
|
|
}
|
2022-05-11 14:58:14 +05:30
|
|
|
}
|
2022-05-26 23:35:09 +05:30
|
|
|
|
2022-06-05 20:49:05 +05:30
|
|
|
private _findThemeDetailsFromId(themeId: string): {themeName: string, cssLocation: string, variant?: string} | undefined {
|
|
|
|
for (const [themeName, themeData] of Object.entries(this._themeMapping)) {
|
2022-05-26 23:35:09 +05:30
|
|
|
if ("id" in themeData && themeData.id === themeId) {
|
2022-06-05 20:49:05 +05:30
|
|
|
return { themeName, cssLocation: themeData.cssLocation };
|
2022-05-26 23:35:09 +05:30
|
|
|
}
|
|
|
|
else if ("light" in themeData && themeData.light?.id === themeId) {
|
2022-06-05 20:49:05 +05:30
|
|
|
return { themeName, cssLocation: themeData.light.cssLocation, variant: "light" };
|
2022-05-26 23:35:09 +05:30
|
|
|
}
|
|
|
|
else if ("dark" in themeData && themeData.dark?.id === themeId) {
|
2022-06-05 20:49:05 +05:30
|
|
|
return { themeName, cssLocation: themeData.dark.cssLocation, variant: "dark" };
|
2022-05-26 23:35:09 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-02 15:05:43 +05:30
|
|
|
|
|
|
|
get preferredColorScheme(): ColorSchemePreference {
|
|
|
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
|
|
return ColorSchemePreference.Dark;
|
|
|
|
}
|
|
|
|
else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
|
|
return ColorSchemePreference.Light;
|
|
|
|
}
|
|
|
|
throw new Error("Cannot find preferred colorscheme!");
|
|
|
|
}
|
2022-05-11 14:58:14 +05:30
|
|
|
}
|