From ac7be0c7a1f66c549e634f082f7a858204d3dbaf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 15 Jul 2022 15:05:50 +0530 Subject: [PATCH] WIP --- src/platform/web/DerivedVariables.ts | 54 +++++++++++++++ src/platform/web/IconColorizer.ts | 79 +++++++++++++++++++++ src/platform/web/ThemeBuilder.ts | 100 +++++++++------------------ src/platform/web/ThemeLoader.ts | 13 ++-- 4 files changed, 174 insertions(+), 72 deletions(-) create mode 100644 src/platform/web/DerivedVariables.ts create mode 100644 src/platform/web/IconColorizer.ts diff --git a/src/platform/web/DerivedVariables.ts b/src/platform/web/DerivedVariables.ts new file mode 100644 index 00000000..4e3a8b46 --- /dev/null +++ b/src/platform/web/DerivedVariables.ts @@ -0,0 +1,54 @@ +/* +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. +*/ +import {derive} from "../../../scripts/postcss/color.mjs"; + +export class DerivedVariables { + private _baseVariables: Record; + private _variablesToDerive: string[] + private _isDark: boolean + + constructor(baseVariables: Record, variablesToDerive: string[], isDark: boolean) { + this._baseVariables = baseVariables; + this._variablesToDerive = variablesToDerive; + this._isDark = isDark; + } + + toVariables(): Record { + const aliases: any = {}; + const resolvedVariables: any = {}; + const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; + for (const variable of this._variablesToDerive) { + // If this is an alias, store it for processing later + const [alias, value] = variable.split("="); + if (value) { + aliases[alias] = value; + continue; + } + // Resolve derived variables + const matches = variable.match(RE_VARIABLE_VALUE); + if (matches) { + const [, baseVariable, operation, argument] = matches; + const value = this._baseVariables[baseVariable]; + const resolvedValue = derive(value, operation, argument, this._isDark); + resolvedVariables[variable] = resolvedValue; + } + } + for (const [alias, variable] of Object.entries(aliases) as any) { + resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable]; + } + return resolvedVariables; + } +} diff --git a/src/platform/web/IconColorizer.ts b/src/platform/web/IconColorizer.ts new file mode 100644 index 00000000..81644603 --- /dev/null +++ b/src/platform/web/IconColorizer.ts @@ -0,0 +1,79 @@ +/* +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. +*/ +import type {Platform} from "./Platform.js"; +import {getColoredSvgString} from "../../../scripts/postcss/svg-colorizer.js"; + +type ParsedStructure = { + [variableName: string]: { + svg: Promise<{ status: number; body: string }>; + primary: string | null; + secondary: string | null; + }; +}; + +export class IconColorizer { + private _iconVariables: Record; + private _resolvedVariables: Record; + private _manifestLocation: string; + private _platform: Platform; + + constructor(platform: Platform, iconVariables: Record, resolvedVariables: Record, manifestLocation: string) { + this._platform = platform; + this._iconVariables = iconVariables; + this._resolvedVariables = resolvedVariables; + this._manifestLocation = manifestLocation; + } + + async toVariables(): Promise> { + const { parsedStructure, promises } = await this._fetchAndParseIcons(); + await Promise.all(promises); + return this._produceColoredIconVariables(parsedStructure); + } + + private async _fetchAndParseIcons(): Promise<{ parsedStructure: ParsedStructure, promises: any[] }> { + const promises: any[] = []; + const parsedStructure: ParsedStructure = {}; + for (const [variable, url] of Object.entries(this._iconVariables)) { + const urlObject = new URL(`https://${url}`); + const pathWithoutQueryParams = urlObject.hostname; + const relativePath = new URL(pathWithoutQueryParams, new URL(this._manifestLocation, window.location.origin)); + const responsePromise = this._platform.request(relativePath, { method: "GET", format: "text", cache: true, }).response() + promises.push(responsePromise); + const searchParams = urlObject.searchParams; + parsedStructure[variable] = { + svg: responsePromise, + primary: searchParams.get("primary"), + secondary: searchParams.get("secondary") + }; + } + return { parsedStructure, promises }; + } + + private async _produceColoredIconVariables(parsedStructure: ParsedStructure): Promise> { + let coloredVariables: Record = {}; + for (const [variable, { svg, primary, secondary }] of Object.entries(parsedStructure)) { + const { body: svgCode } = await svg; + if (!primary) { + throw new Error(`Primary color variable ${primary} not in list of variables!`); + } + const primaryColor = this._resolvedVariables[primary], secondaryColor = this._resolvedVariables[secondary!]; + const coloredSvgCode = getColoredSvgString(svgCode, primaryColor, secondaryColor); + const dataURI = `url('data:image/svg+xml;utf8,${encodeURIComponent(coloredSvgCode)}')`; + coloredVariables[variable] = dataURI; + } + return coloredVariables; + } +} diff --git a/src/platform/web/ThemeBuilder.ts b/src/platform/web/ThemeBuilder.ts index b8e7274b..f05a24b1 100644 --- a/src/platform/web/ThemeBuilder.ts +++ b/src/platform/web/ThemeBuilder.ts @@ -14,72 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ import type {ThemeInformation} from "./ThemeLoader"; +import type {Platform} from "./Platform.js"; import {ColorSchemePreference} from "./ThemeLoader"; -import {derive} from "../../../scripts/postcss/color.mjs"; +import {IconColorizer} from "./IconColorizer"; +import {DerivedVariables} from "./DerivedVariables"; export class ThemeBuilder { // todo: replace any with manifest type when PR is merged private _idToManifest: Map; private _themeMapping: Record = {}; - private _themeToVariables: Record = {}; private _preferredColorScheme?: ColorSchemePreference; + private _platform: Platform; + private _injectedVariables?: Record; - constructor(manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { + constructor(platform: Platform, manifestMap: Map, preferredColorScheme?: ColorSchemePreference) { this._idToManifest = manifestMap; this._preferredColorScheme = preferredColorScheme; + this._platform = platform; } - populateDerivedTheme(manifest) { + async populateDerivedTheme(manifest) { const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends); const runtimeCssLocation = baseManifest.source?.["runtime-asset"]; const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href; const derivedVariables = baseManifest.source?.["derived-variables"]; + const icons = baseManifest.source?.["icon"]; const themeName = manifest.name; let defaultDarkVariant: any = {}, defaultLightVariant: any = {}; for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) { - const themeId = `${manifest.id}-${variant}`; - const { name: variantName, default: isDefault, dark, variables } = variantDetails; - const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark); - Object.assign(variables, resolvedVariables); - 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; - defaultVariant.variables = variables; + try { + const themeId = `${manifest.id}-${variant}`; + const { name: variantName, default: isDefault, dark, variables } = variantDetails; + const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables(); + Object.assign(variables, resolvedVariables); + const iconVariables = await new IconColorizer(this._platform, icons, variables, location).toVariables(); + Object.assign(variables, resolvedVariables, iconVariables); + const themeDisplayName = `${themeName} ${variantName}`; + if (isDefault) { + const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant; + Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables }); + continue; + } + this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, }; + } + catch (e) { + console.error(e); continue; } - // Non-default variants are keyed in themeMapping with "theme_name variant_name" - // eg: "Element Dark" - this._themeMapping[themeDisplayName] = { - cssLocation, - id: themeId, - variables: variables, - }; } 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. - */ const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant; this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant }; } 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 }; } @@ -94,38 +81,17 @@ export class ThemeBuilder { for (const [variable, value] of Object.entries(variables)) { root.style.setProperty(`--${variable}`, value); } + this._injectedVariables = variables; } - removeCSSVariables(variables: string[]) { + removePreviousCSSVariables() { + if (!this._injectedVariables) { + return; + } const root = document.documentElement; - for (const variable of variables) { + for (const variable of Object.keys(this._injectedVariables)) { root.style.removeProperty(`--${variable}`); } - } - - deriveVariables(variables: Record, derivedVariables: string[], isDark: boolean) { - const aliases: any = {}; - const resolvedVariables: any = {}; - const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/; - for (const variable of derivedVariables) { - // If this is an alias, store it for processing later - const [alias, value] = variable.split("="); - if (value) { - aliases[alias] = value; - continue; - } - // Resolve derived variables - const matches = variable.match(RE_VARIABLE_VALUE); - if (matches) { - const [, baseVariable, operation, argument] = matches; - const value = variables[baseVariable]; - const resolvedValue = derive(value, operation, argument, isDark); - resolvedVariables[variable] = resolvedValue; - } - } - for (const [alias, variable] of Object.entries(aliases) as any) { - resolvedVariables[alias] = variables[variable] ?? resolvedVariables[variable]; - } - return resolvedVariables; + this._injectedVariables = undefined; } } diff --git a/src/platform/web/ThemeLoader.ts b/src/platform/web/ThemeLoader.ts index 9806dd10..b1e13161 100644 --- a/src/platform/web/ThemeLoader.ts +++ b/src/platform/web/ThemeLoader.ts @@ -60,17 +60,17 @@ export class ThemeLoader { manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response()) ); results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] })); - this._themeBuilder = new ThemeBuilder(idToManifest, this.preferredColorScheme); - results.forEach(({ body }, i) => { + this._themeBuilder = new ThemeBuilder(this._platform, idToManifest, this.preferredColorScheme); + for (let i = 0; i < results.length; ++i) { + const { body } = results[i]; if (body.extends) { - this._themeBuilder.populateDerivedTheme(body); + await this._themeBuilder.populateDerivedTheme(body); } else { this._populateThemeMap(body, manifestLocations[i], log); } - }); + } Object.assign(this._themeMapping, this._themeBuilder.themeMapping); - console.log("derived theme mapping", this._themeBuilder.themeMapping); }); } @@ -170,6 +170,9 @@ export class ThemeLoader { if (variables) { this._themeBuilder.injectCSSVariables(variables); } + else { + this._themeBuilder.removePreviousCSSVariables(); + } this._platform.settingsStorage.setString("theme-name", themeName); if (themeVariant) { this._platform.settingsStorage.setString("theme-variant", themeVariant);