зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
65f6d086f4
Коммит
1d518958f6
|
@ -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);
|
||||
});
|
Загрузка…
Ссылка в новой задаче