Various feeds changes

This commit is contained in:
Aurimas Vinckevicius 2015-02-03 11:57:32 -06:00 committed by Dan Stillman
parent e7f568d56c
commit 2d46e3d59b
12 changed files with 189 additions and 160 deletions

View file

@ -149,14 +149,14 @@ Zotero.CollectionTreeRow.prototype.isWithinEditableGroup = function () {
}
Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () {
if (this.isTrash() || this.isShare() || this.isBucket() || this.isFeed()) {
if (this.isTrash() || this.isShare() || this.isBucket()) {
return false;
}
if (!this.isWithinGroup() || this.isPublications()) {
return true;
}
var libraryID = this.ref.libraryID;
if (this.isGroup()) {
if (this.isGroup() || this.isFeed()) {
return this.ref.editable;
}
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) {

View file

@ -48,6 +48,7 @@ Zotero.CollectionTreeView = function()
'collection',
'search',
'publications',
'feed',
'share',
'group',
'feedItem',
@ -315,7 +316,7 @@ Zotero.CollectionTreeView.prototype.selectWait = Zotero.Promise.method(function
* Called by Zotero.Notifier on any changes to collections in the data layer
*/
Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) {
if (type == 'feed' && action == 'unreadCountUpdated') {
if (type == 'feed' && (action == 'unreadCountUpdated' || action == 'statusChanged')) {
for (let i=0; i<ids.length; i++) {
this._treebox.invalidateRow(this._rowMap['L' + ids[i]]);
}
@ -492,7 +493,7 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
case 'feed':
case 'group':
yield this.reload();
yield this.selectByID(currentTreeRow.id);
yield this.selectByID("L" + id);
break;
}
}
@ -719,6 +720,11 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
switch (collectionType) {
case 'library':
case 'feed':
if (treeRow.ref.updating) {
collectionType += '-updating';
} else if (treeRow.ref.lastCheckError) {
collectionType += '-error';
}
break;
case 'trash':
@ -731,6 +737,9 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
if (treeRow.ref.id == 'group-libraries-header') {
collectionType = 'groups';
}
else if (treeRow.ref.id == 'feed-libraries-header') {
collectionType = 'feedLibrary';
}
else if (treeRow.ref.id == 'commons-header') {
collectionType = 'commons';
}

View file

@ -29,13 +29,14 @@ Zotero.Feed = function(params = {}) {
this._feedCleanupAfter = null;
this._feedRefreshInterval = null;
// Feeds are not editable/filesEditable by the user. Remove the setter
this.editable = false;
// Feeds are editable by the user. Remove the setter
this.editable = true;
Zotero.defineProperty(this, 'editable', {
get: function() this._get('_libraryEditable')
});
// Feeds are not filesEditable by the user. Remove the setter
this.filesEditable = false;
Zotero.defineProperty(this, 'filesEditable', {
get: function() this._get('_libraryFilesEditable')
@ -53,22 +54,31 @@ Zotero.Feed = function(params = {}) {
return obj[prop];
}
});
this._feedUnreadCount = null;
this._updating = false;
}
Zotero.Feed._colToProp = function(c) {
return "_feed" + Zotero.Utilities.capitalize(c);
}
Zotero.defineProperty(Zotero.Feed, '_unreadCountSQL', {
value: "(SELECT COUNT(*) FROM items I JOIN feedItems FeI USING (itemID)"
+ " WHERE I.libraryID=F.libraryID AND FeI.readTime IS NULL) AS _feedUnreadCount"
});
Zotero.defineProperty(Zotero.Feed, '_dbColumns', {
value: Object.freeze(['name', 'url', 'lastUpdate', 'lastCheck',
'lastCheckError', 'cleanupAfter', 'refreshInterval'])
});
Zotero.Feed._colToProp = function(c) {
return "_feed" + Zotero.Utilities.capitalize(c);
}
Zotero.defineProperty(Zotero.Feed, '_primaryDataSQLParts');
Zotero.defineProperty(Zotero.Feed, '_rowSQLSelect', {
value: Zotero.Library._rowSQLSelect + ", "
+ Zotero.Feed._dbColumns.map(c => "F." + c + " AS " + Zotero.Feed._colToProp(c)).join(", ")
+ ", (SELECT COUNT(*) FROM items I JOIN feedItems FeI USING (itemID)"
+ " WHERE I.libraryID=F.libraryID AND FeI.readTime IS NULL) AS feedUnreadCount"
+ ", " + Zotero.Feed._unreadCountSQL
});
Zotero.defineProperty(Zotero.Feed, '_rowSQL', {
@ -89,6 +99,17 @@ Zotero.defineProperty(Zotero.Feed.prototype, 'isFeed', {
Zotero.defineProperty(Zotero.Feed.prototype, 'libraryTypes', {
value: Object.freeze(Zotero.Feed._super.prototype.libraryTypes.concat(['feed']))
});
Zotero.defineProperty(Zotero.Feed.prototype, 'unreadCount', {
get: function() this._feedUnreadCount
});
Zotero.defineProperty(Zotero.Feed.prototype, 'updating', {
get: function() this._updating,
set: function(v) {
if (!v == !this._updating) return; // Unchanged
this._updating = !!v;
Zotero.Notifier.trigger('statusChanged', 'feed', this.id);
}
});
(function() {
// Create accessors
@ -185,7 +206,7 @@ Zotero.Feed.prototype._loadDataFromRow = function(row) {
this._feedLastUpdate = row._feedLastUpdate || null;
this._feedCleanupAfter = parseInt(row._feedCleanupAfter) || null;
this._feedRefreshInterval = parseInt(row._feedRefreshInterval) || null;
this._feedUnreadCount = parseInt(row.feedUnreadCount);
this._feedUnreadCount = parseInt(row._feedUnreadCount);
}
Zotero.Feed.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
@ -218,7 +239,7 @@ Zotero.Feed.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
Zotero.Feed.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.Feed._super.prototype._saveData.apply(this, arguments);
Zotero.debug("Saving feed data for collection " + this.id);
Zotero.debug("Saving feed data for library " + this.id);
let changedCols = [], params = [];
for (let i=0; i<Zotero.Feed._dbColumns.length; i++) {
@ -268,21 +289,17 @@ Zotero.Feed.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
}
});
Zotero.Feed.prototype._finalizeErase = Zotero.Promise.method(function(env) {
Zotero.Feeds.unregister(this.libraryID);
return Zotero.Feed._super.prototype._finalizeErase.apply(this, arguments);
});
Zotero.Feed.prototype.getExpiredFeedItemIDs = Zotero.Promise.coroutine(function* () {
let sql = "SELECT itemID AS id FROM feedItems "
+ "LEFT JOIN libraryItems LI USING (itemID)"
+ "WHERE LI.libraryID=?"
+ "WHERE readTime IS NOT NULL "
+ "AND (julianday(readTime, 'utc') + (?) - julianday('now', 'utc')) > 0";
let expiredIDs = yield Zotero.DB.queryAsync(sql, [{int: this.cleanupAfter}]);
let expiredIDs = yield Zotero.DB.queryAsync(sql, [this.id, {int: this.cleanupAfter}]);
return expiredIDs.map(row => row.id);
});
Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
let errorMessage = '';
Zotero.Feed.prototype.clearExpiredItems = Zotero.Promise.coroutine(function* () {
try {
// Clear expired items
if (this.cleanupAfter) {
@ -299,10 +316,16 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
Zotero.debug("Error clearing expired feed items.");
Zotero.debug(e);
}
});
Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
this.updating = true;
this.lastCheckError = null;
yield this.clearExpiredItems();
try {
let fr = new Zotero.FeedReader(this.url);
let itemIterator = fr.itemIterator;
let itemIterator = new fr.ItemIterator();
let item, toAdd = [], processedGUIDs = [];
while (item = yield itemIterator.next().value) {
if (item.dateModified && this.lastUpdate
@ -331,6 +354,7 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
feedItem.libraryID = this.id;
} else {
Zotero.debug("Feed item " + item.guid + " already in library.");
if (item.dateModified && feedItem.dateModified
&& feedItem.dateModified == item.dateModified
) {
@ -353,19 +377,23 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
// Save in reverse order
let savePromises = new Array(toAdd.length);
for (let i=toAdd.length-1; i>=0; i--) {
yield toAdd[i].save({skipEditCheck: true, setDateModified: true});
// Saving currently has to happen sequentially so as not to violate the
// unique constraints in dataValues (FIXME)
yield toAdd[i].save({skipEditCheck: true});
}
this.lastUpdate = Zotero.Date.dateToSQL(new Date(), true);
} catch(e) {
Zotero.debug("Error processing feed from " + this.url);
Zotero.debug(e);
errorMessage = e.message || 'Error processing feed';
}
catch (e) {
if (e.message) {
Zotero.debug("Error processing feed from " + this.url);
Zotero.debug(e);
}
this.lastCheckError = e.message || 'Error processing feed';
}
this.lastCheck = Zotero.Date.dateToSQL(new Date(), true);
this.lastCheckError = errorMessage || null;
yield this.saveTx({skipEditCheck: true});
this.updating = false;
});
Zotero.Feed.prototype.updateFeed = function() {
@ -375,17 +403,27 @@ Zotero.Feed.prototype.updateFeed = function() {
});
}
Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* () {
yield this.loadChildItems();
let childItemIDs = this.getChildItems(true, true);
Zotero.Feed.prototype._finalizeErase = Zotero.Promise.coroutine(function* (){
let notifierData = {};
notifierData[this.libraryID] = {
libraryID: this.libraryID
};
Zotero.Notifier.trigger('delete', 'feed', this.id, notifierData);
Zotero.Feeds.unregister(this.libraryID);
return Zotero.Feed._super.prototype._finalizeErase.call(this);
});
Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* (deleteItems) {
let childItemIDs = yield Zotero.FeedItems.getAll(this.id, false, false, true);
yield Zotero.FeedItems.erase(childItemIDs);
return Zotero.Feed._super.prototype.erase.call(this); // Don't tell it to delete child items. They're already gone
})
yield Zotero.Feed._super.prototype.erase.call(this);
});
Zotero.Feed.prototype.updateUnreadCount = Zotero.Promise.coroutine(function* () {
let sql = "SELECT " + this._ObjectsClass._primaryDataSQLParts.feedUnreadCount
+ this._ObjectsClass.primaryDataSQLFrom
+ " AND O.libraryID=?";
let sql = "SELECT " + Zotero.Feed._unreadCountSQL
+ " FROM feeds F JOIN libraries L USING (libraryID)"
+ " WHERE L.libraryID=?";
let newCount = yield Zotero.DB.valueQueryAsync(sql, [this.id]);
if (newCount != this._feedUnreadCount) {

View file

@ -132,38 +132,6 @@ Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync(sql, [env.id, this.guid, this._feedItemReadTime]);
this._clearChanged('feedItemData');
/* let itemID;
if (env.isNew) {
// For new items, run this first so we get an item ID
yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
itemID = env.id;
} else {
itemID = this.id;
}
}
if (!env.isNew) {
if (this.hasChanged()) {
yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
} else {
env.skipPrimaryDataReload = true;
}
Zotero.Notifier.trigger('modify', 'feedItem', itemID);
} else {
Zotero.Notifier.trigger('add', 'feedItem', itemID);
}
if (env.collectionsAdded || env.collectionsRemoved) {
let affectedCollections = (env.collectionsAdded || [])
.concat(env.collectionsRemoved || []);
if (affectedCollections.length) {
let feeds = yield Zotero.Feeds.getAsync(affectedCollections);
for (let i=0; i<feeds.length; i++) {
feeds[i].updateUnreadCount();
}
}*/
}
});
@ -174,7 +142,6 @@ Zotero.FeedItem.prototype.toggleRead = Zotero.Promise.coroutine(function* (state
if (changed) {
yield this.save({skipEditCheck: true, skipDateModifiedUpdate: true});
yield this.loadCollections();
let feed = Zotero.Feeds.get(this.libraryID);
feed.updateUnreadCount();
}

View file

@ -94,14 +94,25 @@ Zotero.FeedItems = new Proxy(function() {
return this.getAsync(id);
});
this.toggleReadById = Zotero.Promise.coroutine(function* (ids, state) {
this.toggleReadByID = Zotero.Promise.coroutine(function* (ids, state) {
if (!Array.isArray(ids)) {
if (typeof ids != 'string') throw new Error('ids must be a string or array in Zotero.FeedItems.toggleReadById');
if (typeof ids != 'string') throw new Error('ids must be a string or array in Zotero.FeedItems.toggleReadByID');
ids = [ids];
}
let items = yield this.getAsync(ids);
if (state == undefined) {
// If state undefined, toggle read if at least one unread
state = true;
for (let item of items) {
if (item.isRead) {
state = false;
break;
}
}
}
for (let i=0; i<items.length; i++) {
items[i].toggleRead(state);
}

View file

@ -105,7 +105,7 @@ Zotero.Feeds = new function() {
.map(id => Zotero.Libraries.get(id));
}
this.get = Zotero.Libraries.get;
this.get = Zotero.Libraries.get.bind(Zotero.Libraries);
this.haveFeeds = function() {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
@ -154,8 +154,8 @@ Zotero.Feeds = new function() {
let sql = "SELECT libraryID AS id FROM feeds "
+ "WHERE refreshInterval IS NOT NULL "
+ "AND ( lastCheck IS NULL "
+ "OR (julianday(lastCheck, 'utc') + (refreshInterval/1440) - julianday('now', 'utc')) <= 0 )";
let needUpdate = yield Zotero.DB.queryAsync(sql).map(row => row.id);
+ "OR (julianday(lastCheck, 'utc') + (refreshInterval/1440.0) - julianday('now', 'utc')) <= 0 )";
let needUpdate = (yield Zotero.DB.queryAsync(sql)).map(row => row.id);
Zotero.debug("Running update for feeds: " + needUpdate.join(', '));
let feeds = Zotero.Libraries.get(needUpdate);
let updatePromises = [];

View file

@ -133,9 +133,10 @@ Zotero.Items = function() {
* @param {Integer} libraryID
* @param {Boolean} [onlyTopLevel=false] If true, don't include child items
* @param {Boolean} [includeDeleted=false] If true, include deleted items
* @return {Promise<Array<Zotero.Item>>}
* @param {Boolean} [onlyIDs=false] If true, resolves only with IDs
* @return {Promise<Array<Zotero.Item|Integer>>}
*/
this.getAll = Zotero.Promise.coroutine(function* (libraryID, onlyTopLevel, includeDeleted) {
this.getAll = Zotero.Promise.coroutine(function* (libraryID, onlyTopLevel, includeDeleted, onlyIDs=false) {
var sql = 'SELECT A.itemID FROM items A';
if (onlyTopLevel) {
sql += ' LEFT JOIN itemNotes B USING (itemID) '
@ -150,6 +151,9 @@ Zotero.Items = function() {
}
sql += " AND libraryID=?";
var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID);
if (onlyIDs) {
return ids;
}
return this.getAsync(ids);
});

View file

@ -45,7 +45,7 @@
*
* @property {Zotero.Promise<Object>} feedProperties An object
* representing feed properties
* @property {Zotero.Promise<FeedItem>*} itemIterator Returns an iterator
* @property {Zotero.Promise<FeedItem>*} ItemIterator Returns an iterator
* for feed items. The iterator returns FeedItem promises that have to be
* resolved before requesting the next promise. When all items are exhausted.
* the promise resolves to null.
@ -170,17 +170,10 @@ Zotero.FeedReader = new function() {
}
/*
* Format JS date as SQL date + time zone offset
* Format JS date as SQL date
*/
function formatDate(date) {
let offset = (date.getTimezoneOffset() / 60) * -1;
let absOffset = Math.abs(offset);
offset = offset
? ' ' + (offset < 0 ? '-' : '+')
+ Zotero.Utilities.lpad(Math.floor(absOffset), '0', 2)
+ ('' + ( (absOffset - Math.floor(absOffset)) || '' )).substr(1) // Get ".5" fraction or "" otherwise
: '';
return Zotero.Date.dateToSQL(date, false) + offset;
return Zotero.Date.dateToSQL(date, true);
}
/*
@ -268,7 +261,6 @@ Zotero.FeedReader = new function() {
if (!item.dateModified) {
// When there's no reliable modification date, we can assume that item doesn't get updated
Zotero.debug("FeedReader: Feed item missing a modification date (" + item.guid + ")");
item.dateModified = null;
}
if (!item.date && item.dateModified) {
@ -470,7 +462,9 @@ Zotero.FeedReader = new function() {
Zotero.debug("FeedReader: Fetching feed from " + feedUrl.spec);
this._channel = ios.newChannelFromURI(feedUrl);
this._channel = ios.newChannelFromURI2(feedUrl, null,
Services.scriptSecurityManager.getSystemPrincipal(), null,
Ci.nsILoadInfo.SEC_NORMAL, Ci.nsIContentPolicy.TYPE_OTHER);
this._channel.asyncOpen(feedProcessor, null); // Sends an HTTP request
}
@ -486,21 +480,25 @@ Zotero.FeedReader = new function() {
* is terminated ahead of time, in which case it will be rejected with the reason
* for termination.
*/
Zotero.defineProperty(FeedReader.prototype, 'itemIterator', {
Zotero.defineProperty(FeedReader.prototype, 'ItemIterator', {
get: function() {
let items = this._feedItems;
return new function() {
let i = 0;
this.next = function() {
let item = items[i++];
return {
value: item ? item.promise : null,
done: i >= items.length
};
let iterator = function() {
this.index = 0;
};
iterator.prototype.next = function() {
let item = items[this.index++];
return {
value: item ? item.promise : null,
done: this.index >= items.length
};
}
};
return iterator;
}
});
}, {lazy: true});
/*
* Terminate feed processing at any given time
@ -521,8 +519,8 @@ Zotero.FeedReader = new function() {
}
// Close feed connection
if (channel.isPending) {
channel.cancel(Components.results.NS_BINDING_ABORTED);
if (this._channel.isPending) {
this._channel.cancel(Components.results.NS_BINDING_ABORTED);
}
};

View file

@ -133,6 +133,14 @@ Zotero.URI = new function () {
}
this.getFeedItemURI = function(feedItem) {
return this.getItemURI(feedItem);
}
this.getFeedItemPath = function(feedItem) {
return this.getItemPath(feedItem);
}
/**
* Return URI of collection, which might be a local URI if user hasn't synced
*/
@ -148,6 +156,14 @@ Zotero.URI = new function () {
return this._getObjectPath(collection);
}
this.getFeedURI = function(feed) {
return this.getLibraryURI(feed);
}
this.getFeedPath = function(feed) {
return this.getLibraryPath(feed);
}
this.getGroupsURL = function () {
return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
@ -172,7 +188,7 @@ Zotero.URI = new function () {
return path;
}
if (obj instanceof Zotero.Item) {
if (obj instanceof Zotero.Item || obj instanceof Zotero.Feed) {
return path + '/items/' + obj.key;
}
@ -208,6 +224,9 @@ Zotero.URI = new function () {
return this._getURIObject(itemURI, 'item');
}
this.getURIFeedItem = function (feedItemURI) {
return this._getURIObject(feedItemURI, 'feedItem');
}
/**
* @param {String} itemURI
@ -264,6 +283,11 @@ Zotero.URI = new function () {
let library = this._getURIObjectLibrary(libraryURI);
return library ? library.id : false;
}
this.getURIFeed = function (feedURI) {
return this._getURIObjectLibrary(feedURI, 'feed');
}
/**

View file

@ -522,22 +522,7 @@ var ZoteroPane = new function()
}
let itemIDs = this.getSelectedItems(true);
Zotero.FeedItems.getAsync(itemIDs)
.then(function(feedItems) {
// Determine what most items are set to;
let allUnread = true;
for (let item of feedItems) {
if (item.isRead) {
allUnread = false;
break;
}
}
// If something is unread, toggle all read by default
for (let i=0; i<feedItems.length; i++) {
feedItems[i].toggleRead(!allUnread);
}
});
Zotero.FeedItems.toggleReadByID(itemIDs);
}
}
}
@ -866,7 +851,7 @@ var ZoteroPane = new function()
return collection.saveTx();
});
this.newFeed = Zotero.Promise.coroutine(function() {
this.newFeed = Zotero.Promise.coroutine(function* () {
let data = {};
window.openDialog('chrome://zotero/content/feedSettings.xul',
null, 'centerscreen, modal', data);
@ -877,7 +862,7 @@ var ZoteroPane = new function()
feed.refreshInterval = data.ttl;
feed.cleanupAfter = data.cleanAfter;
yield feed.save({skipEditCheck: true});
Zotero.Feeds.scheduleNextFeedCheck();
yield feed.updateFeed();
}
});
@ -1774,48 +1759,40 @@ var ZoteroPane = new function()
buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;
if (this.collectionsView.selection.count == 1) {
if (collectionTreeRow.isCollection())
{
var title, message;
// Work out the required title and message
if (collectionTreeRow.isCollection()) {
if (deleteItems) {
var index = ps.confirmEx(
null,
Zotero.getString('pane.collections.deleteWithItems.title'),
Zotero.getString('pane.collections.deleteWithItems'),
buttonFlags,
Zotero.getString('pane.collections.deleteWithItems.title'),
"", "", "", {}
);
title = Zotero.getString('pane.collections.deleteWithItems.title');
message = Zotero.getString('pane.collections.deleteWithItems');
}
else {
var index = ps.confirmEx(
null,
Zotero.getString('pane.collections.delete.title'),
Zotero.getString('pane.collections.delete')
title = Zotero.getString('pane.collections.delete.title');
message = Zotero.getString('pane.collections.delete')
+ "\n\n"
+ Zotero.getString('pane.collections.delete.keepItems'),
buttonFlags,
Zotero.getString('pane.collections.delete.title'),
"", "", "", {}
);
}
if (index == 0) {
this.collectionsView.deleteSelection(deleteItems);
+ Zotero.getString('pane.collections.delete.keepItems');
}
}
else if (collectionTreeRow.isSearch())
{
var index = ps.confirmEx(
null,
Zotero.getString('pane.collections.deleteSearch.title'),
Zotero.getString('pane.collections.deleteSearch'),
buttonFlags,
Zotero.getString('pane.collections.deleteSearch.title'),
"", "", "", {}
);
if (index == 0) {
this.collectionsView.deleteSelection();
}
else if (collectionTreeRow.isFeed()) {
title = Zotero.getString('pane.feed.deleteWithItems.title');
message = Zotero.getString('pane.feed.deleteWithItems');
}
else if (collectionTreeRow.isSearch()) {
title = Zotero.getString('pane.collections.deleteSearch.title');
message = Zotero.getString('pane.collections.deleteSearch');
}
// Display prompt
var index = ps.confirmEx(
null,
title,
message,
buttonFlags,
title,
"", "", "", {}
);
if (index == 0) {
this.collectionsView.deleteSelection(deleteItems);
}
}
}
@ -2230,6 +2207,7 @@ var ZoteroPane = new function()
"newCollection",
"newSavedSearch",
"newSubcollection",
"newFeed",
"refreshFeed",
"sep1",
"showDuplicates",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB