1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
11 BuiltInThemeConfig: "resource:///modules/BuiltInThemeConfig.sys.mjs",
14 const ColorwayL10n = new Localization(["browser/colorways.ftl"], true);
16 const kActiveThemePref = "extensions.activeThemeID";
17 const kRetainedThemesPref = "browser.theme.retainedExpiredThemes";
19 const ColorwayIntensityIdPostfixToL10nMap = [
20 ["-soft-colorway@mozilla.org", "colorway-intensity-soft"],
21 ["-balanced-colorway@mozilla.org", "colorway-intensity-balanced"],
22 ["-bold-colorway@mozilla.org", "colorway-intensity-bold"],
25 XPCOMUtils.defineLazyPreferenceGetter(
38 parsedVal = JSON.parse(val);
40 console.log(`${kRetainedThemesPref} has invalid value.`);
48 class _BuiltInThemes {
50 * The list of themes to be installed. This is exposed on the class so tests
51 * can set custom config files.
53 builtInThemeMap = lazy.BuiltInThemeConfig;
56 * @param {string} id An addon's id string.
58 * If `id` refers to a built-in theme, returns a path pointing to the
59 * theme's preview image. Null otherwise.
61 previewForBuiltInThemeId(id) {
62 let theme = this.builtInThemeMap.get(id);
64 return `${theme.path}preview.svg`;
71 * If the active theme is built-in, this function calls
72 * AddonManager.maybeInstallBuiltinAddon for that theme.
74 maybeInstallActiveBuiltInTheme() {
75 const activeThemeID = Services.prefs.getStringPref(
77 "default-theme@mozilla.org"
79 let activeBuiltInTheme = this.builtInThemeMap.get(activeThemeID);
81 if (activeBuiltInTheme) {
82 lazy.AddonManager.maybeInstallBuiltinAddon(
84 activeBuiltInTheme.version,
85 `resource://builtin-themes/${activeBuiltInTheme.path}`
91 * Ensures that all built-in themes are installed and expired themes are
94 async ensureBuiltInThemes() {
95 let installPromises = [];
96 installPromises.push(this._uninstallExpiredThemes());
98 const now = new Date();
99 for (let [id, themeInfo] of this.builtInThemeMap.entries()) {
102 lazy.retainedThemes.includes(id) ||
103 new Date(themeInfo.expiry) > now
105 installPromises.push(
106 lazy.AddonManager.maybeInstallBuiltinAddon(
115 await Promise.all(installPromises);
119 * This looks up the id in a Map rather than accessing a property on
120 * the addon itself. That makes calls to this function O(m) where m is the
121 * total number of built-in themes offered now or in the past. Since we
122 * are using a Map, calls are O(1) in the average case.
127 * Returns true if the theme is expired. False otherwise.
130 let themeInfo = this.builtInThemeMap.get(id);
131 return themeInfo?.expiry && new Date(themeInfo.expiry) < new Date();
138 * True if the theme with id `id` is both expired and retained. That is,
139 * the user has the ability to use it after its expiry date.
141 isRetainedExpiredTheme(id) {
142 return lazy.retainedThemes.includes(id) && this.themeIsExpired(id);
149 * True if the theme with id `id` is from the currently active theme.
154 Services.prefs.getStringPref(
156 "default-theme@mozilla.org"
162 * Uninstalls themes after they expire. If the expired theme is active, then
163 * it is not uninstalled. Instead, it is saved so that the user can use it
166 async _uninstallExpiredThemes() {
167 const activeThemeID = Services.prefs.getStringPref(
169 "default-theme@mozilla.org"
171 const now = new Date();
172 const expiredThemes = Array.from(this.builtInThemeMap.entries()).filter(
174 !!themeInfo.expiry &&
175 !lazy.retainedThemes.includes(id) &&
176 new Date(themeInfo.expiry) <= now
178 for (let [id] of expiredThemes) {
179 if (id == activeThemeID) {
180 let shouldRetain = true;
183 let addon = await lazy.AddonManager.getAddonByID(id);
185 // Only add the id to the retain themes pref if it is
186 // also a built-in themes (and don't if it was migrated
187 // xpi files installed in the user profile).
188 shouldRetain = addon.isBuiltinColorwayTheme;
192 `Failed to retrieve active theme AddonWrapper ${id}`,
198 this._retainLimitedTimeTheme(id);
202 let addon = await lazy.AddonManager.getAddonByID(id);
203 // Only uninstall the expired colorways theme if they are not
204 // migrated builtins (because on migrated to xpi files
205 // installed in the user profile they are also removed
206 // from the retainedExpiredThemes pref).
207 if (addon?.isBuiltinColorwayTheme) {
208 await addon.uninstall();
211 console.error(`Failed to uninstall expired theme ${id}`, e);
218 * Set a pref to ensure that the user can continue to use a specified theme
219 * past its expiry date.
222 * The ID of the theme to retain.
224 _retainLimitedTimeTheme(id) {
225 if (!lazy.retainedThemes.includes(id)) {
226 lazy.retainedThemes.push(id);
227 Services.prefs.setStringPref(
229 JSON.stringify(lazy.retainedThemes)
235 * Removes from the retained expired theme list colorways themes that have been
236 * migrated from the one installed in the built-in XPIProvider location
237 * to an AMO hosted xpi installed in the user profile XPIProvider location.
240 * The ID of the theme to remove from the retained themes list.
243 unretainMigratedColorwayTheme(id) {
244 if (lazy.retainedThemes.includes(id)) {
245 const retainedThemes = lazy.retainedThemes.filter(
246 retainedThemeId => retainedThemeId !== id
248 Services.prefs.setStringPref(
250 JSON.stringify(retainedThemes)
256 * Colorway collections are usually divided into and presented as "groups".
257 * A group either contains closely related colorways, e.g. stemming from the
258 * same base color but with different intensities (soft, balanced, and bold),
259 * or if the current collection doesn't have intensities, each colorway is
260 * their own group. Group name localization is optional.
262 * @param {string} colorwayId
263 * The ID of the colorway add-on.
265 * Localized colorway group name. null if there's no such name, in which
266 * case the caller should fall back on getting a name from the add-on API.
268 getLocalizedColorwayGroupName(colorwayId) {
269 return this._getColorwayString(colorwayId, "groupName");
273 * @param {string} colorwayId
274 * The ID of the colorway add-on.
276 * L10nId for intensity value of the colorway with the provided id, null if
279 getColorwayIntensityL10nId(colorwayId) {
280 const result = ColorwayIntensityIdPostfixToL10nMap.find(([postfix]) =>
281 colorwayId.endsWith(postfix)
283 return result ? result[1] : null;
287 * @param {string} colorwayId
288 * The ID of the colorway add-on.
290 * Localized description of the colorway with the provided id, null if
293 getLocalizedColorwayDescription(colorwayId) {
294 return this._getColorwayString(colorwayId, "description");
297 _getColorwayString(colorwayId, stringType) {
298 let l10nId = this.builtInThemeMap.get(colorwayId)?.l10nId?.[stringType];
301 [s] = ColorwayL10n.formatMessagesSync([
307 return s?.value || null;
311 export var BuiltInThemes = new _BuiltInThemes();