/* 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 { html, nothing } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; import { AddonManagerListenerHandler } from "../aboutaddons-utils.mjs"; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const PREF_ACTIVE_THEME_ID = "extensions.activeThemeID"; // NOTE: nova themes picker is enabled by default by Firefox Desktop prefs // and disabled by default by Gecko Toolkit level prefs. const PREF_NOVA_THEMES_PICKER = "browser.aboutaddons.novaThemesPickerEnabled"; const lazy = XPCOMUtils.declareLazy({ // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit getThemesList: "moz-src:///browser/themes/ThemesList.sys.mjs", AddonManager: "resource://gre/modules/AddonManager.sys.mjs", activeThemeID: { pref: PREF_ACTIVE_THEME_ID, default: "", }, novaThemesPickerEnabled: { pref: PREF_NOVA_THEMES_PICKER, default: false, }, }); export function isNovaThemesPickerEnabled() { return lazy.novaThemesPickerEnabled; } // Maps each curated theme id to its name's Fluent id. These follow the same // `extension-${idPrefix}-name` convention XPIDatabase.sys.mjs uses for // built-in themes (idPrefix being the addon id minus "@mozilla.org"). // // As a separate followups we plan to extend the theme fluent localization logic // provided by XPIDatabase.sys.mjs to make it handle also the Nova themes selection // hosted on AMO, so that both this picker and addon-card.mjs resolve the same // localized string. export const THEMES_L10N_MAP = new Map([ ["default-theme@mozilla.org", "extension-default-theme-name2"], ["nova-sun@mozilla.org", "extension-nova-sun-name"], ["nova-spark@mozilla.org", "extension-nova-spark-name"], ["nova-flame@mozilla.org", "extension-nova-flame-name"], ["nova-flare@mozilla.org", "extension-nova-flare-name"], ["nova-lavender@mozilla.org", "extension-nova-lavender-name"], ["nova-dusk@mozilla.org", "extension-nova-dusk-name"], ["nova-lagoon@mozilla.org", "extension-nova-lagoon-name"], ["nova-pine@mozilla.org", "extension-nova-pine-name"], ["nova-tide@mozilla.org", "extension-nova-tide-name"], ["nova-ash@mozilla.org", "extension-nova-ash-name"], ["nova-smoke@mozilla.org", "extension-nova-smoke-name"], ]); export function themeIdPrefix(themeId) { return themeId.replace("@mozilla.org", ""); } export class AboutaddonsThemesPicker extends MozLitElement { static properties = { currentThemeId: { type: String }, visibleRows: { type: Number }, _expanded: { state: true }, }; static queries = { pickerCard: "moz-card", themeCards: { all: ".theme-card" }, expandToggle: ".expand-toggle", }; #prefObserver = { observe: (_subject, _topic, prefName) => { if (prefName === PREF_NOVA_THEMES_PICKER) { this.requestUpdate(); } else { this.#updateCurrentThemeFromPref(); } }, QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), }; #addonListener = { onInstalled: addon => { if (addon.type === "theme" && this.#manager?.hasThemeId(addon.id)) { this.#installedAddonIds.add(addon.id); this.requestUpdate(); } }, onUninstalled: addon => { if (addon.type === "theme" && this.#manager?.hasThemeId(addon.id)) { this.#installedAddonIds.delete(addon.id); this.requestUpdate(); } }, }; #manager = null; #themes = []; #installedAddonIds = new Set(); constructor() { super(); this.currentThemeId = ""; this.visibleRows = 3; this._expanded = false; } connectedCallback() { super.connectedCallback(); Services.prefs.addObserver(PREF_ACTIVE_THEME_ID, this.#prefObserver); Services.prefs.addObserver(PREF_NOVA_THEMES_PICKER, this.#prefObserver); AddonManagerListenerHandler.addListener(this.#addonListener); this.#loadThemes(); } disconnectedCallback() { super.disconnectedCallback(); Services.prefs.removeObserver(PREF_ACTIVE_THEME_ID, this.#prefObserver); Services.prefs.removeObserver(PREF_NOVA_THEMES_PICKER, this.#prefObserver); AddonManagerListenerHandler.removeListener(this.#addonListener); } async #loadThemes() { if (!isNovaThemesPickerEnabled()) { // Do not attempt to load the Nova themes on builds that are opt-ing out // of this component explicitly (and it would render as `nothing` anyway). return; } const [manager, installedThemes] = await Promise.all([ lazy.getThemesList({ installSource: "about:addons" }), lazy.AddonManager.getAddonsByTypes(["theme"]), ]); this.#manager = manager; this.#themes = manager.getThemesInfo(); this.#installedAddonIds = new Set(installedThemes.map(a => a.id)); this.#updateCurrentThemeFromPref(); this.requestUpdate(); } #updateCurrentThemeFromPref() { const addonId = lazy.activeThemeID; this.currentThemeId = this.#manager?.hasThemeId(addonId) ? addonId : ""; } get #visibleThemes() { if (this._expanded) { return this.#themes; } return this.#themes.slice(0, this.visibleRows * 2); } get #needsExpandToggle() { return this.#themes.length > this.visibleRows * 2; } #isInstalled(themeId) { return ( this.#manager.isBuiltIn(themeId) || this.#installedAddonIds.has(themeId) ); } #isActive(themeId) { return themeId === this.currentThemeId; } #isButtonHidden(themeId) { return themeId === "default-theme@mozilla.org" && this.#isActive(themeId); } #buttonL10nId(themeId) { if (this.#isActive(themeId)) { return "aboutaddons-themes-picker-disable-button"; } if (this.#isInstalled(themeId)) { return "aboutaddons-themes-picker-enable-button"; } return "aboutaddons-themes-picker-install-button"; } async #onButtonClick(themeId) { // TODO(Bug 2054548): account for download/install failures of the Nova Themes // hosted on AMO await this.#manager.updateThemeState(themeId, !this.#isActive(themeId)); await this.#loadThemes(); } #toggleExpanded() { this._expanded = !this._expanded; } render() { if (!lazy.novaThemesPickerEnabled) { return nothing; } return html`
${this.#visibleThemes.map(({ id: themeId }) => { return html` `; })}
${this.#needsExpandToggle ? html` ` : nothing}
`; } } customElements.define("aboutaddons-themes-picker", AboutaddonsThemesPicker);