diff options
Diffstat (limited to 'browser/themes/BuiltInThemes.sys.mjs')
-rw-r--r-- | browser/themes/BuiltInThemes.sys.mjs | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/browser/themes/BuiltInThemes.sys.mjs b/browser/themes/BuiltInThemes.sys.mjs new file mode 100644 index 0000000000..c2d5dd7a18 --- /dev/null +++ b/browser/themes/BuiltInThemes.sys.mjs @@ -0,0 +1,311 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BuiltInThemeConfig: "resource:///modules/BuiltInThemeConfig.sys.mjs", +}); + +const ColorwayL10n = new Localization(["browser/colorways.ftl"], true); + +const kActiveThemePref = "extensions.activeThemeID"; +const kRetainedThemesPref = "browser.theme.retainedExpiredThemes"; + +const ColorwayIntensityIdPostfixToL10nMap = [ + ["-soft-colorway@mozilla.org", "colorway-intensity-soft"], + ["-balanced-colorway@mozilla.org", "colorway-intensity-balanced"], + ["-bold-colorway@mozilla.org", "colorway-intensity-bold"], +]; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "retainedThemes", + kRetainedThemesPref, + null, + null, + val => { + if (!val) { + return []; + } + + let parsedVal; + try { + parsedVal = JSON.parse(val); + } catch (ex) { + console.log(`${kRetainedThemesPref} has invalid value.`); + return []; + } + + return parsedVal; + } +); + +class _BuiltInThemes { + /** + * The list of themes to be installed. This is exposed on the class so tests + * can set custom config files. + */ + builtInThemeMap = lazy.BuiltInThemeConfig; + + /** + * @param {string} id An addon's id string. + * @returns {string} + * If `id` refers to a built-in theme, returns a path pointing to the + * theme's preview image. Null otherwise. + */ + previewForBuiltInThemeId(id) { + let theme = this.builtInThemeMap.get(id); + if (theme) { + return `${theme.path}preview.svg`; + } + + return null; + } + + /** + * If the active theme is built-in, this function calls + * AddonManager.maybeInstallBuiltinAddon for that theme. + */ + maybeInstallActiveBuiltInTheme() { + const activeThemeID = Services.prefs.getStringPref( + kActiveThemePref, + "default-theme@mozilla.org" + ); + let activeBuiltInTheme = this.builtInThemeMap.get(activeThemeID); + + if (activeBuiltInTheme) { + lazy.AddonManager.maybeInstallBuiltinAddon( + activeThemeID, + activeBuiltInTheme.version, + `resource://builtin-themes/${activeBuiltInTheme.path}` + ); + } + } + + /** + * Ensures that all built-in themes are installed and expired themes are + * uninstalled. + */ + async ensureBuiltInThemes() { + let installPromises = []; + installPromises.push(this._uninstallExpiredThemes()); + + const now = new Date(); + for (let [id, themeInfo] of this.builtInThemeMap.entries()) { + if ( + !themeInfo.expiry || + lazy.retainedThemes.includes(id) || + new Date(themeInfo.expiry) > now + ) { + installPromises.push( + lazy.AddonManager.maybeInstallBuiltinAddon( + id, + themeInfo.version, + themeInfo.path + ) + ); + } + } + + await Promise.all(installPromises); + } + + /** + * This looks up the id in a Map rather than accessing a property on + * the addon itself. That makes calls to this function O(m) where m is the + * total number of built-in themes offered now or in the past. Since we + * are using a Map, calls are O(1) in the average case. + * + * @param {string} id + * A theme's ID. + * @returns {boolean} + * Returns true if the theme is expired. False otherwise. + */ + themeIsExpired(id) { + let themeInfo = this.builtInThemeMap.get(id); + return themeInfo?.expiry && new Date(themeInfo.expiry) < new Date(); + } + + /** + * @param {string} id + * The theme's id. + * @returns {boolean} + * True if the theme with id `id` is both expired and retained. That is, + * the user has the ability to use it after its expiry date. + */ + isRetainedExpiredTheme(id) { + return lazy.retainedThemes.includes(id) && this.themeIsExpired(id); + } + + /** + * @param {string} id + * The theme's id. + * @returns {boolean} + * True if the theme with id `id` is from the currently active theme. + */ + isActiveTheme(id) { + return ( + id === + Services.prefs.getStringPref( + kActiveThemePref, + "default-theme@mozilla.org" + ) + ); + } + + /** + * Uninstalls themes after they expire. If the expired theme is active, then + * it is not uninstalled. Instead, it is saved so that the user can use it + * indefinitely. + */ + async _uninstallExpiredThemes() { + const activeThemeID = Services.prefs.getStringPref( + kActiveThemePref, + "default-theme@mozilla.org" + ); + const now = new Date(); + const expiredThemes = Array.from(this.builtInThemeMap.entries()).filter( + ([id, themeInfo]) => + !!themeInfo.expiry && + !lazy.retainedThemes.includes(id) && + new Date(themeInfo.expiry) <= now + ); + for (let [id] of expiredThemes) { + if (id == activeThemeID) { + let shouldRetain = true; + + try { + let addon = await lazy.AddonManager.getAddonByID(id); + if (addon) { + // Only add the id to the retain themes pref if it is + // also a built-in themes (and don't if it was migrated + // xpi files installed in the user profile). + shouldRetain = addon.isBuiltinColorwayTheme; + } + } catch (e) { + console.error( + `Failed to retrieve active theme AddonWrapper ${id}`, + e + ); + } + + if (shouldRetain) { + this._retainLimitedTimeTheme(id); + } + } else { + try { + let addon = await lazy.AddonManager.getAddonByID(id); + // Only uninstall the expired colorways theme if they are not + // migrated builtins (because on migrated to xpi files + // installed in the user profile they are also removed + // from the retainedExpiredThemes pref). + if (addon?.isBuiltinColorwayTheme) { + await addon.uninstall(); + } + } catch (e) { + console.error(`Failed to uninstall expired theme ${id}`, e); + } + } + } + } + + /** + * Set a pref to ensure that the user can continue to use a specified theme + * past its expiry date. + * + * @param {string} id + * The ID of the theme to retain. + */ + _retainLimitedTimeTheme(id) { + if (!lazy.retainedThemes.includes(id)) { + lazy.retainedThemes.push(id); + Services.prefs.setStringPref( + kRetainedThemesPref, + JSON.stringify(lazy.retainedThemes) + ); + } + } + + /** + * Removes from the retained expired theme list colorways themes that have been + * migrated from the one installed in the built-in XPIProvider location + * to an AMO hosted xpi installed in the user profile XPIProvider location. + * + * @param {string} id + * The ID of the theme to remove from the retained themes list. + */ + + unretainMigratedColorwayTheme(id) { + if (lazy.retainedThemes.includes(id)) { + const retainedThemes = lazy.retainedThemes.filter( + retainedThemeId => retainedThemeId !== id + ); + Services.prefs.setStringPref( + kRetainedThemesPref, + JSON.stringify(retainedThemes) + ); + } + } + + /** + * Colorway collections are usually divided into and presented as "groups". + * A group either contains closely related colorways, e.g. stemming from the + * same base color but with different intensities (soft, balanced, and bold), + * or if the current collection doesn't have intensities, each colorway is + * their own group. Group name localization is optional. + * + * @param {string} colorwayId + * The ID of the colorway add-on. + * @returns {string} + * Localized colorway group name. null if there's no such name, in which + * case the caller should fall back on getting a name from the add-on API. + */ + getLocalizedColorwayGroupName(colorwayId) { + return this._getColorwayString(colorwayId, "groupName"); + } + + /** + * @param {string} colorwayId + * The ID of the colorway add-on. + * @returns {string} + * L10nId for intensity value of the colorway with the provided id, null if + * there's none. + */ + getColorwayIntensityL10nId(colorwayId) { + const result = ColorwayIntensityIdPostfixToL10nMap.find( + ([postfix, l10nId]) => colorwayId.endsWith(postfix) + ); + return result ? result[1] : null; + } + + /** + * @param {string} colorwayId + * The ID of the colorway add-on. + * @returns {string} + * Localized description of the colorway with the provided id, null if + * there's none. + */ + getLocalizedColorwayDescription(colorwayId) { + return this._getColorwayString(colorwayId, "description"); + } + + _getColorwayString(colorwayId, stringType) { + let l10nId = this.builtInThemeMap.get(colorwayId)?.l10nId?.[stringType]; + let s; + if (l10nId) { + [s] = ColorwayL10n.formatMessagesSync([ + { + id: l10nId, + }, + ]); + } + return s?.value || null; + } +} + +export var BuiltInThemes = new _BuiltInThemes(); |