Bug 1514316 - Basic HTML list view for about:addons behind a pref r=aswan,jaws,flod

This sets up a way to create HTML views for about:addons by hooking into the
existing UI. An entire view object must be replaced and this provides a basic
list view.

Differential Revision: https://phabricator.services.mozilla.com/D16277

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Striemer 2019-02-08 16:22:25 +00:00
Родитель 65f6d086f4
Коммит 1d518958f6
10 изменённых файлов: 441 добавлений и 1 удалений

Просмотреть файл

@ -5141,6 +5141,9 @@ pref("extensions.webextensions.enablePerformanceCounters", true);
// reset, so we reduce memory footprint.
pref("extensions.webextensions.performanceCountersMaxAge", 1000);
// The HTML about:addons page.
pref("extensions.htmlaboutaddons.enabled", false);
// Report Site Issue button
// Note that on enabling the button in other release channels, make sure to
// disable it in problematic tests, see disableNonReleaseActions() inside

Просмотреть файл

@ -324,3 +324,11 @@ shortcuts-card-collapse-button = Show Less
go-back-button =
.tooltiptext = Go back
## Add-on actions
remove-addon-button = Remove
disable-addon-button = Disable
enable-addon-button = Enable
addons-enabled-heading = Enabled
addons-disabled-heading = Disabled

Просмотреть файл

@ -0,0 +1,64 @@
:root {
--section-width: 664px;
}
#main {
margin-inline-start: 28px;
margin-bottom: 28px;
max-width: var(--section-width);
}
/* List sections */
.list-section-heading {
font-size: 17px;
font-weight: 600;
margin-block: 16px;
}
/* The margin is set on the main heading, no need on the first subheading. */
.list-section-heading:first-of-type {
margin-top: 0;
}
/* Add-on cards */
.addon.card {
margin-bottom: 16px;
display: flex;
}
.addon.card:hover {
box-shadow: var(--card-shadow);
}
.card-heading-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
margin-inline-end: 16px;
}
.card-contents {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.card-actions {
flex-shrink: 0;
}
.addon-name {
font-size: 16px;
font-weight: 600;
color: var(--grey-90);
line-height: 1;
}
.addon-description {
font-size: 14px;
line-height: 20px;
color: var(--grey-60);
font-weight: 400;
}

Просмотреть файл

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://mozapps/content/extensions/aboutaddons.css" type="text/css"/>
<link rel="localization" href="branding/brand.ftl"/>
<link rel="localization" href="toolkit/about/aboutAddons.ftl"/>
<script type="application/javascript" src="chrome://mozapps/content/extensions/aboutaddons.js"></script>
</head>
<body>
<div id="main">
</div>
<template name="list">
<div class="list-section" type="enabled">
<h2 class="list-section-heading" data-l10n-id="addons-enabled-heading"></h2>
</div>
<div class="list-section" type="disabled">
<h2 class="list-section-heading" data-l10n-id="addons-disabled-heading"></h2>
</div>
</template>
<template name="card">
<div class="card addon">
<img class="card-heading-icon addon-icon"/>
<div class="card-contents">
<span class="addon-name"></span>
<span class="addon-description"></span>
</div>
<div class="card-actions">
<button action="toggle-disabled"></button><button action="remove" data-l10n-id="remove-addon-button"></button>
</div>
</div>
</template>
</body>
</html>

Просмотреть файл

@ -0,0 +1,163 @@
/* 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/. */
/* exported initialize, hide, show */
"use strict";
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
});
const PLUGIN_ICON_URL = "chrome://global/skin/plugins/pluginGeneric.svg";
let _templates = {};
/**
* Import a template from the main document.
*/
function importTemplate(name) {
if (!_templates.hasOwnProperty(name)) {
_templates[name] = document.querySelector(`template[name="${name}"]`);
}
let template = _templates[name];
if (template) {
return document.importNode(template.content, true);
}
throw new Error(`Unknown template: ${name}`);
}
class ListView {
constructor({param, root}) {
this.type = param;
this.root = root;
}
async getAddons() {
let addons = await AddonManager.getAddonsByTypes([this.type]);
addons = addons.filter(addon => !addon.isSystem);
// Sort by name.
addons.sort((a, b) => a.name.localeCompare(b.name));
return addons;
}
setEnableLabel(button, disabled) {
if (disabled) {
document.l10n.setAttributes(button, "enable-addon-button");
} else {
document.l10n.setAttributes(button, "disable-addon-button");
}
}
updateCard(card, addon) {
let icon;
if (addon.type == "plugin") {
icon = PLUGIN_ICON_URL;
} else {
icon = AddonManager.getPreferredIconURL(addon, 32, window);
}
card.querySelector(".addon-icon").src = icon;
card.querySelector(".addon-name").textContent = addon.name;
card.querySelector(".addon-description").textContent = addon.description;
this.setEnableLabel(
card.querySelector('[action="toggle-disabled"]'), addon.userDisabled);
}
renderAddonCard(addon) {
let card = importTemplate("card").firstElementChild;
card.setAttribute("addon-id", addon.id);
// Set the contents.
this.updateCard(card, addon);
card.addEventListener("click", async (e) => {
switch (e.target.getAttribute("action")) {
case "toggle-disabled":
if (addon.userDisabled) {
await addon.enable();
} else {
await addon.disable();
}
this.render();
break;
case "remove":
await addon.uninstall();
this.render();
break;
}
});
return card;
}
renderSections({disabledFrag, enabledFrag}) {
let viewFrag = importTemplate("list");
let enabledSection = viewFrag.querySelector('[type="enabled"]');
let disabledSection = viewFrag.querySelector('[type="disabled"]');
// Set the cards or remove the section if empty.
let setSection = (section, cards) => {
if (cards.children.length > 0) {
section.appendChild(cards);
} else {
section.remove();
}
};
setSection(enabledSection, enabledFrag);
setSection(disabledSection, disabledFrag);
return viewFrag;
}
async render() {
let addons = await this.getAddons();
let disabledFrag = document.createDocumentFragment();
let enabledFrag = document.createDocumentFragment();
// Populate fragments for the enabled and disabled sections.
for (let addon of addons) {
let card = this.renderAddonCard(addon);
if (addon.userDisabled) {
disabledFrag.appendChild(card);
} else {
enabledFrag.appendChild(card);
}
}
// Put the sections into the main template.
let frag = this.renderSections({disabledFrag, enabledFrag});
this.root.textContent = "";
this.root.appendChild(frag);
}
}
// Generic view management.
let root = null;
/**
* Called from extensions.js once, when about:addons is loading.
*/
function initialize() {
root = document.getElementById("main");
}
/**
* Called from extensions.js to load a view. The view's render method should
* resolve once the view has been updated to conform with other about:addons
* views.
*/
async function show(type, param) {
if (type == "list") {
await new ListView({param, root}).render();
}
}
function hide() {
root.textContent = "";
}

