browser-profiles.js (16136B)
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/. */ 4 5 var gProfiles = { 6 async init() { 7 this.copyProfile = this.copyProfile.bind(this); 8 this.createNewProfile = this.createNewProfile.bind(this); 9 this.handleCommand = this.handleCommand.bind(this); 10 this.launchProfile = this.launchProfile.bind(this); 11 this.manageProfiles = this.manageProfiles.bind(this); 12 this.onPopupShowing = this.onPopupShowing.bind(this); 13 this.toggleProfileMenus = this.toggleProfileMenus.bind(this); 14 this.updateView = this.updateView.bind(this); 15 16 this.bundle = Services.strings.createBundle( 17 "chrome://browser/locale/browser.properties" 18 ); 19 20 this.emptyProfilesButton = PanelMultiView.getViewNode( 21 document, 22 "appMenu-empty-profiles-button" 23 ); 24 this.profilesButton = PanelMultiView.getViewNode( 25 document, 26 "appMenu-profiles-button" 27 ); 28 this.fxaMenuEmptyProfilesButton = PanelMultiView.getViewNode( 29 document, 30 "PanelUI-fxa-menu-empty-profiles-button" 31 ); 32 this.fxaMenuProfilesButton = PanelMultiView.getViewNode( 33 document, 34 "PanelUI-fxa-menu-profiles-button" 35 ); 36 this.subview = PanelMultiView.getViewNode(document, "PanelUI-profiles"); 37 this.subview.addEventListener("command", this.handleCommand); 38 39 PanelUI.mainView.addEventListener("ViewShowing", () => 40 this._onPanelShowing(this.profilesButton, this.emptyProfilesButton) 41 ); 42 43 let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa"); 44 fxaPanelView.addEventListener("ViewShowing", () => 45 this._onPanelShowing( 46 this.fxaMenuProfilesButton, 47 this.fxaMenuEmptyProfilesButton 48 ) 49 ); 50 51 this.profilesButton.addEventListener("command", this.handleCommand); 52 this.emptyProfilesButton.addEventListener("command", this.handleCommand); 53 54 this.fxaMenuProfilesButton.addEventListener("command", this.handleCommand); 55 this.fxaMenuEmptyProfilesButton.addEventListener( 56 "command", 57 this.handleCommand 58 ); 59 60 this.toggleProfileMenus(SelectableProfileService?.isEnabled); 61 62 if (SelectableProfileService) { 63 let listener = (event, isEnabled) => this.toggleProfileMenus(isEnabled); 64 65 SelectableProfileService.on("enableChanged", listener); 66 window.addEventListener("unload", () => 67 SelectableProfileService.off("enableChanged", listener) 68 ); 69 } 70 }, 71 72 toggleProfileMenus(isEnabled) { 73 let profilesMenu = document.getElementById("profiles-menu"); 74 profilesMenu.hidden = !isEnabled; 75 }, 76 77 async _onPanelShowing(profilesButton, emptyProfilesButton) { 78 if (!SelectableProfileService?.isEnabled) { 79 emptyProfilesButton.hidden = true; 80 profilesButton.hidden = true; 81 return; 82 } 83 84 // If the feature is preffed on, but we haven't created profiles yet, the 85 // service will not be initialized. 86 let profiles = SelectableProfileService.initialized 87 ? await SelectableProfileService.getAllProfiles() 88 : []; 89 if (profiles.length < 2) { 90 profilesButton.hidden = true; 91 emptyProfilesButton.hidden = false; 92 return; 93 } 94 95 emptyProfilesButton.hidden = true; 96 profilesButton.hidden = false; 97 98 let { themeBg, themeFg } = SelectableProfileService.currentProfile.theme; 99 profilesButton.style.setProperty("--appmenu-profiles-theme-bg", themeBg); 100 profilesButton.style.setProperty("--appmenu-profiles-theme-fg", themeFg); 101 profilesButton.setAttribute( 102 "label", 103 SelectableProfileService.currentProfile.name 104 ); 105 let avatarURL = 106 await SelectableProfileService.currentProfile.getAvatarURL(16); 107 profilesButton.setAttribute("image", avatarURL); 108 }, 109 110 /** 111 * Draws the menubar panel contents. 112 */ 113 async onPopupShowing() { 114 let menuPopup = document.getElementById("menu_ProfilesPopup"); 115 let profiles = await SelectableProfileService.getAllProfiles(); 116 let currentProfile = SelectableProfileService.currentProfile; 117 let insertionPoint = document.getElementById("menu_newProfile"); 118 let existingItems = [ 119 ...menuPopup.querySelectorAll(":scope > menuitem[profileid]"), 120 ]; 121 for (let profile of profiles) { 122 let menuitem = existingItems.shift(); 123 let isNewItem = !menuitem; 124 if (isNewItem) { 125 menuitem = document.createXULElement("menuitem"); 126 menuitem.classList.add("menuitem-iconic", "menuitem-iconic-profile"); 127 menuitem.setAttribute("command", "Profiles:LaunchProfile"); 128 } 129 let { themeBg, themeFg } = profile.theme; 130 menuitem.setAttribute("profileid", profile.id); 131 menuitem.setAttribute("image", await profile.getAvatarURL(48)); 132 menuitem.style.setProperty("--menu-profiles-theme-bg", themeBg); 133 menuitem.style.setProperty("--menu-profiles-theme-fg", themeFg); 134 135 if (profile.id === currentProfile.id) { 136 menuitem.classList.add("current"); 137 menuitem.setAttribute("data-l10n-id", "menu-profiles-current"); 138 menuitem.setAttribute( 139 "data-l10n-args", 140 JSON.stringify({ profileName: profile.name }) 141 ); 142 } else { 143 menuitem.classList.remove("current"); 144 menuitem.removeAttribute("data-l10n-id"); 145 menuitem.removeAttribute("data-l10n-args"); 146 menuitem.setAttribute("label", profile.name); 147 } 148 149 if (isNewItem) { 150 menuPopup.insertBefore(menuitem, insertionPoint); 151 } 152 } 153 // If there's any old item to remove, do so now. 154 for (let remaining of existingItems) { 155 remaining.remove(); 156 } 157 }, 158 159 manageProfiles() { 160 return SelectableProfileService.maybeSetupDataStore().then(() => { 161 toOpenWindowByType( 162 "about:profilemanager", 163 "about:profilemanager", 164 "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar,centerscreen" 165 ); 166 }); 167 }, 168 169 copyProfile() { 170 SelectableProfileService.maybeSetupDataStore().then(() => { 171 SelectableProfileService.currentProfile.copyProfile(); 172 }); 173 }, 174 175 createNewProfile() { 176 SelectableProfileService.createNewProfile(); 177 }, 178 179 updateView(target) { 180 this.populateSubView(); 181 PanelUI.showSubView("PanelUI-profiles", target); 182 }, 183 184 updateFxAView(target) { 185 this.populateSubView(); 186 PanelUI.showSubView("PanelUI-profiles", target); 187 }, 188 189 launchProfile(aEvent) { 190 SelectableProfileService.getProfile( 191 aEvent.target.getAttribute("profileid") 192 ).then(profile => { 193 SelectableProfileService.launchInstance(profile); 194 }); 195 }, 196 197 async openTabsInProfile(aEvent, tabsToOpen) { 198 let profile = await SelectableProfileService.getProfile( 199 aEvent.target.getAttribute("profileid") 200 ); 201 SelectableProfileService.launchInstance( 202 profile, 203 tabsToOpen.map(tab => tab.linkedBrowser.currentURI.spec) 204 ); 205 }, 206 207 async handleCommand(aEvent) { 208 switch (aEvent.target.id) { 209 /* App menu button events */ 210 case "appMenu-profiles-button": 211 // deliberate fallthrough 212 case "appMenu-empty-profiles-button": { 213 this.updateView(aEvent.target); 214 break; 215 } 216 /* FxA menu button events */ 217 case "PanelUI-fxa-menu-empty-profiles-button": 218 // deliberate fallthrough 219 case "PanelUI-fxa-menu-profiles-button": { 220 aEvent.stopPropagation(); 221 this.updateFxAView(aEvent.target); 222 break; 223 } 224 /* Subpanel events that may be triggered in FxA menu or app menu */ 225 case "profiles-appmenu-back-button": { 226 aEvent.target.closest("panelview").panelMultiView.goBack(); 227 aEvent.target.blur(); 228 break; 229 } 230 case "profiles-edit-this-profile-button": { 231 openTrustedLinkIn("about:editprofile", "tab"); 232 break; 233 } 234 case "profiles-manage-profiles-button": { 235 this.manageProfiles(); 236 break; 237 } 238 case "profiles-copy-profile-button": { 239 this.copyProfile(); 240 break; 241 } 242 case "profiles-create-profile-button": { 243 this.createNewProfile(); 244 break; 245 } 246 247 /* Menubar events */ 248 case "Profiles:CreateProfile": { 249 this.createNewProfile(); 250 break; 251 } 252 case "Profiles:ManageProfiles": { 253 this.manageProfiles(); 254 break; 255 } 256 case "Profiles:LaunchProfile": { 257 this.launchProfile(aEvent.sourceEvent); 258 break; 259 } 260 case "Profiles:MoveTabsToProfile": { 261 let tabs; 262 if (TabContextMenu.contextTab.multiselected) { 263 tabs = gBrowser.selectedTabs; 264 } else { 265 tabs = [TabContextMenu.contextTab]; 266 } 267 this.openTabsInProfile(aEvent.sourceEvent, tabs); 268 break; 269 } 270 } 271 /* Subpanel profile events that may be triggered in FxA menu or app menu */ 272 if (aEvent.target.classList.contains("profile-item")) { 273 this.launchProfile(aEvent); 274 } 275 }, 276 277 /** 278 * Inserts the subpanel contents for the PanelUI subpanel, which may be shown 279 * either in the app menu or the FxA toolbar button menu. 280 */ 281 async populateSubView() { 282 let profiles = []; 283 let currentProfile = null; 284 285 if (SelectableProfileService.initialized) { 286 profiles = await SelectableProfileService.getAllProfiles(); 287 currentProfile = SelectableProfileService.currentProfile; 288 } 289 290 let subview = PanelMultiView.getViewNode(document, "PanelUI-profiles"); 291 292 let backButton = PanelMultiView.getViewNode( 293 document, 294 "profiles-appmenu-back-button" 295 ); 296 backButton.setAttribute( 297 "aria-label", 298 this.bundle.GetStringFromName("panel.back") 299 ); 300 backButton.style.fill = "var(--appmenu-profiles-theme-fg, currentColor)"; 301 302 let currentProfileCard = PanelMultiView.getViewNode( 303 document, 304 "current-profile" 305 ); 306 currentProfileCard.hidden = !(currentProfile && profiles.length > 1); 307 308 let profilesHeader = PanelMultiView.getViewNode( 309 document, 310 "PanelUI-profiles-header" 311 ); 312 313 let editButton = PanelMultiView.getViewNode( 314 document, 315 "profiles-edit-this-profile-button" 316 ); 317 318 let profilesList = PanelMultiView.getViewNode(document, "profiles-list"); 319 // Automatically created by PanelMultiView. 320 const headerSeparator = profilesHeader.nextElementSibling; 321 let footerSeparator = PanelMultiView.getViewNode( 322 document, 323 "footer-separator" 324 ); 325 if (!footerSeparator) { 326 footerSeparator = document.createXULElement("toolbarseparator"); 327 footerSeparator.id = "footer-separator"; 328 } 329 330 let createProfileButton = PanelMultiView.getViewNode( 331 document, 332 "profiles-create-profile-button" 333 ); 334 if (!createProfileButton) { 335 createProfileButton = document.createXULElement("toolbarbutton"); 336 createProfileButton.id = "profiles-create-profile-button"; 337 createProfileButton.classList.add( 338 "subviewbutton", 339 "subviewbutton-iconic" 340 ); 341 createProfileButton.setAttribute( 342 "data-l10n-id", 343 "appmenu-create-profile" 344 ); 345 } 346 347 let copyProfileButton = PanelMultiView.getViewNode( 348 document, 349 "profiles-copy-profile-button" 350 ); 351 352 if (!copyProfileButton) { 353 copyProfileButton = document.createXULElement("toolbarbutton"); 354 copyProfileButton.id = "profiles-copy-profile-button"; 355 copyProfileButton.classList.add("subviewbutton", "subviewbutton-iconic"); 356 copyProfileButton.setAttribute("data-l10n-id", "appmenu-copy-profile"); 357 } 358 359 let manageProfilesButton = PanelMultiView.getViewNode( 360 document, 361 "profiles-manage-profiles-button" 362 ); 363 364 if (!manageProfilesButton) { 365 manageProfilesButton = document.createXULElement("toolbarbutton"); 366 manageProfilesButton.id = "profiles-manage-profiles-button"; 367 manageProfilesButton.classList.add( 368 "subviewbutton", 369 "panel-subview-footer-button" 370 ); 371 manageProfilesButton.setAttribute( 372 "data-l10n-id", 373 "appmenu-manage-profiles" 374 ); 375 } 376 377 if (profiles.length < 2) { 378 profilesHeader.removeAttribute("style"); 379 editButton.hidden = true; 380 381 headerSeparator.hidden = false; 382 footerSeparator.hidden = true; 383 const subviewBody = subview.querySelector(".panel-subview-body"); 384 subview.insertBefore(createProfileButton, subviewBody); 385 subview.insertBefore(copyProfileButton, subviewBody); 386 subview.insertBefore(manageProfilesButton, subviewBody); 387 } else { 388 profilesHeader.style.backgroundColor = "var(--appmenu-profiles-theme-bg)"; 389 profilesHeader.style.color = "var(--appmenu-profiles-theme-fg)"; 390 editButton.hidden = false; 391 } 392 393 if (currentProfile && profiles.length > 1) { 394 let { themeBg, themeFg } = currentProfile.theme; 395 subview.style.setProperty("--appmenu-profiles-theme-bg", themeBg); 396 subview.style.setProperty("--appmenu-profiles-theme-fg", themeFg); 397 398 headerSeparator.hidden = true; 399 footerSeparator.hidden = false; 400 subview.appendChild(footerSeparator); 401 subview.appendChild(createProfileButton); 402 subview.appendChild(copyProfileButton); 403 subview.appendChild(manageProfilesButton); 404 405 let headerText = PanelMultiView.getViewNode( 406 document, 407 "profiles-header-content" 408 ); 409 headerText.textContent = currentProfile.name; 410 411 let profileIconEl = PanelMultiView.getViewNode( 412 document, 413 "profile-icon-image" 414 ); 415 currentProfileCard.style.setProperty( 416 "--appmenu-profiles-theme-bg", 417 themeBg 418 ); 419 currentProfileCard.style.setProperty( 420 "--appmenu-profiles-theme-fg", 421 themeFg 422 ); 423 424 profileIconEl.style.listStyleImage = `url(${await currentProfile.getAvatarURL(80)})`; 425 } 426 427 let subtitle = PanelMultiView.getViewNode(document, "profiles-subtitle"); 428 subtitle.hidden = profiles.length < 2; 429 430 while (profilesList.lastElementChild) { 431 profilesList.lastElementChild.remove(); 432 } 433 for (let profile of profiles) { 434 if (profile.id === SelectableProfileService.currentProfile.id) { 435 continue; 436 } 437 438 let button = document.createXULElement("toolbarbutton"); 439 button.setAttribute("profileid", profile.id); 440 button.setAttribute("label", profile.name); 441 button.className = "subviewbutton subviewbutton-iconic profile-item"; 442 let { themeFg, themeBg } = profile.theme; 443 button.style.setProperty("--appmenu-profiles-theme-bg", themeBg); 444 button.style.setProperty("--appmenu-profiles-theme-fg", themeFg); 445 button.setAttribute("image", await profile.getAvatarURL(16)); 446 447 profilesList.appendChild(button); 448 } 449 }, 450 451 async populateMoveTabMenu(menuPopup) { 452 if (!SelectableProfileService.initialized) { 453 return; 454 } 455 456 const profiles = await SelectableProfileService.getAllProfiles(); 457 const currentProfile = SelectableProfileService.currentProfile; 458 459 const separator = document.getElementById("moveTabSeparator"); 460 separator.hidden = profiles.length < 2; 461 462 let existingItems = [ 463 ...menuPopup.querySelectorAll(":scope > menuitem[profileid]"), 464 ]; 465 466 for (let profile of profiles) { 467 if (profile.id === currentProfile.id) { 468 continue; 469 } 470 471 let menuitem = existingItems.shift(); 472 let isNewItem = !menuitem; 473 if (isNewItem) { 474 menuitem = document.createXULElement("menuitem"); 475 menuitem.setAttribute("tbattr", "tabbrowser-multiple-visible"); 476 menuitem.setAttribute("data-l10n-id", "move-to-new-profile"); 477 menuitem.setAttribute("command", "Profiles:MoveTabsToProfile"); 478 } 479 480 menuitem.disabled = false; 481 menuitem.setAttribute("profileid", profile.id); 482 menuitem.setAttribute( 483 "data-l10n-args", 484 JSON.stringify({ profileName: profile.name }) 485 ); 486 487 if (isNewItem) { 488 menuPopup.appendChild(menuitem); 489 } 490 } 491 // If there's any old item to remove, do so now. 492 for (let remaining of existingItems) { 493 remaining.remove(); 494 } 495 }, 496 };