summaryrefslogtreecommitdiffstats
path: root/browser/themes/BuiltInThemes.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/themes/BuiltInThemes.sys.mjs')
-rw-r--r--browser/themes/BuiltInThemes.sys.mjs311
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();