Просмотреть файл

@ -39,6 +39,8 @@ XPCOMUtils.defineLazyPreferenceGetter(this, "allowPrivateBrowsingByDefault",
XPCOMUtils.defineLazyPreferenceGetter(this, "SUPPORT_URL", "app.support.baseURL",
"", null, val => Services.urlFormatter.formatURL(val));
XPCOMUtils.defineLazyPreferenceGetter(this, "useHtmlViews",
"extensions.htmlaboutaddons.enabled");
const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
@ -705,12 +707,17 @@ var gViewController = {
this.backButton = document.getElementById("go-back");
this.viewObjects.discover = gDiscoverView;
this.viewObjects.list = gListView;
this.viewObjects.legacy = gLegacyView;
this.viewObjects.detail = gDetailView;
this.viewObjects.updates = gUpdatesView;
this.viewObjects.shortcuts = gShortcutsView;
if (useHtmlViews) {
this.viewObjects.list = htmlView("list");
} else {
this.viewObjects.list = gListView;
}
for (let type in this.viewObjects) {
let view = this.viewObjects[type];
view.initialize();
@ -3710,3 +3717,46 @@ var gBrowser = {
updatePositionTask.arm();
}, true);
}
// View wrappers for the HTML version of about:addons. These delegate to an
// HTML browser that renders the actual views.
let htmlBrowser;
let htmlBrowserLoaded;
function getHtmlBrowser() {
if (!htmlBrowser) {
htmlBrowser = document.getElementById("html-view-browser");
htmlBrowser.loadURI("chrome://mozapps/content/extensions/aboutaddons.html", {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
htmlBrowserLoaded = new Promise(
resolve => htmlBrowser.addEventListener("load", resolve, {once: true})
).then(() => htmlBrowser.contentWindow.initialize());
}
return htmlBrowser;
}
function htmlView(type) {
return {
node: null,
isRoot: true,
initialize() {
this.node = getHtmlBrowser();
},
async show(param, request, state, refresh) {
await htmlBrowserLoaded;
await this.node.contentWindow.show(type, param);
gViewController.notifyViewChanged();
},
async hide() {
await htmlBrowserLoaded;
return this.node.contentWindow.hide();
},
getSelectedAddon() {
return null;
},
};
}

Просмотреть файл

@ -648,6 +648,8 @@
<spacer flex="1"/>
</hbox>
</scrollbox>
<browser id="html-view-browser" type="content" flex="1" disablehistory="true"/>
</deck>
</vbox>
</deck>

Просмотреть файл

@ -21,4 +21,7 @@ toolkit.jar:
content/mozapps/extensions/pluginPrefs.xul (content/pluginPrefs.xul)
content/mozapps/extensions/pluginPrefs.js (content/pluginPrefs.js)
content/mozapps/extensions/OpenH264-license.txt (content/OpenH264-license.txt)
content/mozapps/extensions/aboutaddons.html (content/aboutaddons.html)
content/mozapps/extensions/aboutaddons.js (content/aboutaddons.js)
content/mozapps/extensions/aboutaddons.css (content/aboutaddons.css)
#endif

Просмотреть файл

@ -113,3 +113,4 @@ tags = webextensions
skip-if = os == 'linux' || (os == 'mac' && debug) # bug 1483347
[browser_webext_options_addon_reload.js]
tags = webextensions
[browser_html_list_view.js]

Просмотреть файл

@ -0,0 +1,107 @@
let gManagerWindow;
let gCategoryUtilities;
async function loadInitialView(type) {
gManagerWindow = await open_manager(null);
gCategoryUtilities = new CategoryUtilities(gManagerWindow);
await gCategoryUtilities.openType(type);
let browser = gManagerWindow.document.getElementById("html-view-browser");
return browser.contentWindow;
}
function closeView() {
return close_manager(gManagerWindow);
}
function getSection(doc, type) {
return doc.querySelector(`.list-section[type="${type}"]`);
}
add_task(async function enableHtmlViews() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.htmlaboutaddons.enabled", true]],
});
});
add_task(async function testExtensionList() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
name: "Test extension",
applications: {gecko: {id: "test@mochi.test"}},
icons: {
32: "test-icon.png",
},
},
useAddonManager: "temporary",
});
await extension.startup();
let addon = await AddonManager.getAddonByID("test@mochi.test");
ok(addon, "The add-on can be found");
let win = await loadInitialView("extension");
let doc = win.document;
// There shouldn't be any disabled extensions.
is(getSection(doc, "disabled"), null, "The disabled section is hidden");
// The loaded extension should be in the enabled list.
let enabledSection = getSection(doc, "enabled");
let card = enabledSection.querySelector('[addon-id="test@mochi.test"]');
ok(card, "The card is in the enabled section");
// Check the properties of the card.
is(card.querySelector(".addon-name").textContent, "Test extension",
"The name is set");
let icon = card.querySelector(".addon-icon");
ok(icon.src.endsWith("/test-icon.png"), "The icon is set");
// Disable the extension.
let disableButton = card.querySelector('[action="toggle-disabled"]');
is(doc.l10n.getAttributes(disableButton).id, "disable-addon-button",
"The button has the disable label");
let disabled = TestUtils.waitForCondition(() => getSection(doc, "disabled"));
disableButton.click();
await disabled;
// The disable button is now enable.
let disabledSection = getSection(doc, "disabled");
card = disabledSection.querySelector('[addon-id="test@mochi.test"]');
let enableButton = card.querySelector('[action="toggle-disabled"]');
is(doc.l10n.getAttributes(enableButton).id, "enable-addon-button",
"The button has the enable label");
// Remove the add-on.
let removeButton = card.querySelector('[action="remove"]');
is(doc.l10n.getAttributes(removeButton).id, "remove-addon-button",
"The button has the remove label");
let removed = TestUtils.waitForCondition(() => !getSection(doc, "disabled"));
removeButton.click();
await removed;
addon = await AddonManager.getAddonByID("test@mochi.test");
ok(!addon, "The addon is not longer found");
await extension.unload();
await closeView(win);
});
add_task(async function testPluginIcons() {
const pluginIconUrl = "chrome://global/skin/plugins/pluginGeneric.svg";
let win = await loadInitialView("plugin");
let doc = win.document;
// Check that the icons are set to the plugin icon.
let icons = doc.querySelectorAll(".card-heading-icon");
ok(icons.length > 0, "There are some plugins listed");
for (let icon of icons) {
is(icon.src, pluginIconUrl, "Plugins use the plugin icon");
}
await closeView(win);
});