/* # 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/. */ XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm"); const READINGLIST_COMMAND_ID = "readingListSidebar"; let ReadingListUI = { /** * Frame-script messages we want to listen to. * @type {[string]} */ MESSAGES: [ "ReadingList:GetVisibility", "ReadingList:ToggleVisibility", "ReadingList:ShowIntro", ], /** * Add-to-ReadingList toolbar button in the URLbar. * @type {Element} */ toolbarButton: null, /** * Whether this object is currently registered as a listener with ReadingList. * Used to avoid inadvertantly loading the ReadLingList.jsm module on startup. * @type {Boolean} */ listenerRegistered: false, /** * Initialize the ReadingList UI. */ init() { this.toolbarButton = document.getElementById("readinglist-addremove-button"); Preferences.observe("browser.readinglist.enabled", this.updateUI, this); const mm = window.messageManager; for (let msg of this.MESSAGES) { mm.addMessageListener(msg, this); } this.updateUI(); }, /** * Un-initialize the ReadingList UI. */ uninit() { Preferences.ignore("browser.readinglist.enabled", this.updateUI, this); const mm = window.messageManager; for (let msg of this.MESSAGES) { mm.removeMessageListener(msg, this); } if (this.listenerRegistered) { ReadingList.removeListener(this); this.listenerRegistered = false; } }, /** * Whether the ReadingList feature is enabled or not. * @type {boolean} */ get enabled() { return Preferences.get("browser.readinglist.enabled", false); }, /** * Whether the ReadingList sidebar is currently open or not. * @type {boolean} */ get isSidebarOpen() { return SidebarUI.isOpen && SidebarUI.currentID == READINGLIST_COMMAND_ID; }, /** * Update the UI status, ensuring the UI is shown or hidden depending on * whether the feature is enabled or not. */ updateUI() { let enabled = this.enabled; if (enabled) { // This is a no-op if we're already registered. ReadingList.addListener(this); this.listenerRegistered = true; } else { if (this.listenerRegistered) { // This is safe to call if we're not currently registered, but we don't // want to forcibly load the normally lazy-loaded module on startup. ReadingList.removeListener(this); this.listenerRegistered = false; } this.hideSidebar(); } document.getElementById(READINGLIST_COMMAND_ID).setAttribute("hidden", !enabled); document.getElementById(READINGLIST_COMMAND_ID).setAttribute("disabled", !enabled); }, /** * Show the ReadingList sidebar. * @return {Promise} */ showSidebar() { if (this.enabled) { return SidebarUI.show(READINGLIST_COMMAND_ID); } }, /** * Hide the ReadingList sidebar, if it is currently shown. */ hideSidebar() { if (this.isSidebarOpen) { SidebarUI.hide(); } }, /** * Re-refresh the ReadingList bookmarks submenu when it opens. * * @param {Element} target - Menu element opening. */ onReadingListPopupShowing: Task.async(function* (target) { if (target.id == "BMB_readingListPopup") { // Setting this class in the .xul file messes with the way // browser-places.js inserts bookmarks in the menu. document.getElementById("BMB_viewReadingListSidebar") .classList.add("panel-subview-footer"); } while (!target.firstChild.id) target.firstChild.remove(); let classList = "menuitem-iconic bookmark-item menuitem-with-favicon"; let insertPoint = target.firstChild; if (insertPoint.classList.contains("subviewbutton")) classList += " subviewbutton"; let hasItems = false; yield ReadingList.forEachItem(item => { hasItems = true; let menuitem = document.createElement("menuitem"); menuitem.setAttribute("label", item.title || item.url); menuitem.setAttribute("class", classList); let node = menuitem._placesNode = { // Passing the PlacesUtils.nodeIsURI check is required for the // onCommand handler to load our URI. type: Ci.nsINavHistoryResultNode.RESULT_TYPE_URI, // makes PlacesUIUtils.canUserRemove return false. // The context menu is broken without this. parent: {type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER}, // A -1 id makes this item a non-bookmark, which avoids calling // PlacesUtils.annotations.itemHasAnnotation to check if the // bookmark should be opened in the sidebar (this call fails for // readinglist item, and breaks loading our URI). itemId: -1, // Used by the tooltip and onCommand handlers. uri: item.url, // Used by the tooltip. title: item.title }; Favicons.getFaviconURLForPage(item.uri, uri => { if (uri) { menuitem.setAttribute("image", Favicons.getFaviconLinkForIcon(uri).spec); } }); target.insertBefore(menuitem, insertPoint); }, {sort: "addedOn", descending: true}); if (!hasItems) { let menuitem = document.createElement("menuitem"); let bundle = Services.strings.createBundle("chrome://browser/locale/places/places.properties"); menuitem.setAttribute("label", bundle.GetStringFromName("bookmarksMenuEmptyFolder")); menuitem.setAttribute("class", "bookmark-item"); menuitem.setAttribute("disabled", true); target.insertBefore(menuitem, insertPoint); } }), /** * Hide the ReadingList sidebar, if it is currently shown. */ toggleSidebar() { if (this.enabled) { SidebarUI.toggle(READINGLIST_COMMAND_ID); } }, /** * Respond to messages. */ receiveMessage(message) { switch (message.name) { case "ReadingList:GetVisibility": { if (message.target.messageManager) { message.target.messageManager.sendAsyncMessage("ReadingList:VisibilityStatus", { isOpen: this.isSidebarOpen }); } break; } case "ReadingList:ToggleVisibility": { this.toggleSidebar(); break; } case "ReadingList:ShowIntro": { if (this.enabled && !Preferences.get("browser.readinglist.introShown", false)) { Preferences.set("browser.readinglist.introShown", true); this.showSidebar(); } break; } } }, /** * Handles toolbar button styling based on page proxy state changes. * * @see SetPageProxyState() * * @param {string} state - New state. Either "valid" or "invalid". */ onPageProxyStateChanged: Task.async(function* (state) { if (!this.toolbarButton) { // nothing to do if we have no button. return; } let uri; if (this.enabled && state == "valid") { uri = gBrowser.currentURI; if (uri.schemeIs("about")) uri = ReaderMode.getOriginalUrl(uri.spec); else if (!uri.schemeIs("http") && !uri.schemeIs("https")) uri = null; } let msg = {topic: "UpdateActiveItem", url: null}; if (!uri) { this.toolbarButton.setAttribute("hidden", true); if (this.isSidebarOpen) document.getElementById("sidebar").contentWindow.postMessage(msg, "*"); return; } let isInList = yield ReadingList.hasItemForURL(uri); if (window.closed) { // Skip updating the UI if the window was closed since our hasItemForURL call. return; } if (this.isSidebarOpen) { if (isInList) msg.url = typeof uri == "string" ? uri : uri.spec; document.getElementById("sidebar").contentWindow.postMessage(msg, "*"); } this.setToolbarButtonState(isInList); }), /** * Set the state of the ReadingList toolbar button in the urlbar. * If the current tab's page is in the ReadingList (active), sets the button * to allow removing the page. Otherwise, sets the button to allow adding the * page (not active). * * @param {boolean} active - True if the button should be active (page is * already in the list). */ setToolbarButtonState(active) { this.toolbarButton.setAttribute("already-added", active); let type = (active ? "remove" : "add"); let tooltip = gNavigatorBundle.getString(`readingList.urlbar.${type}`); this.toolbarButton.setAttribute("tooltiptext", tooltip); this.toolbarButton.removeAttribute("hidden"); }, buttonClick(event) { if (event.button != 0) { return; } this.togglePageByBrowser(gBrowser.selectedBrowser); }, /** * Toggle a page (from a browser) in the ReadingList, adding if it's not already added, or * removing otherwise. * * @param {} browser - Browser with page to toggle. * @returns {Promise} Promise resolved when operation has completed. */ togglePageByBrowser: Task.async(function* (browser) { let uri = browser.currentURI; if (uri.spec.startsWith("about:reader?")) uri = ReaderMode.getOriginalUrl(uri.spec); if (!uri) return; let item = yield ReadingList.itemForURL(uri); if (item) { yield item.delete(); } else { yield ReadingList.addItemFromBrowser(browser, uri); } }), /** * Checks if a given item matches the current tab in this window. * * @param {ReadingListItem} item - Item to check * @returns True if match, false otherwise. */ isItemForCurrentBrowser(item) { let currentURL = gBrowser.currentURI.spec; if (currentURL.startsWith("about:reader?")) currentURL = ReaderMode.getOriginalUrl(currentURL); if (item.url == currentURL || item.resolvedURL == currentURL) { return true; } return false; }, /** * ReadingList event handler for when an item is added. * * @param {ReadingListItem} item - Item added. */ onItemAdded(item) { if (!Services.prefs.getBoolPref("browser.readinglist.sidebarEverOpened")) { SidebarUI.show("readingListSidebar"); } if (this.isItemForCurrentBrowser(item)) { this.setToolbarButtonState(true); if (this.isSidebarOpen) { let msg = {topic: "UpdateActiveItem", url: item.url}; document.getElementById("sidebar").contentWindow.postMessage(msg, "*"); } } }, /** * ReadingList event handler for when an item is deleted. * * @param {ReadingListItem} item - Item deleted. */ onItemDeleted(item) { if (this.isItemForCurrentBrowser(item)) { this.setToolbarButtonState(false); } }, };