 |
1 /*
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7 XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
8 "resource:///modules/readinglist/ReadingList.jsm");
9
10 const READINGLIST_COMMAND_ID = "readingListSidebar";
11
12 let ReadingListUI = {
13 /**
14 * Frame-script messages we want to listen to.
15 * @type {[string]}
16 */
17 MESSAGES: [
18 "ReadingList:GetVisibility",
19 "ReadingList:ToggleVisibility",
20 "ReadingList:ShowIntro",
21 ],
22
23 /**
24 * Add-to-ReadingList toolbar button in the URLbar.
25 * @type {Element}
26 */
27 toolbarButton: null,
28
29 /**
30 * Whether this object is currently registered as a listener with ReadingList.
31 * Used to avoid inadvertantly loading the ReadLingList.jsm module on startup.
32 * @type {Boolean}
33 */
34 listenerRegistered: false,
35
36 /**
37 * Initialize the ReadingList UI.
38 */
39 init() {
40 this.toolbarButton = document.getElementById("readinglist-addremove-button");
41
42 Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
43
44 const mm = window.messageManager;
45 for (let msg of this.MESSAGES) {
46 mm.addMessageListener(msg, this);
47 }
48
49 this.updateUI();
50 },
51
52 /**
53 * Un-initialize the ReadingList UI.
54 */
55 uninit() {
56 Preferences.ignore("browser.readinglist.enabled", this.updateUI, this);
57
58 const mm = window.messageManager;
59 for (let msg of this.MESSAGES) {
60 mm.removeMessageListener(msg, this);
61 }
62
63 if (this.listenerRegistered) {
64 ReadingList.removeListener(this);
65 this.listenerRegistered = false;
66 }
67 },
68
69 /**
70 * Whether the ReadingList feature is enabled or not.
71 * @type {boolean}
72 */
73 get enabled() {
74 return Preferences.get("browser.readinglist.enabled", false);
75 },
76
77 /**
78 * Whether the ReadingList sidebar is currently open or not.
79 * @type {boolean}
80 */
81 get isSidebarOpen() {
82 return SidebarUI.isOpen && SidebarUI.currentID == READINGLIST_COMMAND_ID;
83 },
84
85 /**
86 * Update the UI status, ensuring the UI is shown or hidden depending on
87 * whether the feature is enabled or not.
88 */
89 updateUI() {
90 let enabled = this.enabled;
91 if (enabled) {
92 // This is a no-op if we're already registered.
93 ReadingList.addListener(this);
94 this.listenerRegistered = true;
95 } else {
96 if (this.listenerRegistered) {
97 // This is safe to call if we're not currently registered, but we don't
98 // want to forcibly load the normally lazy-loaded module on startup.
99 ReadingList.removeListener(this);
100 this.listenerRegistered = false;
101 }
102
103 this.hideSidebar();
104 }
105
106 document.getElementById(READINGLIST_COMMAND_ID).setAttribute("hidden", !enabled);
107 },
108
109 /**
110 * Show the ReadingList sidebar.
111 * @return {Promise}
112 */
113 showSidebar() {
114 if (this.enabled) {
115 return SidebarUI.show(READINGLIST_COMMAND_ID);
116 }
117 },
118
119 /**
120 * Hide the ReadingList sidebar, if it is currently shown.
121 */
122 hideSidebar() {
123 if (this.isSidebarOpen) {
124 SidebarUI.hide();
125 }
126 },
127
128 /**
129 * Re-refresh the ReadingList bookmarks submenu when it opens.
130 *
131 * @param {Element} target - Menu element opening.
132 */
133 onReadingListPopupShowing: Task.async(function* (target) {
134 if (target.id == "BMB_readingListPopup") {
135 // Setting this class in the .xul file messes with the way
136 // browser-places.js inserts bookmarks in the menu.
137 document.getElementById("BMB_viewReadingListSidebar")
138 .classList.add("panel-subview-footer");
139 }
140
141 while (!target.firstChild.id)
142 target.firstChild.remove();
143
144 let classList = "menuitem-iconic bookmark-item menuitem-with-favicon";
145 let insertPoint = target.firstChild;
146 if (insertPoint.classList.contains("subviewbutton"))
147 classList += " subviewbutton";
148
149 let hasItems = false;
150 yield ReadingList.forEachItem(item => {
151 hasItems = true;
152
153 let menuitem = document.createElement("menuitem");
154 menuitem.setAttribute("label", item.title || item.url);
155 menuitem.setAttribute("class", classList);
156
157 let node = menuitem._placesNode = {
158 // Passing the PlacesUtils.nodeIsURI check is required for the
159 // onCommand handler to load our URI.
160 type: Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
161
162 // makes PlacesUIUtils.canUserRemove return false.
163 // The context menu is broken without this.
164 parent: {type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER},
165
166 // A -1 id makes this item a non-bookmark, which avoids calling
167 // PlacesUtils.annotations.itemHasAnnotation to check if the
168 // bookmark should be opened in the sidebar (this call fails for
169 // readinglist item, and breaks loading our URI).
170 itemId: -1,
171
172 // Used by the tooltip and onCommand handlers.
173 uri: item.url,
174
175 // Used by the tooltip.
176 title: item.title
177 };
178
179 Favicons.getFaviconURLForPage(item.uri, uri => {
180 if (uri) {
181 menuitem.setAttribute("image",
182 Favicons.getFaviconLinkForIcon(uri).spec);
183 }
184 });
185
186 target.insertBefore(menuitem, insertPoint);
187 }, {sort: "addedOn", descending: true});
188
189 if (!hasItems) {
190 let menuitem = document.createElement("menuitem");
191 let bundle =
192 Services.strings.createBundle("chrome://browser/locale/places/places.properties");
193 menuitem.setAttribute("label", bundle.GetStringFromName("bookmarksMenuEmptyFolder"));
194 menuitem.setAttribute("class", "bookmark-item");
195 menuitem.setAttribute("disabled", true);
196 target.insertBefore(menuitem, insertPoint);
197 }
198 }),
199
200 /**
201 * Hide the ReadingList sidebar, if it is currently shown.
202 */
203 toggleSidebar() {
204 if (this.enabled) {
205 SidebarUI.toggle(READINGLIST_COMMAND_ID);
206 }
207 },
208
209 /**
210 * Respond to messages.
211 */
212 receiveMessage(message) {
213 switch (message.name) {
214 case "ReadingList:GetVisibility": {
215 if (message.target.messageManager) {
216 message.target.messageManager.sendAsyncMessage("ReadingList:VisibilityStatus",
217 { isOpen: this.isSidebarOpen });
218 }
219 break;
220 }
221
222 case "ReadingList:ToggleVisibility": {
223 this.toggleSidebar();
224 break;
225 }
226
227 case "ReadingList:ShowIntro": {
228 if (this.enabled && !Preferences.get("browser.readinglist.introShown", false)) {
229 Preferences.set("browser.readinglist.introShown", true);
230 this.showSidebar();
231 }
232 break;
233 }
234 }
235 },
236
237 /**
238 * Handles toolbar button styling based on page proxy state changes.
239 *
240 * @see SetPageProxyState()
241 *
242 * @param {string} state - New state. Either "valid" or "invalid".
243 */
244 onPageProxyStateChanged: Task.async(function* (state) {
245 if (!this.toolbarButton) {
246 // nothing to do if we have no button.
247 return;
248 }
249
250 let uri;
251 if (this.enabled && state == "valid") {
252 uri = gBrowser.currentURI;
253 if (uri.schemeIs("about"))
254 uri = ReaderParent.parseReaderUrl(uri.spec);
255 else if (!uri.schemeIs("http") && !uri.schemeIs("https"))
256 uri = null;
257 }
258
259 let msg = {topic: "UpdateActiveItem", url: null};
260 if (!uri) {
261 this.toolbarButton.setAttribute("hidden", true);
262 if (this.isSidebarOpen)
263 document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
264 return;
265 }
266
267 let isInList = yield ReadingList.hasItemForURL(uri);
268
269 if (window.closed) {
270 // Skip updating the UI if the window was closed since our hasItemForURL call.
271 return;
272 }
273
274 if (this.isSidebarOpen) {
275 if (isInList)
276 msg.url = typeof uri == "string" ? uri : uri.spec;
277 document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
278 }
279 this.setToolbarButtonState(isInList);
280 }),
281
282 /**
283 * Set the state of the ReadingList toolbar button in the urlbar.
284 * If the current tab's page is in the ReadingList (active), sets the button
285 * to allow removing the page. Otherwise, sets the button to allow adding the
286 * page (not active).
287 *
288 * @param {boolean} active - True if the button should be active (page is
289 * already in the list).
290 */
291 setToolbarButtonState(active) {
292 this.toolbarButton.setAttribute("already-added", active);
293
294 let type = (active ? "remove" : "add");
295 let tooltip = gNavigatorBundle.getString(`readingList.urlbar.${type}`);
296 this.toolbarButton.setAttribute("tooltiptext", tooltip);
297
298 this.toolbarButton.removeAttribute("hidden");
299 },
300
301 /**
302 * Toggle a page (from a browser) in the ReadingList, adding if it's not already added, or
303 * removing otherwise.
304 *
305 * @param {<xul:browser>} browser - Browser with page to toggle.
306 * @returns {Promise} Promise resolved when operation has completed.
307 */
308 togglePageByBrowser: Task.async(function* (browser) {
309 let uri = browser.currentURI;
310 if (uri.spec.startsWith("about:reader?"))
311 uri = ReaderParent.parseReaderUrl(uri.spec);
312 if (!uri)
313 return;
314
315 let item = yield ReadingList.itemForURL(uri);
316 if (item) {
317 yield item.delete();
318 } else {
319 yield ReadingList.addItemFromBrowser(browser, uri);
320 }
321 }),
322
323 /**
324 * Checks if a given item matches the current tab in this window.
325 *
326 * @param {ReadingListItem} item - Item to check
327 * @returns True if match, false otherwise.
328 */
329 isItemForCurrentBrowser(item) {
330 let currentURL = gBrowser.currentURI.spec;
331 if (currentURL.startsWith("about:reader?"))
332 currentURL = ReaderParent.parseReaderUrl(currentURL);
333
334 if (item.url == currentURL || item.resolvedURL == currentURL) {
335 return true;
336 }
337 return false;
338 },
339
340 /**
341 * ReadingList event handler for when an item is added.
342 *
343 * @param {ReadingListItem} item - Item added.
344 */
345 onItemAdded(item) {
346 if (!Services.prefs.getBoolPref("browser.readinglist.sidebarEverOpened")) {
347 SidebarUI.show("readingListSidebar");
348 }
349 if (this.isItemForCurrentBrowser(item)) {
350 this.setToolbarButtonState(true);
351 if (this.isSidebarOpen) {
352 let msg = {topic: "UpdateActiveItem", url: item.url};
353 document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
354 }
355 }
356 },
357
358 /**
359 * ReadingList event handler for when an item is deleted.
360 *
361 * @param {ReadingListItem} item - Item deleted.
362 */
363 onItemDeleted(item) {
364 if (this.isItemForCurrentBrowser(item)) {
365 this.setToolbarButtonState(false);
366 }
367 },
368 };
369