www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit d26bb248490b473d536ddddadc8176da010ce8f3
parent f1f4044018c40235c67fe90134ccbcfdd99461f8
Author: Simon Kornblith <simon@simonster.com>
Date:   Sun, 10 Jun 2012 21:56:15 -0400

Merge branch 'attachment-progress'

Diffstat:
Mchrome/content/zotero/browser.js | 119++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mchrome/content/zotero/progressWindow.xul | 4++--
Mchrome/content/zotero/xpcom/attachments.js | 15++++++++++-----
Mchrome/content/zotero/xpcom/connector/translate_item.js | 2+-
Mchrome/content/zotero/xpcom/progressWindow.js | 316+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mchrome/content/zotero/xpcom/server_connector.js | 134++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mchrome/content/zotero/xpcom/translation/translate.js | 144+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mchrome/content/zotero/xpcom/translation/translate_item.js | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mchrome/content/zotero/xpcom/utilities.js | 11+++++++++++
Mchrome/locale/en-US/zotero/zotero.properties | 1+
Mchrome/skin/default/zotero/zotero.css | 15+++++++++++++--
11 files changed, 582 insertions(+), 265 deletions(-)

diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js @@ -50,8 +50,6 @@ var Zotero_Browser = new function() { this.tabClose = tabClose; this.resize = resize; this.updateStatus = updateStatus; - this.finishScraping = finishScraping; - this.itemDone = itemDone; this.tabbrowser = null; this.appcontent = null; @@ -447,51 +445,6 @@ var Zotero_Browser = new function() { } } - /* - * Callback to be executed when scraping is complete - */ - function finishScraping(obj, returnValue) { - if(!returnValue) { - Zotero_Browser.progress.show(); - Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError")); - // Include link to Known Translator Issues page - var url = "http://www.zotero.org/documentation/known_translator_issues"; - var linkText = '<a href="' + url + '" tooltiptext="' + url + '">' - + Zotero.getString('ingester.scrapeErrorDescription.linkText') + '</a>'; - Zotero_Browser.progress.addDescription(Zotero.getString("ingester.scrapeErrorDescription", linkText)); - Zotero_Browser.progress.startCloseTimer(8000); - } else { - Zotero_Browser.progress.startCloseTimer(); - } - Zotero_Browser.isScraping = false; - } - - - /* - * Callback to be executed when an item has been finished - */ - function itemDone(obj, dbItem, item, collection) { - var title = item.title; - var icon = Zotero.ItemTypes.getImageSrc(item.itemType); - Zotero_Browser.progress.show(); - Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scraping")); - Zotero_Browser.progress.addLines([title], [icon]); - - // add item to collection, if one was specified - if(collection) { - collection.addItem(dbItem.id); - } - - if(Zotero_Browser.isScraping) { - // initialize close timer between item saves in case translator doesn't call done - Zotero_Browser.progress.startCloseTimer(10000); // is this long enough? - } else { - // if we aren't supposed to be scraping now, the translator is broken; assume we're - // done - Zotero_Browser.progress.startCloseTimer(); - } - } - /** * Called when status bar icon is right-clicked */ @@ -725,6 +678,28 @@ Zotero_Browser.Tab.prototype.translate = function(libraryID, collectionID, trans var collection = false; } + if(Zotero.isConnector) { + Zotero.Connector.callMethod("getSelectedCollection", {}, function(response, status) { + if(status !== 200) return; + Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapingTo"), + "chrome://zotero/skin/treesource-"+(response.id ? "collection" : "library")+".png", + response.name+"\u2026"); + }); + } else { + var name; + if(collection) { + name = collection.name; + } else if(libraryID) { + name = Zotero.Libraries.getName(libraryID); + } else { + name = Zotero.getString("pane.collections.library"); + } + + Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapingTo"), + "chrome://zotero/skin/treesource-"+(collection ? "collection" : "library")+".png", + name+"\u2026"); + } + var me = this; // use first translator available @@ -733,8 +708,54 @@ Zotero_Browser.Tab.prototype.translate = function(libraryID, collectionID, trans this.page.translate.clearHandlers("done"); this.page.translate.clearHandlers("itemDone"); - this.page.translate.setHandler("done", function(obj, item) { Zotero_Browser.finishScraping(obj, item) }); - this.page.translate.setHandler("itemDone", function(obj, dbItem, item) { Zotero_Browser.itemDone(obj, dbItem, item, collection) }); + this.page.translate.setHandler("done", function(obj, returnValue) { + if(!returnValue) { + Zotero_Browser.progress.show(); + Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError")); + // Include link to Known Translator Issues page + var url = "http://www.zotero.org/documentation/known_translator_issues"; + var linkText = '<a href="' + url + '" tooltiptext="' + url + '">' + + Zotero.getString('ingester.scrapeErrorDescription.linkText') + '</a>'; + Zotero_Browser.progress.addDescription(Zotero.getString("ingester.scrapeErrorDescription", linkText)); + Zotero_Browser.progress.startCloseTimer(8000); + } else { + Zotero_Browser.progress.startCloseTimer(); + } + Zotero_Browser.isScraping = false; + }); + + var attachmentsMap = new WeakMap(); + + this.page.translate.setHandler("itemDone", function(obj, dbItem, item) { + Zotero_Browser.progress.show(); + var itemProgress = new Zotero_Browser.progress.ItemProgress(Zotero.ItemTypes.getImageSrc(item.itemType), + item.title); + itemProgress.setProgress(100); + for(var i=0; i<item.attachments.length; i++) { + var attachment = item.attachments[i]; + attachmentsMap.set(attachment, + new Zotero_Browser.progress.ItemProgress( + Zotero.Utilities.determineAttachmentIcon(attachment), + attachment.title, itemProgress)); + } + + // add item to collection, if one was specified + if(collection) { + collection.addItem(dbItem.id); + } + }); + + this.page.translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) { + var itemProgress = attachmentsMap.get(attachment); + if(progress === false) { + itemProgress.setError(); + } else { + itemProgress.setProgress(progress); + if(progress === 100) { + itemProgress.setIcon(Zotero.Utilities.determineAttachmentIcon(attachment)); + } + } + }); this.page.translate.translate(libraryID); } diff --git a/chrome/content/zotero/progressWindow.xul b/chrome/content/zotero/progressWindow.xul @@ -8,8 +8,8 @@ windowtype="alert:alert"> <hbox id="zotero-progress-box"> - <vbox id="zotero-progress-text-box"> - <label id="zotero-progress-text-headline"/> + <vbox id="zotero-progress-text-box" flex="1"> + <hbox id="zotero-progress-text-headline" pack="start"/> </vbox> </hbox> </window> diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js @@ -220,6 +220,7 @@ Zotero.Attachments = new function(){ var urlRe = /^https?:\/\/[^\s]*$/; var matches = urlRe.exec(url); if (!matches) { + if(callback) callback(false); throw ("Invalid URL '" + url + "' in Zotero.Attachments.importFromURL()"); } @@ -297,9 +298,11 @@ Zotero.Attachments = new function(){ if (mimeType == 'application/pdf' && Zotero.MIME.sniffForMIMEType(str) != 'application/pdf') { - Zotero.debug("Downloaded PDF did not have MIME type " - + "'application/pdf' in Attachments.importFromURL()", 2); + var errString = "Downloaded PDF did not have MIME type " + + "'application/pdf' in Attachments.importFromURL()"; + Zotero.debug(errString, 2); attachmentItem.erase(); + if(callback) callback(false, new Error(errString)); return; } @@ -311,6 +314,8 @@ Zotero.Attachments = new function(){ Zotero.Notifier.trigger('add', 'item', itemID); Zotero.Notifier.trigger('modify', 'item', sourceItemID); + + if(callback) callback(attachmentItem); // We don't have any way of knowing that the file // is flushed to disk, so we just wait a second @@ -325,6 +330,7 @@ Zotero.Attachments = new function(){ catch (e) { // Clean up attachmentItem.erase(); + if(callback) callback(false, e); throw (e); } @@ -346,8 +352,6 @@ Zotero.Attachments = new function(){ nsIURL.spec = url; wbp.saveURI(nsIURL, null, null, null, null, file); - if(callback) callback(attachmentItem); - return attachmentItem; } catch (e){ @@ -553,7 +557,7 @@ Zotero.Attachments = new function(){ Zotero.Fulltext.indexDocument(document, itemID); Zotero.Notifier.trigger('refresh', 'item', itemID); if (callback) { - callback(); + callback(attachmentItem); } }; } @@ -612,6 +616,7 @@ Zotero.Attachments = new function(){ // Clean up var item = Zotero.Items.get(itemID); item.erase(); + if(callback) callback(false, e); throw (e); } diff --git a/chrome/content/zotero/xpcom/connector/translate_item.js b/chrome/content/zotero/xpcom/connector/translate_item.js @@ -115,7 +115,7 @@ Zotero.Translate.ItemSaver.prototype = { if(typedArraysSupported) { // Get rid of attachments that we won't be able to save properly and add ids for(var j=0; j<item.attachments.length; j++) { - if(!item.attachments[j].url || item.attachments[j].mimeType === "text/html") { + if(item.attachments[j].url && item.attachments[j].mimeType !== "text/html") { item.attachments.splice(j--, 1); } else { item.attachments[j].id = Zotero.Utilities.randomString(); diff --git a/chrome/content/zotero/xpcom/progressWindow.js b/chrome/content/zotero/xpcom/progressWindow.js @@ -107,30 +107,22 @@ Zotero.ProgressWindowSet = new function() { * Pass the active window into the constructor */ Zotero.ProgressWindow = function(_window){ - this.show = show; - this.changeHeadline = changeHeadline; - this.addLines = addLines; - this.addDescription = addDescription; - this.startCloseTimer = startCloseTimer; - this.close = close; + var self = this, + _window = null, + _progressWindow = null, + _windowLoaded = false, + _windowLoading = false, + _timeoutID = false, + _closing = false, + _mouseWasOver = false, + _deferredUntilWindowLoad = [], + _deferredUntilWindowLoadThis = [], + _deferredUntilWindowLoadArgs = []; - var _window = null; - - var _progressWindow = null; - var _windowLoaded = false; - var _windowLoading = false; - var _timeoutID = false; - var _mouseWasOver = false - - // keep track of all of these things in case they're called before we're - // done loading the progress window - var _loadHeadline = ''; - var _loadLines = []; - var _loadIcons = []; - var _loadDescription = null; - - - function show() { + /** + * Shows the progress window + */ + this.show = function show() { if(_windowLoading || _windowLoaded) { // already loading or loaded return false; } @@ -164,89 +156,90 @@ Zotero.ProgressWindow = function(_window){ return true; } - function changeHeadline(headline) { - if(_windowLoaded) { - _progressWindow.document.getElementById("zotero-progress-text-headline").value = headline; - } else { - _loadHeadline = headline; + /** + * Changes the "headline" shown at the top of the progress window + */ + this.changeHeadline = _deferUntilWindowLoad(function changeHeadline(text, icon, postText) { + var doc = _progressWindow.document, + headline = doc.getElementById("zotero-progress-text-headline"); + while(headline.hasChildNodes()) headline.removeChild(headline.firstChild); + + var preNode = doc.createElement("label"); + preNode.setAttribute("value", text); + preNode.setAttribute("crop", "end"); + headline.appendChild(preNode); + + if(icon) { + var img = doc.createElement("image"); + img.width = 16; + img.height = 16; + img.setAttribute("src", icon); + headline.appendChild(img); } - } - - function addLines(labels, icons) { - if(_windowLoaded) { - for (var i in labels) { - var newText = _progressWindow.document.createElement("description"); - newText.appendChild( - _progressWindow.document.createTextNode(labels[i]) - ); - newText.setAttribute("class", "zotero-progress-item-label"); - newText.setAttribute("crop", "end"); - - var newImageHolder = _progressWindow.document.createElement("vbox"); - var newImage = _progressWindow.document.createElement("image"); - newImage.setAttribute("class", "zotero-progress-item-icon"); - newImage.setAttribute("src", icons[i]); - newImage.setAttribute("flex", 0); - newImage.setAttribute("orient", "horizontal"); - newImage.setAttribute("pack", "start"); - newImageHolder.appendChild(newImage); - - var newHB = _progressWindow.document.createElement("hbox"); - newHB.setAttribute("class", "zotero-progress-item-hbox"); - - newHB.appendChild(newImageHolder); - newHB.appendChild(newText); - - _progressWindow.document.getElementById("zotero-progress-text-box").appendChild(newHB); - } - - _move(); - } else { - _loadLines = _loadLines.concat(labels); - _loadIcons = _loadIcons.concat(icons); + + if(postText) { + var postNode = doc.createElement("label"); + postNode.style.marginLeft = 0; + postNode.setAttribute("value", " "+postText); + postNode.setAttribute("crop", "end"); + postNode.setAttribute("flex", "1"); + headline.appendChild(postNode); } - } + }); + /** + * Adds a line to the progress window with the specified icon + */ + this.addLines = _deferUntilWindowLoad(function addLines(labels, icons) { + for (var i in labels) { + new this.ItemProgress(icons[i], labels[i]); + } + + _move(); + }); - /* + /** * Add a description to the progress window * * <a> elements are turned into XUL links */ - function addDescription(text) { - if(_windowLoaded) { - var newHB = _progressWindow.document.createElement("hbox"); - newHB.setAttribute("class", "zotero-progress-item-hbox"); - var newDescription = _progressWindow.document.createElement("description"); - - var parts = Zotero.Utilities.parseMarkup(text); - for each(var part in parts) { - if (part.type == 'text') { - var elem = _progressWindow.document.createTextNode(part.text); - } - else if (part.type == 'link') { - var elem = _progressWindow.document.createElement('label'); - elem.setAttribute('value', part.text); - elem.setAttribute('class', 'zotero-text-link'); - for (var i in part.attributes) { - elem.setAttribute(i, part.attributes[i]); - } + this.addDescription = _deferUntilWindowLoad(function addDescription(text) { + var newHB = _progressWindow.document.createElement("hbox"); + newHB.setAttribute("class", "zotero-progress-item-hbox"); + var newDescription = _progressWindow.document.createElement("description"); + + var parts = Zotero.Utilities.parseMarkup(text); + for each(var part in parts) { + if (part.type == 'text') { + var elem = _progressWindow.document.createTextNode(part.text); + } + else if (part.type == 'link') { + var elem = _progressWindow.document.createElement('label'); + elem.setAttribute('value', part.text); + elem.setAttribute('class', 'zotero-text-link'); + for (var i in part.attributes) { + elem.setAttribute(i, part.attributes[i]); } - - newDescription.appendChild(elem); } - newHB.appendChild(newDescription); - _progressWindow.document.getElementById("zotero-progress-text-box").appendChild(newHB); - - _move(); - } else { - _loadDescription = text; + newDescription.appendChild(elem); } - } - + + newHB.appendChild(newDescription); + _progressWindow.document.getElementById("zotero-progress-text-box").appendChild(newHB); + + _move(); + }); - function startCloseTimer(ms, requireMouseOver) { + /** + * Sets a timer to close the progress window. If a previous close timer was set, + * clears it. + * @param {Integer} ms The number of milliseconds to wait before closing the progress + * window. + * @param {Boolean} [requireMouseOver] If true, wait until the mouse has touched the + * window before closing. + */ + this.startCloseTimer = function startCloseTimer(ms, requireMouseOver) { if (_windowLoaded || _windowLoading) { if (requireMouseOver && !_mouseWasOver) { return; @@ -261,10 +254,14 @@ Zotero.ProgressWindow = function(_window){ } _timeoutID = _progressWindow.setTimeout(_timeout, ms); + _closing = true; } } - function close() { + /** + * Immediately closes the progress window if it is open. + */ + this.close = function close() { _disableTimeout(); _windowLoaded = false; _windowLoading = false; @@ -275,23 +272,104 @@ Zotero.ProgressWindow = function(_window){ } catch(ex) {} } + /** + * Creates a new object representing a line in the progressWindow. This is the OO + * version of addLines() above. + */ + this.ItemProgress = _deferUntilWindowLoad(function(iconSrc, title, parentItemProgress) { + this._itemText = _progressWindow.document.createElement("description"); + this._itemText.appendChild(_progressWindow.document.createTextNode(title)); + this._itemText.setAttribute("class", "zotero-progress-item-label"); + this._itemText.setAttribute("crop", "end"); + + this._image = _progressWindow.document.createElement("hbox"); + this._image.setAttribute("class", "zotero-progress-item-icon"); + this._image.setAttribute("flex", 0); + this._image.style.width = "16px"; + this._image.style.backgroundRepeat = "no-repeat"; + this.setIcon(iconSrc); + + this._hbox = _progressWindow.document.createElement("hbox"); + this._hbox.setAttribute("class", "zotero-progress-item-hbox"); + if(parentItemProgress) { + this._hbox.style.marginLeft = "16px"; + this._hbox.zoteroIsChildItem; + } else { + this._hbox.setAttribute("parent", "true"); + } + this._hbox.style.opacity = "0.5"; + + this._hbox.appendChild(this._image); + this._hbox.appendChild(this._itemText); + + var container = _progressWindow.document.getElementById("zotero-progress-text-box"); + if(parentItemProgress) { + var nextItem = parentItemProgress._hbox.nextSibling; + while(nextItem && nextItem.zoteroIsChildItem) { + nextItem = nextItem.nextSibling; + } + container.insertBefore(this._hbox, nextItem); + } else { + container.appendChild(this._hbox); + } + + _move(); + }); + + /** + * Sets the current save progress for this item. + * @param {Integer} percent A percentage from 0 to 100. + */ + this.ItemProgress.prototype.setProgress = _deferUntilWindowLoad(function(percent) { + if(percent != 0 && percent != 100) { + // Indication of partial progress, so we will use the circular indicator + this._image.style.backgroundImage = "url('chrome://zotero/skin/progress_arcs.png')"; + this._image.style.backgroundPosition = "-"+(Math.round(percent/100*nArcs)*16)+"px 0"; + this._hbox.style.opacity = percent/200+.5; + this._hbox.style.filter = "alpha(opacity = "+(percent/2+50)+")"; + } else if(percent == 100) { + this._image.style.backgroundImage = "url('"+this._iconSrc+"')"; + this._image.style.backgroundPosition = ""; + this._hbox.style.opacity = "1"; + this._hbox.style.filter = ""; + } + }); + + /** + * Sets the icon for this item. + * @param {Integer} percent A percentage from 0 to 100. + */ + this.ItemProgress.prototype.setIcon = _deferUntilWindowLoad(function(iconSrc) { + this._image.style.backgroundImage = "url('"+iconSrc+"')"; + this._image.style.backgroundPosition = ""; + this._iconSrc = iconSrc; + }); + + /** + * Indicates that an error occurred saving this item. + */ + this.ItemProgress.prototype.setError = _deferUntilWindowLoad(function() { + this._image.style.backgroundImage = "url('chrome://zotero/skin/cross.png')"; + this._image.style.backgroundPosition = ""; + this._itemText.style.color = "red"; + this._hbox.style.opacity = "1"; + this._hbox.style.filter = ""; + }); + function _onWindowLoaded() { _windowLoading = false; _windowLoaded = true; _move(); + // do things we delayed because the window was loading - changeHeadline(_loadHeadline); - addLines(_loadLines, _loadIcons); - if (_loadDescription) { - addDescription(_loadDescription); + for(var i=0; i<_deferredUntilWindowLoad.length; i++) { + _deferredUntilWindowLoad[i].apply(_deferredUntilWindowLoadThis[i], + _deferredUntilWindowLoadArgs[i]); } - - // reset parameters - _loadHeadline = ''; - _loadLines = []; - _loadIcons = []; - _loadDescription = null; + _deferredUntilWindowLoad = []; + _deferredUntilWindowLoadThis = []; + _deferredUntilWindowLoadArgs = []; } function _move() { @@ -303,8 +381,8 @@ Zotero.ProgressWindow = function(_window){ } function _timeout() { - close(); // could check to see if we're really supposed to close yet - // (in case multiple scrapers are operating at once) + self.close(); // could check to see if we're really supposed to close yet + // (in case multiple scrapers are operating at once) _timeoutID = false; } @@ -319,7 +397,6 @@ Zotero.ProgressWindow = function(_window){ _timeoutID = false; } - /* * Disable the close timer when the mouse is over the window */ @@ -328,8 +405,7 @@ Zotero.ProgressWindow = function(_window){ _disableTimeout(); } - - /* + /** * Start the close timer when the mouse leaves the window * * Note that this onmouseout doesn't work correctly on popups in Fx2, @@ -345,11 +421,27 @@ Zotero.ProgressWindow = function(_window){ && (e.screenY >= top) && e.screenY <= (top + this.outerHeight)) { return; } - startCloseTimer(); + if(_closing) self.startCloseTimer(); } - function _onMouseUp(e) { - close(); + self.close(); + } + + /** + * Wraps a function to ensure it isn't called until the window is loaded + */ + function _deferUntilWindowLoad(fn) { + return function() { + if(_window.closed) return; + + if(_windowLoaded) { + fn.apply(this, Array.prototype.slice.call(arguments)); + } else { + _deferredUntilWindowLoad.push(fn); + _deferredUntilWindowLoadThis.push(this); + _deferredUntilWindowLoadArgs.push(Array.prototype.slice.call(arguments)); + } + } } } diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js @@ -27,6 +27,35 @@ const CONNECTOR_API_VERSION = 2; Zotero.Server.Connector = function() {}; Zotero.Server.Connector._waitingForSelection = {}; Zotero.Server.Connector.Data = {}; +Zotero.Server.Connector.AttachmentProgressManager = new function() { + var attachmentsInProgress = new WeakMap(), + attachmentProgress = {}, + i = 1; + + /** + * Adds attachments to attachment progress manager + */ + this.add = function(attachments) { + for(var i=0; i<attachments.length; i++) { + var attachment = attachments[i]; + attachmentsInProgress.set(attachment, (attachment.id = i++)); + } + } + + /** + * Called on attachment progress + */ + this.onProgress = function(attachment, progress, error) { + attachmentProgress[attachmentsInProgress.get(attachment)] = progress; + }; + + /** + * Gets progress for a given progressID + */ + this.getProgressForID = function(progressID) { + return progressID in attachmentProgress ? attachmentProgress[progressID] : 0; + }; +}; /** * Lists all available translators, including code for translators that should be run on every page @@ -169,7 +198,7 @@ Zotero.Server.Connector.Detect.prototype = { * cookie - document.cookie or equivalent * * Returns: - * If a single item, sends response code 201 with no body. + * If a single item, sends response code 201 with item in body. * If multiple items, sends response code 300 with the following content: * items - list of items in the format typically passed to the selectItems handler * instanceID - an ID that must be maintained for the subsequent Zotero.Connector.Select call @@ -246,9 +275,14 @@ Zotero.Server.Connector.SavePage.prototype = { if(collection) { collection.addItem(item.id); } + jsonItems.push(jsonItem); }); - translate.setHandler("done", function(obj, item) { + translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) { + Zotero.Server.Connector.AttachmentProgressManager.onProgress(attachment, progress, error); + }); + translate.setHandler("itemsDone", function(obj, item) { + Zotero.Server.Connector.AttachmentProgressManager.add(item.attachments); Zotero.Browser.deleteHiddenBrowser(me._browser); if(jsonItems.length || me.selectedItems === false) { me.sendResponse(201, "application/json", JSON.stringify({"items":jsonItems})); @@ -269,7 +303,7 @@ Zotero.Server.Connector.SavePage.prototype = { * Accepts: * items - an array of JSON format items * Returns: - * 201 response code with empty body + * 201 response code with item in body. */ Zotero.Server.Connector.SaveItem = function() {}; Zotero.Server.Endpoints["/connector/saveItems"] = Zotero.Server.Connector.SaveItem; @@ -299,22 +333,23 @@ Zotero.Server.Connector.SaveItem.prototype = { // save items var itemSaver = new Zotero.Translate.ItemSaver(libraryID, Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD, 1, undefined, cookieSandbox); - itemSaver.saveItems(data.items, function(returnValue, data) { + itemSaver.saveItems(data.items, function(returnValue, newItems) { if(returnValue) { try { - for each(var item in data) { + for each(var item in newItems) { if(collection) collection.addItem(item.id); } - sendResponseCallback(201); + + sendResponseCallback(201, "application/json", JSON.stringify({"items":data.items})); } catch(e) { Zotero.logError(e); sendResponseCallback(500); } } else { sendResponseCallback(500); - throw data; + throw newItems; } - }); + }, Zotero.Server.Connector.AttachmentProgressManager.onProgress); } } @@ -435,6 +470,31 @@ Zotero.Server.Connector.SelectItems.prototype = { } /** + * Gets progress for an attachment that is currently being saved + * + * Accepts: + * Array of attachment IDs returned by savePage, saveItems, or saveSnapshot + * Returns: + * 200 response code with current progress in body. Progress is either a number + * between 0 and 100 or "false" to indicate that saving failed. + */ +Zotero.Server.Connector.Progress = function() {}; +Zotero.Server.Endpoints["/connector/attachmentProgress"] = Zotero.Server.Connector.Progress; +Zotero.Server.Connector.Progress.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * @param {String} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(data, sendResponseCallback) { + sendResponseCallback(200, "application/json", + JSON.stringify([Zotero.Server.Connector.AttachmentProgressManager.getProgressForID(id) for each(id in data)])); + } +}; + +/** * Get code for a translator * * Accepts: @@ -460,6 +520,64 @@ Zotero.Server.Connector.GetTranslatorCode.prototype = { } /** + * Get selected collection + * + * Accepts: + * Nothing + * Returns: + * libraryID + * libraryName + * collectionID + * collectionName + */ +Zotero.Server.Connector.GetSelectedCollection = function() {}; +Zotero.Server.Endpoints["/connector/getSelectedCollection"] = Zotero.Server.Connector.GetSelectedCollection; +Zotero.Server.Connector.GetSelectedCollection.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Returns a 200 response to say the server is alive + * @param {String} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(postData, sendResponseCallback) { + var zp = Zotero.getActiveZoteroPane(), + libraryID = null, + collection = null, + editable = true; + + try { + libraryID = zp.getSelectedLibraryID(); + collection = zp.getSelectedCollection(); + editable = zp.collectionsView.editable; + } catch(e) {} + + var response = { + "editable":editable, + "libraryID":libraryID + }; + + if(libraryID) { + response.libraryName = Zotero.Libraries.getName(libraryID); + } else { + response.libraryName = Zotero.getString("pane.collections.library"); + } + + if(collection && collection.id) { + response.id = collection.id; + response.name = collection.name; + } else { + response.id = null; + response.name = response.libraryName; + } + + sendResponseCallback(200, "application/json", JSON.stringify(response)); + } +} + + +/** * Test connection * * Accepts: diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js @@ -130,28 +130,16 @@ Zotero.Translate.Sandbox = { } } } + + // Fire itemSaving event + translate._runHandler("itemSaving", item); if(translate instanceof Zotero.Translate.Web) { // For web translators, we queue saves translate.saveQueue.push(item); - translate._runHandler("itemSaving", item); } else { - var newItem; - translate._itemSaver.saveItems([item], function(returnValue, data) { - if(returnValue) { - newItem = data[0]; - translate.newItems.push(newItem); - } else { - translate.complete(false, data); - throw data; - } - }, function(arg1, arg2, arg3) { - translate._attachmentProgress(arg1, arg2, arg3); - }); - - translate._runHandler("itemSaving", item); - // pass both the saved item and the original JS array item - translate._runHandler("itemDone", newItem, item); + // Save items + translate._saveItems([item]); } }, @@ -1014,7 +1002,8 @@ Zotero.Translate.Base.prototype = { this._libraryID = libraryID; this._saveAttachments = saveAttachments === undefined || saveAttachments; - this._attachmentsSaving = []; + this._savingAttachments = []; + this._savingItems = 0; var me = this; if(typeof this.translator[0] === "object") { @@ -1143,14 +1132,11 @@ Zotero.Translate.Base.prototype = { if(returnValue) { if(this.saveQueue.length) { - var me = this; - this._itemSaver.saveItems(this.saveQueue.slice(), - function(returnValue, data) { me._itemsSaved(returnValue, data); }, - function(arg1, arg2, arg3) { me._attachmentProgress(arg1, arg2, arg3); }); + this._saveItems(this.saveQueue); + this.saveQueue = []; return; - } else { - this._debug("Translation successful"); } + this._debug("Translation successful"); } else { if(error) { // report error to console @@ -1176,61 +1162,79 @@ Zotero.Translate.Base.prototype = { }, /** - * Callback executed when items have been saved (which may happen asynchronously, if in - * connector) - * - * @param {Boolean} returnValue Whether saving was successful - * @param {Zotero.Item[]|Error} data If returnValue is true, this will be an array of - * Zotero.Item objects. If returnValue is false, this will - * be a string error message. + * Saves items to the database, taking care to defer attachmentProgress notifications + * until after save */ - "_itemsSaved":function(returnValue, data) { - if(returnValue) { - // trigger deferred itemDone events - var nItems = data.length; - for(var i=0; i<nItems; i++) { - this._runHandler("itemDone", data[i], this.saveQueue[i]); + "_saveItems":function(items) { + var me = this, + itemDoneEventsDispatched = false, + deferredProgress = [], + attachmentsWithProgress = []; + + this._savingItems++; + this._itemSaver.saveItems(items.slice(), function(returnValue, newItems) { + if(returnValue) { + // Remove attachments not being saved from item.attachments + for(var i=0; i<items.length; i++) { + var item = items[i]; + for(var j=0; j<item.attachments.length; j++) { + if(attachmentsWithProgress.indexOf(item.attachments[j]) === -1) { + item.attachments.splice(j--, 1); + } + } + } + + // Trigger itemDone events + for(var i=0, nItems = items.length; i<nItems; i++) { + me._runHandler("itemDone", newItems[i], items[i]); + } + + // Specify that itemDone event was dispatched, so that we don't defer + // attachmentProgress notifications anymore + itemDoneEventsDispatched = true; + + // Run deferred attachmentProgress notifications + for(var i=0; i<deferredProgress.length; i++) { + me._runHandler("attachmentProgress", deferredProgress[i][0], + deferredProgress[i][1], deferredProgress[i][2]); + } + + me.newItems = me.newItems.concat(newItems); + me._savingItems--; + me._checkIfDone(); + } else { + Zotero.logError(newItems); + me.complete(returnValue, newItems); + } + }, + function(attachment, progress, error) { + var attachmentIndex = me._savingAttachments.indexOf(attachment); + if(progress === false || progress === 100) { + if(attachmentIndex !== -1) { + me._savingAttachments.splice(attachmentIndex, 1); + } + } else if(attachmentIndex === -1) { + me._savingAttachments.push(attachment); } - this.saveQueue = []; - } else { - Zotero.logError(data); - } - - if(returnValue) { - this._checkIfDone(); - } else { - this._runHandler("done", returnValue); - } - }, - - /** - * Callback for attachment progress, passed as third argument to Zotero.ItemSaver#saveItems - * - * @param {Object} attachment Attachment object to be saved. Should remain the same between - * repeated calls to callback. - * @param {Boolean|Number} progress Percent complete, or false if an error occurred. - * @param {Error} [error] Error, if an error occurred during saving. - */ - "_attachmentProgress":function(attachment, progress, error) { - Zotero.debug("Attachment progress (progress = "+progress+")"); - Zotero.debug(attachment); - var attachmentIndex = this._attachmentsSaving.indexOf(attachment); - if((progress === false || progress === 100) && attachmentIndex !== -1) { - this._attachmentsSaving.splice(attachmentIndex, 1); - } else if(attachmentIndex === -1) { - this._attachmentsSaving.push(attachment); - } - - this._runHandler("attachmentProgress", attachment, progress, error); - this._checkIfDone(); + if(itemDoneEventsDispatched) { + // itemDone event has already fired, so we can fire attachmentProgress + // notifications + me._runHandler("attachmentProgress", attachment, progress, error); + me._checkIfDone(); + } else { + // Defer until after we fire the itemDone event + deferredProgress.push([attachment, progress, error]); + attachmentsWithProgress.push(attachment); + } + }); }, /** * Checks if saving done, and if so, fires done event */ "_checkIfDone":function() { - if(!this._attachmentsSaving.length) { + if(!this._savingItems && !this._savingAttachments.length && !this._currentState) { this._runHandler("done", true); } }, diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js @@ -87,7 +87,18 @@ Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1; Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2; Zotero.Translate.ItemSaver.prototype = { - "saveItems":function(items, callback) { + /** + * Saves items to Standalone or the server + * @param items Items in Zotero.Item.toArray() format + * @param {Function} callback A callback to be executed when saving is complete. If saving + * succeeded, this callback will be passed true as the first argument and a list of items + * saved as the second. If saving failed, the callback will be passed false as the first + * argument and an error object as the second + * @param {Function} [attachmentCallback] A callback that receives information about attachment + * save progress. The callback will be called as attachmentCallback(attachment, false, error) + * on failure or attachmentCallback(attachment, progressPercent) periodically during saving. + */ + "saveItems":function(items, callback, attachmentCallback) { // if no open transaction, open a transaction and add a timer call to close it var openedTransaction = false; if(!Zotero.DB.transactionInProgress()) { @@ -110,8 +121,8 @@ Zotero.Translate.ItemSaver.prototype = { newItem = Zotero.Items.get(myID); } else { if(type == "attachment") { // handle attachments differently - newItem = this._saveAttachment(item); - if(!newItem) return; + newItem = this._saveAttachment(item, null, attachmentCallback); + if(!newItem) continue; var myID = newItem.id; } else { var typeID = Zotero.ItemTypes.getID(type); @@ -137,8 +148,10 @@ Zotero.Translate.ItemSaver.prototype = { // handle attachments if(item.attachments) { for(var i=0; i<item.attachments.length; i++) { - var newAttachment = this._saveAttachment(item.attachments[i], myID); - if(newAttachment) this._saveTags(item.attachments[i], newAttachment); + var newAttachment = this._saveAttachment(item.attachments[i], myID, attachmentCallback); + if(typeof newAttachment === "object") { + this._saveTags(item.attachments[i], newAttachment); + } } } } @@ -203,13 +216,13 @@ Zotero.Translate.ItemSaver.prototype = { return topLevelCollection; }, - "_saveAttachmentFile":function(attachment, parentID) { + "_saveAttachmentFile":function(attachment, parentID, attachmentCallback) { const urlRe = /(([A-Za-z]+):\/\/[^\s]*)/i; Zotero.debug("Translate: Adding attachment", 4); if(!attachment.url && !attachment.path) { Zotero.debug("Translate: Ignoring attachment: no path or URL specified", 2); - return; + return false; } if(!attachment.path) { @@ -221,34 +234,40 @@ Zotero.Translate.ItemSaver.prototype = { attachment.url = false; } else if(protocol != "http" && protocol != "https") { Zotero.debug("Translate: Unrecognized protocol "+protocol, 2); - return; + return false; } } if(!attachment.path) { // create from URL + attachment.linkMode = "linked_file"; try { var myID = Zotero.Attachments.linkFromURL(attachment.url, parentID, (attachment.mimeType ? attachment.mimeType : undefined), (attachment.title ? attachment.title : undefined)); } catch(e) { Zotero.debug("Translate: Error adding attachment "+attachment.url, 2); - return; + attachmentCallback(attachment, false, e); + return false; } Zotero.debug("Translate: Created attachment; id is "+myID, 4); + attachmentCallback(attachment, 100); var newItem = Zotero.Items.get(myID); } else { var file = this._parsePath(attachment.path); if(!file || !file.exists()) return; if (attachment.url) { + attachment.linkMode = "imported_url"; var myID = Zotero.Attachments.importSnapshotFromFile(file, attachment.url, attachment.title, attachment.mimeType, attachment.charset, parentID); } else { + attachment.linkMode = "imported_file"; var myID = Zotero.Attachments.importFromFile(file, parentID); } + attachmentCallback(attachment, 100); } var newItem = Zotero.Items.get(myID); @@ -306,7 +325,7 @@ Zotero.Translate.ItemSaver.prototype = { return file; }, - "_saveAttachmentDownload":function(attachment, parentID) { + "_saveAttachmentDownload":function(attachment, parentID, attachmentCallback) { Zotero.debug("Translate: Adding attachment", 4); // determine whether to save attachments at all @@ -319,7 +338,7 @@ Zotero.Translate.ItemSaver.prototype = { var shouldAttach = ((attachment.document || (attachment.mimeType && attachment.mimeType == "text/html")) && automaticSnapshots) || downloadAssociatedFiles; - if(!shouldAttach) return; + if(!shouldAttach) return false; if(attachment.document) { attachment.document = Zotero.Translate.DOMWrapper.unwrap(attachment.document); @@ -327,10 +346,18 @@ Zotero.Translate.ItemSaver.prototype = { if(attachment.snapshot === false || !this._saveFiles) { // if snapshot is explicitly set to false, attach as link + attachment.linkMode = "linked_url"; if(attachment.document) { - Zotero.Attachments.linkFromURL(attachment.document.location.href, parentID, - (attachment.mimeType ? attachment.mimeType : attachment.document.contentType), - (attachment.title ? attachment.title : attachment.document.title)); + try { + Zotero.Attachments.linkFromURL(attachment.document.location.href, parentID, + (attachment.mimeType ? attachment.mimeType : attachment.document.contentType), + (attachment.title ? attachment.title : attachment.document.title)); + attachmentCallback(attachment, 100); + } catch(e) { + Zotero.debug("Translate: Error adding attachment "+attachment.url, 2); + attachmentCallback(attachment, false, e); + } + return true; } else { if(!attachment.mimeType || !attachment.title) { Zotero.debug("Translate: Either mimeType or title is missing; attaching file will be slower", 3); @@ -340,19 +367,33 @@ Zotero.Translate.ItemSaver.prototype = { Zotero.Attachments.linkFromURL(attachment.url, parentID, (attachment.mimeType ? attachment.mimeType : undefined), (attachment.title ? attachment.title : undefined)); + attachmentCallback(attachment, 100); } catch(e) { Zotero.debug("Translate: Error adding attachment "+attachment.url, 2); + attachmentCallback(attachment, false, e); } + return true; } } else { // if snapshot is not explicitly set to false, retrieve snapshot if(attachment.document) { if(automaticSnapshots) { try { - Zotero.Attachments.importFromDocument(attachment.document, parentID, attachment.title); + attachment.linkMode = "imported_url"; + Zotero.Attachments.importFromDocument(attachment.document, + parentID, attachment.title, function(status, err) { + if(status) { + attachmentCallback(attachment, 100); + } else { + attachmentCallback(attachment, false, err); + } + }, this._libraryID); + attachmentCallback(attachment, 0); } catch(e) { Zotero.debug("Translate: Error attaching document", 2); + attachmentCallback(attachment, false, e); } + return true; } // Save attachment if snapshot pref enabled or not HTML // (in which case downloadAssociatedFiles applies) @@ -364,14 +405,27 @@ Zotero.Translate.ItemSaver.prototype = { var fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentID); try { Zotero.debug('Importing attachment from URL'); + attachment.linkMode = "imported_url"; Zotero.Attachments.importFromURL(attachment.url, parentID, title, - fileBaseName, null, mimeType, this._libraryID, null, this._cookieSandbox); + fileBaseName, null, mimeType, this._libraryID, function(status, err) { + // TODO: actually indicate progress during download + if(status) { + attachmentCallback(attachment, 100); + } else { + attachmentCallback(attachment, false, err); + } + }, this._cookieSandbox); + attachmentCallback(attachment, 0); } catch(e) { Zotero.debug("Translate: Error adding attachment "+attachment.url, 2); + attachmentCallback(attachment, false, e); } + return true; } } } + + return false; }, "_saveFields":function(item, newItem) { diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -1575,5 +1575,16 @@ Zotero.Utilities = { } } return length; + }, + + /** + * Gets the icon for a JSON-style attachment + */ + "determineAttachmentIcon":function(attachment) { + if(attachment.linkMode === "linked_url") { + return Zotero.ItemTypes.getImageSrc("attachment-web-link"); + } + return Zotero.ItemTypes.getImageSrc(attachment.mimeType === "application/pdf" + ? "attachment-pdf" : "attachment-snapshot"); } } diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -412,6 +412,7 @@ save.error.cannotAddFilesToCollection = You cannot add files to the currently se ingester.saveToZotero = Save to Zotero ingester.saveToZoteroUsing = Save to Zotero using "%S" ingester.scraping = Saving Item… +ingester.scrapingTo = Saving to ingester.scrapeComplete = Item Saved ingester.scrapeError = Could Not Save Item ingester.scrapeErrorDescription = An error occurred while saving this item. Check %S for more information. diff --git a/chrome/skin/default/zotero/zotero.css b/chrome/skin/default/zotero/zotero.css @@ -228,12 +228,18 @@ label.zotero-text-link { margin: 0; min-height: 50px; width: 250px; - padding-bottom: 3px; + padding: 3px 0 3px 0; } #zotero-progress-text-headline { font-weight: bold; + margin-bottom: 2px; +} + +.zotero-progress-icon-headline { + width: 16px; + height: 16px; } .zotero-progress-item-icon @@ -245,8 +251,13 @@ label.zotero-text-link { .zotero-progress-item-hbox { padding-left: 5px; + margin-top: 0; + margin-bottom: 0; +} + +.zotero-progress-item-hbox[parent] +{ margin-top: 3px; - margin-bottom: 3px; } .zotero-progress-item-label