mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-03 17:58:55 +02:00
Bug 1965529 - Add saving custom image functionality to profiles avatar picker. r=profiles-reviewers,fluent-reviewers,bolsson,jhirsch
Differential Revision: https://phabricator.services.mozilla.com/D250926
This commit is contained in:
parent
6fa0204b44
commit
e87c1712fb
20 changed files with 642 additions and 78 deletions
|
|
@ -104,11 +104,9 @@ var gProfiles = {
|
||||||
"label",
|
"label",
|
||||||
SelectableProfileService.currentProfile.name
|
SelectableProfileService.currentProfile.name
|
||||||
);
|
);
|
||||||
let avatar = SelectableProfileService.currentProfile.avatar;
|
let avatarURL =
|
||||||
profilesButton.setAttribute(
|
await SelectableProfileService.currentProfile.getAvatarURL(16);
|
||||||
"image",
|
profilesButton.setAttribute("image", `${avatarURL}`);
|
||||||
`chrome://browser/content/profiles/assets/16_${avatar}.svg`
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -131,7 +129,7 @@ var gProfiles = {
|
||||||
menuitem.setAttribute("label", profile.name);
|
menuitem.setAttribute("label", profile.name);
|
||||||
menuitem.style.setProperty("--menu-profiles-theme-bg", themeBg);
|
menuitem.style.setProperty("--menu-profiles-theme-bg", themeBg);
|
||||||
menuitem.style.setProperty("--menu-profiles-theme-fg", themeFg);
|
menuitem.style.setProperty("--menu-profiles-theme-fg", themeFg);
|
||||||
menuitem.style.listStyleImage = `url(chrome://browser/content/profiles/assets/48_${profile.avatar}.svg)`;
|
menuitem.style.listStyleImage = `url(${await profile.getAvatarURL(48)})`;
|
||||||
menuitem.classList.add("menuitem-iconic", "menuitem-iconic-profile");
|
menuitem.classList.add("menuitem-iconic", "menuitem-iconic-profile");
|
||||||
|
|
||||||
if (profile.id === currentProfile.id) {
|
if (profile.id === currentProfile.id) {
|
||||||
|
|
@ -325,8 +323,7 @@ var gProfiles = {
|
||||||
themeFg
|
themeFg
|
||||||
);
|
);
|
||||||
|
|
||||||
let avatar = currentProfile.avatar;
|
profileIconEl.style.listStyleImage = `url(${await currentProfile.getAvatarURL(80)})`;
|
||||||
profileIconEl.style.listStyleImage = `url("chrome://browser/content/profiles/assets/80_${avatar}.svg")`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let subtitle = PanelMultiView.getViewNode(document, "profiles-subtitle");
|
let subtitle = PanelMultiView.getViewNode(document, "profiles-subtitle");
|
||||||
|
|
@ -348,10 +345,7 @@ var gProfiles = {
|
||||||
let { themeFg, themeBg } = profile.theme;
|
let { themeFg, themeBg } = profile.theme;
|
||||||
button.style.setProperty("--appmenu-profiles-theme-bg", themeBg);
|
button.style.setProperty("--appmenu-profiles-theme-bg", themeBg);
|
||||||
button.style.setProperty("--appmenu-profiles-theme-fg", themeFg);
|
button.style.setProperty("--appmenu-profiles-theme-fg", themeFg);
|
||||||
button.setAttribute(
|
button.setAttribute("image", `${await profile.getAvatarURL(16)}`);
|
||||||
"image",
|
|
||||||
`chrome://browser/content/profiles/assets/16_${profile.avatar}.svg`
|
|
||||||
);
|
|
||||||
|
|
||||||
profilesList.appendChild(button);
|
profilesList.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,8 +183,8 @@ export class ProfilesParent extends JSWindowActorParent {
|
||||||
let profiles = await SelectableProfileService.getAllProfiles();
|
let profiles = await SelectableProfileService.getAllProfiles();
|
||||||
let themes = await this.getSafeForContentThemes();
|
let themes = await this.getSafeForContentThemes();
|
||||||
return {
|
return {
|
||||||
currentProfile: currentProfile.toObject(),
|
currentProfile: await currentProfile.toContentSafeObject(),
|
||||||
profiles: profiles.map(p => p.toObject()),
|
profiles: await Promise.all(profiles.map(p => p.toContentSafeObject())),
|
||||||
themes,
|
themes,
|
||||||
isInAutomation: Cu.isInAutomation,
|
isInAutomation: Cu.isInAutomation,
|
||||||
};
|
};
|
||||||
|
|
@ -282,7 +282,8 @@ export class ProfilesParent extends JSWindowActorParent {
|
||||||
// Make sure SelectableProfileService is initialized
|
// Make sure SelectableProfileService is initialized
|
||||||
await SelectableProfileService.init();
|
await SelectableProfileService.init();
|
||||||
Glean.profilesDelete.displayed.record();
|
Glean.profilesDelete.displayed.record();
|
||||||
let profileObj = SelectableProfileService.currentProfile.toObject();
|
let profileObj =
|
||||||
|
await SelectableProfileService.currentProfile.toContentSafeObject();
|
||||||
let windowCount = lazy.EveryWindow.readyWindows.length;
|
let windowCount = lazy.EveryWindow.readyWindows.length;
|
||||||
let tabCount = lazy.EveryWindow.readyWindows
|
let tabCount = lazy.EveryWindow.readyWindows
|
||||||
.flatMap(win => win.gBrowser.openTabs.length)
|
.flatMap(win => win.gBrowser.openTabs.length)
|
||||||
|
|
@ -326,14 +327,20 @@ export class ProfilesParent extends JSWindowActorParent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "Profiles:UpdateProfileAvatar": {
|
case "Profiles:UpdateProfileAvatar": {
|
||||||
let avatar = message.data.avatar;
|
let { avatarOrFile } = message.data;
|
||||||
SelectableProfileService.currentProfile.avatar = avatar;
|
await SelectableProfileService.currentProfile.setAvatar(avatarOrFile);
|
||||||
|
let value = SelectableProfileService.currentProfile.isCustomAvatar
|
||||||
|
? "custom"
|
||||||
|
: avatarOrFile;
|
||||||
|
|
||||||
if (source === "about:editprofile") {
|
if (source === "about:editprofile") {
|
||||||
Glean.profilesExisting.avatar.record({ value: avatar });
|
Glean.profilesExisting.avatar.record({ value });
|
||||||
} else if (source === "about:newprofile") {
|
} else if (source === "about:newprofile") {
|
||||||
Glean.profilesNew.avatar.record({ value: avatar });
|
Glean.profilesNew.avatar.record({ value });
|
||||||
}
|
}
|
||||||
break;
|
let profileObj =
|
||||||
|
await SelectableProfileService.currentProfile.toContentSafeObject();
|
||||||
|
return profileObj;
|
||||||
}
|
}
|
||||||
case "Profiles:UpdateProfileTheme": {
|
case "Profiles:UpdateProfileTheme": {
|
||||||
let themeId = message.data;
|
let themeId = message.data;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,42 @@
|
||||||
import { ProfilesDatastoreService } from "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs";
|
import { ProfilesDatastoreService } from "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs";
|
||||||
import { SelectableProfileService } from "resource:///modules/profiles/SelectableProfileService.sys.mjs";
|
import { SelectableProfileService } from "resource:///modules/profiles/SelectableProfileService.sys.mjs";
|
||||||
|
|
||||||
|
const STANDARD_AVATARS = new Set([
|
||||||
|
"barbell",
|
||||||
|
"bike",
|
||||||
|
"book",
|
||||||
|
"briefcase",
|
||||||
|
"canvas",
|
||||||
|
"craft",
|
||||||
|
"default-favicon",
|
||||||
|
"diamond",
|
||||||
|
"flower",
|
||||||
|
"folder",
|
||||||
|
"hammer",
|
||||||
|
"heart",
|
||||||
|
"heart-rate",
|
||||||
|
"history",
|
||||||
|
"leaf",
|
||||||
|
"lightbulb",
|
||||||
|
"makeup",
|
||||||
|
"message",
|
||||||
|
"musical-note",
|
||||||
|
"palette",
|
||||||
|
"paw-print",
|
||||||
|
"plane",
|
||||||
|
"present",
|
||||||
|
"shopping",
|
||||||
|
"soccer",
|
||||||
|
"sparkle-single",
|
||||||
|
"star",
|
||||||
|
"video-game-controller",
|
||||||
|
]);
|
||||||
|
const STANDARD_AVATAR_SIZES = [16, 20, 24, 48, 60, 80];
|
||||||
|
|
||||||
|
function standardAvatarURL(avatar, size = "80") {
|
||||||
|
return `chrome://browser/content/profiles/assets/${size}_${avatar}.svg`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selectable profile
|
* The selectable profile
|
||||||
*/
|
*/
|
||||||
|
|
@ -19,10 +55,15 @@ export class SelectableProfile {
|
||||||
// The user-editable name
|
// The user-editable name
|
||||||
#name;
|
#name;
|
||||||
|
|
||||||
// Name of the user's chosen avatar, which corresponds to a list of built-in
|
// Name of the user's chosen avatar, which corresponds to a list of standard
|
||||||
// SVG avatars.
|
// SVG avatars. Or if the avatar is a custom image, the filename of the image
|
||||||
|
// stored in the avatars directory.
|
||||||
#avatar;
|
#avatar;
|
||||||
|
|
||||||
|
// lastAvatarURL is saved when URL.createObjectURL is invoked so we can
|
||||||
|
// revoke the url at a later time.
|
||||||
|
#lastAvatarURL;
|
||||||
|
|
||||||
// Cached theme properties, used to allow displaying a SelectableProfile
|
// Cached theme properties, used to allow displaying a SelectableProfile
|
||||||
// without loading the AddonManager to get theme info.
|
// without loading the AddonManager to get theme info.
|
||||||
#themeId;
|
#themeId;
|
||||||
|
|
@ -122,16 +163,116 @@ export class SelectableProfile {
|
||||||
return this.#avatar;
|
return this.#avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path of the current avatar.
|
||||||
|
* If the avatar is standard, the return value will be of the form
|
||||||
|
* 'chrome://browser/content/profiles/assets/{avatar}.svg'.
|
||||||
|
* If the avatar is custom, the return value will be the path to the file on
|
||||||
|
* disk.
|
||||||
|
*
|
||||||
|
* @returns {string} Path to the current avatar.
|
||||||
|
*/
|
||||||
|
getAvatarPath(size) {
|
||||||
|
if (!this.hasCustomAvatar) {
|
||||||
|
return standardAvatarURL(this.avatar, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PathUtils.join(
|
||||||
|
ProfilesDatastoreService.constructor.PROFILE_GROUPS_DIR,
|
||||||
|
"avatars",
|
||||||
|
this.avatar
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL of the current avatar.
|
||||||
|
* If the avatar is standard, the return value will be of the form
|
||||||
|
* 'chrome://browser/content/profiles/assets/${size}_${avatar}.svg'.
|
||||||
|
* If the avatar is custom, the return value will be a blob URL.
|
||||||
|
*
|
||||||
|
* @param {string|number} size optional Must be one of the sizes in
|
||||||
|
* STANDARD_AVATAR_SIZES. Will be converted to a string.
|
||||||
|
*
|
||||||
|
* @returns {Promise<string>} Resolves to the URL of the current avatar
|
||||||
|
*/
|
||||||
|
async getAvatarURL(size) {
|
||||||
|
if (!this.hasCustomAvatar) {
|
||||||
|
return standardAvatarURL(this.avatar, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#lastAvatarURL) {
|
||||||
|
URL.revokeObjectURL(this.#lastAvatarURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExists = await IOUtils.exists(this.getAvatarPath());
|
||||||
|
if (!fileExists) {
|
||||||
|
throw new Error("Custom avatar file doesn't exist.");
|
||||||
|
}
|
||||||
|
const file = await File.createFromFileName(this.getAvatarPath());
|
||||||
|
this.#lastAvatarURL = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
return this.#lastAvatarURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the avatar file. This is only used for custom avatars to generate an
|
||||||
|
* object url. Standard avatars should use getAvatarURL or getAvatarPath.
|
||||||
|
*
|
||||||
|
* @returns {Promise<File>} Resolves to a file of the avatar
|
||||||
|
*/
|
||||||
|
async getAvatarFile() {
|
||||||
|
if (!this.hasCustomAvatar) {
|
||||||
|
throw new Error(
|
||||||
|
"Profile does not have custom avatar. Custom avatar file doesn't exist."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return File.createFromFileName(this.getAvatarPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasCustomAvatar() {
|
||||||
|
return !STANDARD_AVATARS.has(this.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the avatar, then trigger saving the profile, which will notify()
|
* Update the avatar, then trigger saving the profile, which will notify()
|
||||||
* other running instances.
|
* other running instances.
|
||||||
*
|
*
|
||||||
* @param {string} aAvatar Name of the avatar
|
* @param {string|File} aAvatarOrFile Name of the avatar or File os custom avatar
|
||||||
*/
|
*/
|
||||||
set avatar(aAvatar) {
|
async setAvatar(aAvatarOrFile) {
|
||||||
this.#avatar = aAvatar;
|
if (aAvatarOrFile === this.avatar) {
|
||||||
|
// The avatar is the same so do nothing. See the comment in
|
||||||
|
// SelectableProfileService.maybeSetupDataStore for resetting the avatar
|
||||||
|
// to draw the avatar icon in the dock.
|
||||||
|
} else if (STANDARD_AVATARS.has(aAvatarOrFile)) {
|
||||||
|
this.#avatar = aAvatarOrFile;
|
||||||
|
} else {
|
||||||
|
await this.#uploadCustomAvatar(aAvatarOrFile);
|
||||||
|
}
|
||||||
|
|
||||||
this.saveUpdatesToDB();
|
await this.saveUpdatesToDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #uploadCustomAvatar(file) {
|
||||||
|
const avatarsDir = PathUtils.join(
|
||||||
|
ProfilesDatastoreService.constructor.PROFILE_GROUPS_DIR,
|
||||||
|
"avatars"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create avatars directory if it does not exist
|
||||||
|
await IOUtils.makeDirectory(avatarsDir, { ignoreExisting: true });
|
||||||
|
|
||||||
|
let uuid = Services.uuid.generateUUID().toString().slice(1, -1);
|
||||||
|
|
||||||
|
const filePath = PathUtils.join(avatarsDir, uuid);
|
||||||
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer);
|
||||||
|
|
||||||
|
await IOUtils.write(filePath, uint8Array, { tmpPath: `${filePath}.tmp` });
|
||||||
|
|
||||||
|
this.#avatar = uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -155,7 +296,7 @@ export class SelectableProfile {
|
||||||
return "star-avatar-alt";
|
return "star-avatar-alt";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "custom-avatar-alt";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note, theme properties are set and returned as a group.
|
// Note, theme properties are set and returned as a group.
|
||||||
|
|
@ -205,16 +346,66 @@ export class SelectableProfile {
|
||||||
SelectableProfileService.updateProfile(this);
|
SelectableProfileService.updateProfile(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
toObject() {
|
/**
|
||||||
|
* Returns on object with only fields needed for the database.
|
||||||
|
*
|
||||||
|
* @returns {object} An object with only fields need for the database
|
||||||
|
*/
|
||||||
|
async toDbObject() {
|
||||||
|
let profileObj = {
|
||||||
|
id: this.id,
|
||||||
|
path: this.#path,
|
||||||
|
name: this.name,
|
||||||
|
avatar: this.avatar,
|
||||||
|
...this.theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
return profileObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object representation of the profile.
|
||||||
|
* Note: No custom avatar URLs are included because URL.createObjectURL needs
|
||||||
|
* to be invoked in the content process for the avatar to be visible.
|
||||||
|
*
|
||||||
|
* @returns {object} An object representation of the profile
|
||||||
|
*/
|
||||||
|
async toContentSafeObject() {
|
||||||
let profileObj = {
|
let profileObj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
path: this.#path,
|
path: this.#path,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
avatar: this.avatar,
|
avatar: this.avatar,
|
||||||
avatarL10nId: this.avatarL10nId,
|
avatarL10nId: this.avatarL10nId,
|
||||||
|
hasCustomAvatar: this.hasCustomAvatar,
|
||||||
...this.theme,
|
...this.theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.hasCustomAvatar) {
|
||||||
|
let path = this.getAvatarPath();
|
||||||
|
let file = await this.getAvatarFile();
|
||||||
|
|
||||||
|
profileObj.avatarPaths = Object.fromEntries(
|
||||||
|
STANDARD_AVATAR_SIZES.map(s => [`path${s}`, path])
|
||||||
|
);
|
||||||
|
profileObj.avatarFiles = Object.fromEntries(
|
||||||
|
STANDARD_AVATAR_SIZES.map(s => [`file${s}`, file])
|
||||||
|
);
|
||||||
|
profileObj.avatarURLs = {};
|
||||||
|
} else {
|
||||||
|
profileObj.avatarPaths = Object.fromEntries(
|
||||||
|
STANDARD_AVATAR_SIZES.map(s => [`path${s}`, this.getAvatarPath(s)])
|
||||||
|
);
|
||||||
|
profileObj.avatarURLs = Object.fromEntries(
|
||||||
|
await Promise.all(
|
||||||
|
STANDARD_AVATAR_SIZES.map(async s => [
|
||||||
|
`url${s}`,
|
||||||
|
await this.getAvatarURL(s),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return profileObj;
|
return profileObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const COMMAND_LINE_ACTIVATE = "profiles-activate";
|
||||||
|
|
||||||
const gSupportsBadging = "nsIMacDockSupport" in Ci || "nsIWinTaskbar" in Ci;
|
const gSupportsBadging = "nsIMacDockSupport" in Ci || "nsIWinTaskbar" in Ci;
|
||||||
|
|
||||||
function loadImage(url) {
|
function loadBuiltInAvatarImage(uri, channel) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
|
let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
|
||||||
let imageContainer;
|
let imageContainer;
|
||||||
|
|
@ -67,15 +67,8 @@ function loadImage(url) {
|
||||||
});
|
});
|
||||||
|
|
||||||
imageTools.decodeImageFromChannelAsync(
|
imageTools.decodeImageFromChannelAsync(
|
||||||
url,
|
uri,
|
||||||
Services.io.newChannelFromURI(
|
channel,
|
||||||
url,
|
|
||||||
null,
|
|
||||||
Services.scriptSecurityManager.getSystemPrincipal(),
|
|
||||||
null, // aTriggeringPrincipal
|
|
||||||
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
|
|
||||||
Ci.nsIContentPolicy.TYPE_IMAGE
|
|
||||||
),
|
|
||||||
(image, status) => {
|
(image, status) => {
|
||||||
if (!Components.isSuccessCode(status)) {
|
if (!Components.isSuccessCode(status)) {
|
||||||
reject(new Components.Exception("Image loading failed", status));
|
reject(new Components.Exception("Image loading failed", status));
|
||||||
|
|
@ -88,6 +81,66 @@ function loadImage(url) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCustomAvatarImageType(blob, channel) {
|
||||||
|
let octets = await new Promise((resolve, reject) => {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.addEventListener("load", () => {
|
||||||
|
resolve(Array.from(reader.result).map(c => c.charCodeAt(0)));
|
||||||
|
});
|
||||||
|
reader.addEventListener("error", reject);
|
||||||
|
reader.readAsBinaryString(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
|
||||||
|
Ci.nsIContentSniffer
|
||||||
|
);
|
||||||
|
let type = sniffer.getMIMETypeFromContent(channel, octets, octets.length);
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCustomAvatarImage(profile, channel) {
|
||||||
|
let blob = await profile.getAvatarFile();
|
||||||
|
|
||||||
|
const imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
|
||||||
|
let type = await getCustomAvatarImageType(blob, channel);
|
||||||
|
|
||||||
|
let buffer = await blob.arrayBuffer();
|
||||||
|
const image = imageTools.decodeImageFromArrayBuffer(buffer, type);
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage(profile) {
|
||||||
|
let uri;
|
||||||
|
|
||||||
|
if (SelectableProfileService.currentProfile.hasCustomAvatar) {
|
||||||
|
const file = await IOUtils.getFile(
|
||||||
|
SelectableProfileService.currentProfile.getAvatarPath(48)
|
||||||
|
);
|
||||||
|
uri = Services.io.newFileURI(file);
|
||||||
|
} else {
|
||||||
|
uri = Services.io.newURI(
|
||||||
|
SelectableProfileService.currentProfile.getAvatarPath(48)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = Services.io.newChannelFromURI(
|
||||||
|
uri,
|
||||||
|
null,
|
||||||
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
null,
|
||||||
|
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
|
||||||
|
Ci.nsIContentPolicy.TYPE_IMAGE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile.isCustomAvatar) {
|
||||||
|
return loadCustomAvatarImage(profile, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadBuiltInAvatarImage(uri, channel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service that manages selectable profiles
|
* The service that manages selectable profiles
|
||||||
*/
|
*/
|
||||||
|
|
@ -589,13 +642,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
||||||
|
|
||||||
if (count > 1 && !this.#badge) {
|
if (count > 1 && !this.#badge) {
|
||||||
this.#badge = {
|
this.#badge = {
|
||||||
image: await loadImage(
|
image: await loadImage(this.#currentProfile),
|
||||||
Services.io.newURI(
|
|
||||||
`chrome://browser/content/profiles/assets/48_${
|
|
||||||
this.#currentProfile.avatar
|
|
||||||
}.svg`
|
|
||||||
)
|
|
||||||
),
|
|
||||||
iconPaintContext: this.#currentProfile.iconPaintContext,
|
iconPaintContext: this.#currentProfile.iconPaintContext,
|
||||||
description: this.#currentProfile.name,
|
description: this.#currentProfile.name,
|
||||||
};
|
};
|
||||||
|
|
@ -611,11 +658,18 @@ class SelectableProfileServiceClass extends EventEmitter {
|
||||||
.getOverlayIconController(win.docShell);
|
.getOverlayIconController(win.docShell);
|
||||||
TASKBAR_ICON_CONTROLLERS.set(win, iconController);
|
TASKBAR_ICON_CONTROLLERS.set(win, iconController);
|
||||||
|
|
||||||
iconController.setOverlayIcon(
|
if (this.#currentProfile.isCustomAvatar) {
|
||||||
this.#badge.image,
|
iconController.setOverlayIcon(
|
||||||
this.#badge.description,
|
this.#badge.image,
|
||||||
this.#badge.iconPaintContext
|
this.#badge.description
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
iconController.setOverlayIcon(
|
||||||
|
this.#badge.image,
|
||||||
|
this.#badge.description,
|
||||||
|
this.#badge.iconPaintContext
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (count <= 1 && this.#badge) {
|
} else if (count <= 1 && this.#badge) {
|
||||||
|
|
@ -1116,7 +1170,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
||||||
lazy.setTimeout(() => {
|
lazy.setTimeout(() => {
|
||||||
// To avoid displeasing the linter, assign to a temporary variable.
|
// To avoid displeasing the linter, assign to a temporary variable.
|
||||||
let avatar = SelectableProfileService.currentProfile.avatar;
|
let avatar = SelectableProfileService.currentProfile.avatar;
|
||||||
SelectableProfileService.currentProfile.avatar = avatar;
|
SelectableProfileService.currentProfile.setAvatar(avatar);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1239,8 +1293,7 @@ class SelectableProfileServiceClass extends EventEmitter {
|
||||||
* @param {SelectableProfile} aSelectableProfile The SelectableProfile to be updated
|
* @param {SelectableProfile} aSelectableProfile The SelectableProfile to be updated
|
||||||
*/
|
*/
|
||||||
async updateProfile(aSelectableProfile) {
|
async updateProfile(aSelectableProfile) {
|
||||||
let profileObj = aSelectableProfile.toObject();
|
let profileObj = await aSelectableProfile.toDbObject();
|
||||||
delete profileObj.avatarL10nId;
|
|
||||||
|
|
||||||
await this.#connection.execute(
|
await this.#connection.execute(
|
||||||
`UPDATE Profiles
|
`UPDATE Profiles
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ export class DeleteProfileCard extends MozLitElement {
|
||||||
|
|
||||||
this.data = await RPMSendQuery("Profiles:GetDeleteProfileContent");
|
this.data = await RPMSendQuery("Profiles:GetDeleteProfileContent");
|
||||||
|
|
||||||
|
if (this.data.profile.hasCustomAvatar) {
|
||||||
|
const objURL = URL.createObjectURL(this.data.profile.avatarFiles.file16);
|
||||||
|
this.data.profile.avatarURLs.url16 = objURL;
|
||||||
|
this.data.profile.avatarURLs.url80 = objURL;
|
||||||
|
}
|
||||||
|
|
||||||
let titleEl = document.querySelector("title");
|
let titleEl = document.querySelector("title");
|
||||||
titleEl.setAttribute(
|
titleEl.setAttribute(
|
||||||
"data-l10n-args",
|
"data-l10n-args",
|
||||||
|
|
@ -64,7 +70,7 @@ export class DeleteProfileCard extends MozLitElement {
|
||||||
|
|
||||||
setFavicon() {
|
setFavicon() {
|
||||||
let favicon = document.getElementById("favicon");
|
let favicon = document.getElementById("favicon");
|
||||||
favicon.href = `chrome://browser/content/profiles/assets/16_${this.data.profile.avatar}.svg`;
|
favicon.href = this.data.profile.avatarURLs.url16;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelDelete() {
|
cancelDelete() {
|
||||||
|
|
@ -95,8 +101,7 @@ export class DeleteProfileCard extends MozLitElement {
|
||||||
width="80"
|
width="80"
|
||||||
height="80"
|
height="80"
|
||||||
data-l10n-id=${this.data.profile.avatarL10nId}
|
data-l10n-id=${this.data.profile.avatarL10nId}
|
||||||
src="chrome://browser/content/profiles/assets/80_${this.data.profile
|
src=${this.data.profile.avatarURLs.url80}
|
||||||
.avatar}.svg"
|
|
||||||
/>
|
/>
|
||||||
<div id="profile-content">
|
<div id="profile-content">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src resource: chrome:; object-src 'none'; img-src chrome:;"
|
content="default-src resource: chrome:; object-src 'none'; img-src blob: chrome:;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link id="favicon" rel="icon" type="image/svg+xml" />
|
<link id="favicon" rel="icon" type="image/svg+xml" />
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,11 @@ export class EditProfileCard extends MozLitElement {
|
||||||
|
|
||||||
window.addEventListener("beforeunload", this);
|
window.addEventListener("beforeunload", this);
|
||||||
window.addEventListener("pagehide", this);
|
window.addEventListener("pagehide", this);
|
||||||
document.addEventListener("click", this);
|
|
||||||
|
if (RPMGetBoolPref(UPDATED_AVATAR_SELECTOR_PREF, false)) {
|
||||||
|
document.addEventListener("click", this);
|
||||||
|
document.addEventListener("Profiles:CustomAvatarUpload", this);
|
||||||
|
}
|
||||||
|
|
||||||
this.init().then(() => (this.initialized = true));
|
this.init().then(() => (this.initialized = true));
|
||||||
}
|
}
|
||||||
|
|
@ -146,9 +150,27 @@ export class EditProfileCard extends MozLitElement {
|
||||||
this.profiles = profiles;
|
this.profiles = profiles;
|
||||||
this.themes = themes;
|
this.themes = themes;
|
||||||
|
|
||||||
|
if (this.profile.hasCustomAvatar) {
|
||||||
|
this.createAvatarURL();
|
||||||
|
}
|
||||||
|
|
||||||
this.setFavicon();
|
this.setFavicon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createAvatarURL() {
|
||||||
|
if (this.profile.avatarURLs.url16) {
|
||||||
|
URL.revokeObjectURL(this.profile.avatarURLs.url16);
|
||||||
|
delete this.profile.avatarURLs.url16;
|
||||||
|
delete this.profile.avatarURLs.url80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.profile.avatarFiles?.file16) {
|
||||||
|
const objURL = URL.createObjectURL(this.profile.avatarFiles.file16);
|
||||||
|
this.profile.avatarURLs.url16 = objURL;
|
||||||
|
this.profile.avatarURLs.url80 = objURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getUpdateComplete() {
|
async getUpdateComplete() {
|
||||||
const result = await super.getUpdateComplete();
|
const result = await super.getUpdateComplete();
|
||||||
|
|
||||||
|
|
@ -163,7 +185,7 @@ export class EditProfileCard extends MozLitElement {
|
||||||
|
|
||||||
setFavicon() {
|
setFavicon() {
|
||||||
let favicon = document.getElementById("favicon");
|
let favicon = document.getElementById("favicon");
|
||||||
favicon.href = `chrome://browser/content/profiles/assets/16_${this.profile.avatar}.svg`;
|
favicon.href = this.profile.avatarURLs.url16;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvatarL10nId(value) {
|
getAvatarL10nId(value) {
|
||||||
|
|
@ -208,6 +230,11 @@ export class EditProfileCard extends MozLitElement {
|
||||||
this.avatarSelector.hidden = true;
|
this.avatarSelector.hidden = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "Profiles:CustomAvatarUpload": {
|
||||||
|
let { file } = event.detail;
|
||||||
|
this.updateAvatar(file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,8 +281,16 @@ export class EditProfileCard extends MozLitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.profile.avatar = newAvatar;
|
let updatedProfile = await RPMSendQuery("Profiles:UpdateProfileAvatar", {
|
||||||
RPMSendAsyncMessage("Profiles:UpdateProfileAvatar", this.profile);
|
avatarOrFile: newAvatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.profile = updatedProfile;
|
||||||
|
|
||||||
|
if (this.profile.hasCustomAvatar) {
|
||||||
|
this.createAvatarURL();
|
||||||
|
}
|
||||||
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
this.setFavicon();
|
this.setFavicon();
|
||||||
}
|
}
|
||||||
|
|
@ -413,8 +448,7 @@ export class EditProfileCard extends MozLitElement {
|
||||||
<img
|
<img
|
||||||
id="header-avatar"
|
id="header-avatar"
|
||||||
data-l10n-id=${this.profile.avatarL10nId}
|
data-l10n-id=${this.profile.avatarL10nId}
|
||||||
src="chrome://browser/content/profiles/assets/80_${this.profile
|
src=${this.profile.avatarURLs.url80}
|
||||||
.avatar}.svg"
|
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
id="profile-avatar-selector-link"
|
id="profile-avatar-selector-link"
|
||||||
|
|
@ -433,8 +467,7 @@ export class EditProfileCard extends MozLitElement {
|
||||||
return html`<img
|
return html`<img
|
||||||
id="header-avatar"
|
id="header-avatar"
|
||||||
data-l10n-id=${this.profile.avatarL10nId}
|
data-l10n-id=${this.profile.avatarL10nId}
|
||||||
src="chrome://browser/content/profiles/assets/20_${this.profile
|
src=${this.profile.avatarURLs.url80}
|
||||||
.avatar}.svg"
|
|
||||||
/>`;
|
/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src chrome:; object-src 'none'; img-src chrome:;"
|
content="default-src chrome:; object-src 'none'; img-src blob: chrome:;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link id="favicon" rel="icon" type="image/svg+xml" />
|
<link id="favicon" rel="icon" type="image/svg+xml" />
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ export class NewProfileCard extends EditProfileCard {
|
||||||
this.profiles = profiles;
|
this.profiles = profiles;
|
||||||
this.themes = themes;
|
this.themes = themes;
|
||||||
|
|
||||||
|
if (this.profile.hasCustomAvatar) {
|
||||||
|
super.createAvatarURL();
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.setInitialInput(),
|
this.setInitialInput(),
|
||||||
this.setRandomTheme(isInAutomation),
|
this.setRandomTheme(isInAutomation),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src chrome:; object-src 'none'; img-src chrome:;"
|
content="default-src chrome:; object-src 'none'; img-src blob: chrome:;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link id="favicon" rel="icon" type="image/svg+xml" />
|
<link id="favicon" rel="icon" type="image/svg+xml" />
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,51 @@
|
||||||
--button-icon-fill: transparent;
|
--button-icon-fill: transparent;
|
||||||
--button-icon-stroke: currentColor;
|
--button-icon-stroke: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-avatar-area {
|
||||||
|
width: 230px;
|
||||||
|
height: 194px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-text {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drag-text {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
#custom-avatar-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-avatar-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#custom-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-messages {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-small);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,20 @@ import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
export class ProfileAvatarSelector extends MozLitElement {
|
export class ProfileAvatarSelector extends MozLitElement {
|
||||||
static properties = {
|
static properties = {
|
||||||
value: { type: String },
|
value: { type: String },
|
||||||
|
state: { type: String },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static queries = {
|
||||||
|
input: "#custom-image",
|
||||||
|
saveButton: "#save-button",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = "custom";
|
||||||
|
}
|
||||||
|
|
||||||
getAvatarL10nId(value) {
|
getAvatarL10nId(value) {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "book":
|
case "book":
|
||||||
|
|
@ -33,7 +45,7 @@ export class ProfileAvatarSelector extends MozLitElement {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
iconTabContent() {
|
iconTabContentTemplate() {
|
||||||
let avatars = [
|
let avatars = [
|
||||||
"star",
|
"star",
|
||||||
"flower",
|
"flower",
|
||||||
|
|
@ -90,12 +102,104 @@ export class ProfileAvatarSelector extends MozLitElement {
|
||||||
>`;
|
>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customTabUploadFileContentTemplate() {
|
||||||
|
return html`<div class="custom-avatar-area">
|
||||||
|
<input
|
||||||
|
@change=${this.handleFileUpload}
|
||||||
|
id="custom-image"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
label="Upload a file"
|
||||||
|
/>
|
||||||
|
<div id="file-messages">
|
||||||
|
<img src="chrome://browser/skin/open.svg" />
|
||||||
|
<span
|
||||||
|
id="upload-text"
|
||||||
|
data-l10n-id="avatar-selector-upload-file"
|
||||||
|
></span>
|
||||||
|
<span id="drag-text" data-l10n-id="avatar-selector-drag-file"></span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancelClick(event) {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
this.state = "custom";
|
||||||
|
if (this.blobURL) {
|
||||||
|
URL.revokeObjectURL(this.blobURL);
|
||||||
|
}
|
||||||
|
this.file = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSaveClick(event) {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("Profiles:CustomAvatarUpload", {
|
||||||
|
detail: { file: this.file },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.blobURL) {
|
||||||
|
URL.revokeObjectURL(this.blobURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = "custom";
|
||||||
|
this.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
customTabViewImageTemplate() {
|
||||||
|
return html`<div class="custom-avatar-area">
|
||||||
|
<img id="custom-avatar-image" src=${this.blobURL} />
|
||||||
|
</div>
|
||||||
|
<moz-button-group class="custom-avatar-actions"
|
||||||
|
><moz-button
|
||||||
|
@click=${this.handleCancelClick}
|
||||||
|
data-l10n-id="avatar-selector-cancel-button"
|
||||||
|
></moz-button
|
||||||
|
><moz-button
|
||||||
|
type="primary"
|
||||||
|
id="save-button"
|
||||||
|
@click=${this.handleSaveClick}
|
||||||
|
data-l10n-id="avatar-selector-save-button"
|
||||||
|
></moz-button
|
||||||
|
></moz-button-group>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileUpload(event) {
|
||||||
|
const [file] = event.target.files;
|
||||||
|
this.file = file;
|
||||||
|
|
||||||
|
if (this.blobURL) {
|
||||||
|
URL.revokeObjectURL(this.blobURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blobURL = URL.createObjectURL(file);
|
||||||
|
this.state = "crop";
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTemplate() {
|
||||||
|
switch (this.state) {
|
||||||
|
case "icon": {
|
||||||
|
return this.iconTabContentTemplate();
|
||||||
|
}
|
||||||
|
case "custom": {
|
||||||
|
return this.customTabUploadFileContentTemplate();
|
||||||
|
}
|
||||||
|
case "crop": {
|
||||||
|
return this.customTabViewImageTemplate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<link
|
return html`<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="chrome://browser/content/profiles/profile-avatar-selector.css"
|
href="chrome://browser/content/profiles/profile-avatar-selector.css"
|
||||||
/>
|
/>
|
||||||
<moz-card>
|
<moz-card id="avatar-selector">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<moz-button
|
<moz-button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -103,7 +207,7 @@ export class ProfileAvatarSelector extends MozLitElement {
|
||||||
></moz-button
|
></moz-button
|
||||||
><moz-button data-l10n-id="avatar-selector-custom-tab"></moz-button>
|
><moz-button data-l10n-id="avatar-selector-custom-tab"></moz-button>
|
||||||
</div>
|
</div>
|
||||||
${this.iconTabContent()}
|
${this.contentTemplate()}
|
||||||
</moz-card>`;
|
</moz-card>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ export class ProfileCard extends MozLitElement {
|
||||||
this.backgroundImage.style.stroke = themeFg;
|
this.backgroundImage.style.stroke = themeFg;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAvatarImage() {
|
async setAvatarImage() {
|
||||||
this.avatarImage.style.backgroundImage = `url("chrome://browser/content/profiles/assets/80_${this.profile.avatar}.svg")`;
|
this.avatarImage.style.backgroundImage = `url(${await this.profile.getAvatarURL(80)})`;
|
||||||
let { themeFg, themeBg } = this.profile.theme;
|
let { themeFg, themeBg } = this.profile.theme;
|
||||||
this.avatarImage.style.fill = themeBg;
|
this.avatarImage.style.fill = themeBg;
|
||||||
this.avatarImage.style.stroke = themeFg;
|
this.avatarImage.style.stroke = themeFg;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src resource: chrome:; object-src 'none'; img-src chrome:;"
|
content="default-src resource: chrome:; object-src 'none'; img-src blob: chrome:;"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="localization" href="branding/brand.ftl" />
|
<link rel="localization" href="branding/brand.ftl" />
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ add_task(async function test_new_profile_avatar() {
|
||||||
let expectedAvatar = "briefcase";
|
let expectedAvatar = "briefcase";
|
||||||
// Before we load the new page, set the profile's avatar to something other
|
// Before we load the new page, set the profile's avatar to something other
|
||||||
// than the expected item.
|
// than the expected item.
|
||||||
profile.avatar = "flower";
|
await profile.setAvatar("flower");
|
||||||
await SelectableProfileService.updateProfile(profile);
|
await SelectableProfileService.updateProfile(profile);
|
||||||
|
|
||||||
await BrowserTestUtils.withNewTab(
|
await BrowserTestUtils.withNewTab(
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ add_task(async function test_edit_profile_custom_avatar() {
|
||||||
},
|
},
|
||||||
async browser => {
|
async browser => {
|
||||||
await SpecialPowers.spawn(browser, [], async () => {
|
await SpecialPowers.spawn(browser, [], async () => {
|
||||||
await new Promise(r => content.setTimeout(r, 4000));
|
|
||||||
let editProfileCard =
|
let editProfileCard =
|
||||||
content.document.querySelector("edit-profile-card").wrappedJSObject;
|
content.document.querySelector("edit-profile-card").wrappedJSObject;
|
||||||
|
|
||||||
|
|
@ -55,5 +54,119 @@ add_task(async function test_edit_profile_custom_avatar() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_edit_profile_custom_avatar_upload() {
|
||||||
|
if (!AppConstants.MOZ_SELECTABLE_PROFILES) {
|
||||||
|
// `mochitest-browser` suite `add_task` does not yet support
|
||||||
|
// `properties.skip_if`.
|
||||||
|
ok(true, "Skipping because !AppConstants.MOZ_SELECTABLE_PROFILES");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const profile = await setup();
|
||||||
|
|
||||||
|
await SpecialPowers.pushPrefEnv({
|
||||||
|
set: [["browser.profiles.updated-avatar-selector", true]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockAvatarFilePath = await IOUtils.createUniqueFile(
|
||||||
|
PathUtils.tempDir,
|
||||||
|
"avatar.png"
|
||||||
|
);
|
||||||
|
const mockAvatarFile = Cc["@mozilla.org/file/local;1"].createInstance(
|
||||||
|
Ci.nsIFile
|
||||||
|
);
|
||||||
|
mockAvatarFile.initWithPath(mockAvatarFilePath);
|
||||||
|
|
||||||
|
const MockFilePicker = SpecialPowers.MockFilePicker;
|
||||||
|
MockFilePicker.init(window.browsingContext);
|
||||||
|
MockFilePicker.setFiles([mockAvatarFile]);
|
||||||
|
MockFilePicker.returnValue = MockFilePicker.returnOK;
|
||||||
|
|
||||||
|
let curProfile = await SelectableProfileService.getProfile(profile.id);
|
||||||
|
Assert.ok(
|
||||||
|
!curProfile.hasCustomAvatar,
|
||||||
|
"Current profile does not have a custom avatar"
|
||||||
|
);
|
||||||
|
|
||||||
|
await BrowserTestUtils.withNewTab(
|
||||||
|
{
|
||||||
|
gBrowser,
|
||||||
|
url: "about:editprofile",
|
||||||
|
},
|
||||||
|
async browser => {
|
||||||
|
await SpecialPowers.spawn(browser, [], async () => {
|
||||||
|
let editProfileCard =
|
||||||
|
content.document.querySelector("edit-profile-card").wrappedJSObject;
|
||||||
|
|
||||||
|
await ContentTaskUtils.waitForCondition(
|
||||||
|
() => editProfileCard.initialized,
|
||||||
|
"Waiting for edit-profile-card to be initialized"
|
||||||
|
);
|
||||||
|
|
||||||
|
await editProfileCard.updateComplete;
|
||||||
|
|
||||||
|
const avatarSelector = editProfileCard.avatarSelector;
|
||||||
|
|
||||||
|
EventUtils.synthesizeMouseAtCenter(
|
||||||
|
editProfileCard.avatarSelectorLink,
|
||||||
|
{},
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.ok(
|
||||||
|
ContentTaskUtils.isVisible(avatarSelector),
|
||||||
|
"Should be showing the profile avatar selector"
|
||||||
|
);
|
||||||
|
|
||||||
|
avatarSelector.state = "custom";
|
||||||
|
await avatarSelector.updateComplete;
|
||||||
|
|
||||||
|
await ContentTaskUtils.waitForCondition(
|
||||||
|
() => ContentTaskUtils.isVisible(avatarSelector.input),
|
||||||
|
"Waiting for avatar selector input to be visible"
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputReceived = new Promise(resolve =>
|
||||||
|
avatarSelector.input.addEventListener(
|
||||||
|
"input",
|
||||||
|
event => {
|
||||||
|
resolve(event.target.files[0].name);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
EventUtils.synthesizeMouseAtCenter(avatarSelector.input, {}, content);
|
||||||
|
|
||||||
|
await inputReceived;
|
||||||
|
|
||||||
|
await ContentTaskUtils.waitForCondition(
|
||||||
|
() => ContentTaskUtils.isVisible(avatarSelector.saveButton),
|
||||||
|
"Waiting for avatar selector save button to be visible"
|
||||||
|
);
|
||||||
|
|
||||||
|
EventUtils.synthesizeMouseAtCenter(
|
||||||
|
avatarSelector.saveButton,
|
||||||
|
{},
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sometimes the async message takes a bit longer to arrive.
|
||||||
|
await new Promise(resolve => content.setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
curProfile = await SelectableProfileService.getProfile(profile.id);
|
||||||
|
Assert.ok(
|
||||||
|
curProfile.hasCustomAvatar,
|
||||||
|
"Current profile has a custom avatar image"
|
||||||
|
);
|
||||||
|
|
||||||
|
MockFilePicker.cleanup();
|
||||||
|
|
||||||
await SpecialPowers.popPrefEnv();
|
await SpecialPowers.popPrefEnv();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ add_task(async function test_edit_profile_avatar() {
|
||||||
|
|
||||||
// Before we load the edit page, set the profile's avatar to something other
|
// Before we load the edit page, set the profile's avatar to something other
|
||||||
// than the 0th item.
|
// than the 0th item.
|
||||||
profile.avatar = "flower";
|
await profile.setAvatar("flower");
|
||||||
let expectedAvatar = "book";
|
let expectedAvatar = "book";
|
||||||
|
|
||||||
is(
|
is(
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,12 @@ edit-profile-page-delete-button =
|
||||||
edit-profile-page-avatar-selector-opener-link = Edit
|
edit-profile-page-avatar-selector-opener-link = Edit
|
||||||
avatar-selector-icon-tab = Icon
|
avatar-selector-icon-tab = Icon
|
||||||
avatar-selector-custom-tab = Custom
|
avatar-selector-custom-tab = Custom
|
||||||
|
avatar-selector-cancel-button =
|
||||||
|
.label = Cancel
|
||||||
|
avatar-selector-save-button =
|
||||||
|
.label = Save
|
||||||
|
avatar-selector-upload-file = Upload a file
|
||||||
|
avatar-selector-drag-file = Or drag a file here
|
||||||
|
|
||||||
edit-profile-page-no-name = Name this profile to help you find it later. Rename it any time.
|
edit-profile-page-no-name = Name this profile to help you find it later. Rename it any time.
|
||||||
edit-profile-page-duplicate-name = Profile name already in use. Try a new name.
|
edit-profile-page-duplicate-name = Profile name already in use. Try a new name.
|
||||||
|
|
@ -134,6 +140,8 @@ shopping-avatar-alt =
|
||||||
.alt = Shopping cart
|
.alt = Shopping cart
|
||||||
star-avatar-alt =
|
star-avatar-alt =
|
||||||
.alt = Star
|
.alt = Star
|
||||||
|
custom-avatar-alt =
|
||||||
|
.alt = Custom avatar
|
||||||
|
|
||||||
## Labels for default avatar icons
|
## Labels for default avatar icons
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1319,15 +1319,19 @@ static nsLiteralCString sStyleSrcUnsafeInlineAllowList[] = {
|
||||||
static nsLiteralCString sImgSrcDataBlobAllowList[] = {
|
static nsLiteralCString sImgSrcDataBlobAllowList[] = {
|
||||||
"about:addons"_ns,
|
"about:addons"_ns,
|
||||||
"about:debugging"_ns,
|
"about:debugging"_ns,
|
||||||
|
"about:deleteprofile"_ns,
|
||||||
"about:devtools-toolbox"_ns,
|
"about:devtools-toolbox"_ns,
|
||||||
|
"about:editprofile"_ns,
|
||||||
"about:firefoxview"_ns,
|
"about:firefoxview"_ns,
|
||||||
"about:home"_ns,
|
"about:home"_ns,
|
||||||
"about:inference"_ns,
|
"about:inference"_ns,
|
||||||
"about:logins"_ns,
|
"about:logins"_ns,
|
||||||
|
"about:newprofile"_ns,
|
||||||
"about:newtab"_ns,
|
"about:newtab"_ns,
|
||||||
"about:preferences"_ns,
|
"about:preferences"_ns,
|
||||||
"about:privatebrowsing"_ns,
|
"about:privatebrowsing"_ns,
|
||||||
"about:processes"_ns,
|
"about:processes"_ns,
|
||||||
|
"about:profilemanager"_ns,
|
||||||
"about:protections"_ns,
|
"about:protections"_ns,
|
||||||
"about:reader"_ns,
|
"about:reader"_ns,
|
||||||
"about:sessionrestore"_ns,
|
"about:sessionrestore"_ns,
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,10 @@ export let RemotePageAccessManager = {
|
||||||
RPMSendQuery: [
|
RPMSendQuery: [
|
||||||
"Profiles:GetEditProfileContent",
|
"Profiles:GetEditProfileContent",
|
||||||
"Profiles:UpdateProfileTheme",
|
"Profiles:UpdateProfileTheme",
|
||||||
|
"Profiles:UpdateProfileAvatar",
|
||||||
],
|
],
|
||||||
RPMSendAsyncMessage: [
|
RPMSendAsyncMessage: [
|
||||||
"Profiles:UpdateProfileName",
|
"Profiles:UpdateProfileName",
|
||||||
"Profiles:UpdateProfileAvatar",
|
|
||||||
"Profiles:OpenDeletePage",
|
"Profiles:OpenDeletePage",
|
||||||
"Profiles:CloseProfileTab",
|
"Profiles:CloseProfileTab",
|
||||||
"Profiles:MoreThemes",
|
"Profiles:MoreThemes",
|
||||||
|
|
@ -160,10 +160,10 @@ export let RemotePageAccessManager = {
|
||||||
RPMSendQuery: [
|
RPMSendQuery: [
|
||||||
"Profiles:GetNewProfileContent",
|
"Profiles:GetNewProfileContent",
|
||||||
"Profiles:UpdateProfileTheme",
|
"Profiles:UpdateProfileTheme",
|
||||||
|
"Profiles:UpdateProfileAvatar",
|
||||||
],
|
],
|
||||||
RPMSendAsyncMessage: [
|
RPMSendAsyncMessage: [
|
||||||
"Profiles:UpdateProfileName",
|
"Profiles:UpdateProfileName",
|
||||||
"Profiles:UpdateProfileAvatar",
|
|
||||||
"Profiles:DeleteProfile",
|
"Profiles:DeleteProfile",
|
||||||
"Profiles:CloseProfileTab",
|
"Profiles:CloseProfileTab",
|
||||||
"Profiles:MoreThemes",
|
"Profiles:MoreThemes",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue