Merge pull request #902 from adomasven/feature/feed-reader-UI

Feed Reader UI

(I squashed a bunch of commits from the PR.)
This commit is contained in:
Dan Stillman 2016-03-22 07:11:50 -04:00
commit 6c43e75d26
63 changed files with 3349 additions and 501 deletions

View file

@ -341,7 +341,9 @@
fieldNames.push(Zotero.ItemFields.getName(fields[i]));
}
fieldNames.push("dateAdded", "dateModified");
if (!(this.item instanceof Zotero.FeedItem)) {
fieldNames.push("dateAdded", "dateModified");
}
}
for (var i=0; i<fieldNames.length; i++) {

View file

@ -596,14 +596,7 @@ var Zotero_Browser = new function() {
*/
this.performTranslation = Zotero.Promise.coroutine(function* (translate, libraryID, collection) {
if (Zotero.locked) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
var desc = Zotero.localeJoin([
Zotero.getString('general.operationInProgress'),
Zotero.getString('general.operationInProgress.waitUntilFinishedAndTryAgain')
]);
Zotero_Browser.progress.addDescription(desc);
Zotero_Browser.progress.show();
Zotero_Browser.progress.startCloseTimer(8000);
Zotero_Browser.progress.Translation.operationInProgress();
return;
}
@ -616,11 +609,7 @@ var Zotero_Browser = new function() {
if(libraryID === undefined && ZoteroPane && !Zotero.isConnector) {
try {
if (!ZoteroPane.collectionsView.editable) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
var desc = Zotero.getString('save.error.cannotMakeChangesToCollection');
Zotero_Browser.progress.addDescription(desc);
Zotero_Browser.progress.show();
Zotero_Browser.progress.startCloseTimer(8000);
Zotero_Browser.progress.cannotEditCollection();
return;
}
@ -632,87 +621,37 @@ var Zotero_Browser = new function() {
}
if (libraryID === Zotero.Libraries.publicationsLibraryID) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
var desc = Zotero.getString('save.error.cannotAddToMyPublications');
Zotero_Browser.progress.addDescription(desc);
Zotero_Browser.progress.show();
Zotero_Browser.progress.startCloseTimer(8000);
Zotero_Browser.progress.Translation.cannotAddToPublications();
return;
}
if(Zotero.isConnector) {
Zotero.Connector.callMethod("getSelectedCollection", {}, function(response, status) {
if(status !== 200) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scraping"));
} else {
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");
if (Zotero.Feeds.get(libraryID)) {
Zotero_Browser.progress.Translation.cannotAddToFeed();
return;
}
Zotero_Browser.progress.Translation.scrapingTo(libraryID, collection);
translate.clearHandlers("done");
translate.clearHandlers("itemDone");
translate.clearHandlers("attachmentProgress");
var deferred = Zotero.Promise.defer();
translate.setHandler("done", function(obj, returnValue) {
if(!returnValue) {
Zotero_Browser.progress.show();
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
// Include link to translator troubleshooting page
var url = "https://www.zotero.org/support/troubleshooting_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();
}
translate.setHandler("done", function() {
Zotero_Browser.progress.Translation.doneHandler.apply(Zotero_Browser.progress.Translation, arguments);
Zotero_Browser.isScraping = false;
deferred.resolve();
});
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));
}
translate.setHandler("itemDone", function() {
let handler = Zotero_Browser.progress.Translation.itemDoneHandler(_attachmentsMap);
handler.apply(Zotero_Browser.progress.Translation, arguments);
});
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));
}
}
translate.setHandler("attachmentProgress", function() {
let handler = Zotero_Browser.progress.Translation.attachmentProgressHandler(_attachmentsMap);
handler.apply(Zotero_Browser.progress.Translation, arguments);
});
translate.translate({

View file

@ -0,0 +1,169 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
//////////////////////////////////////////////////////////////////////////////
//
// Zotero_Feed_Settings
//
//////////////////////////////////////////////////////////////////////////////
var Zotero_Feed_Settings = new function() {
let urlIsValid = true,
data = null,
feedReader = null,
urlTainted = false;
let cleanURL = function(url) {
let cleanURL = Zotero.Utilities.cleanURL(url, true);
if (cleanURL) {
if (/^https?:\/\/[^\/\s]+\/\S/.test(cleanURL)) {
return cleanURL;
} else {
Zotero.debug(uri.scheme + " is not a supported protocol for feeds");
}
}
};
this.init = Zotero.Promise.coroutine(function* () {
this.toggleAdvancedOptions(false);
data = window.arguments[0];
if (data.url) {
document.getElementById('feed-url').value = data.url;
// Do not allow to change URL for existing feed
document.getElementById('feed-url').readOnly = true;
} else {
this.invalidateURL();
}
if (data.title) {
document.getElementById('feed-title').value = data.title;
}
let ttl;
if (data.ttl !== undefined) {
ttl = Math.floor(data.ttl / 60);
} else {
ttl = Zotero.Prefs.get('feeds.defaultTTL');
}
document.getElementById('feed-ttl').value = ttl;
let cleanupAfter = data.cleanupAfter;
if (cleanupAfter === undefined) cleanupAfter = Zotero.Prefs.get('feeds.defaultCleanupAfter');
document.getElementById('feed-cleanupAfter').value = cleanupAfter;
if (data.url && !data.urlIsValid) {
yield this.validateURL();
}
});
this.invalidateURL = function() {
urlTainted = true;
if (feedReader) {
feedReader.terminate();
feedReader = null;
}
if (!urlIsValid) return;
urlIsValid = false;
document.getElementById('feed-title').disabled = true;
document.getElementById('feed-ttl').disabled = true;
document.getElementById('feed-cleanupAfter').disabled = true;
document.documentElement.getButton('accept').disabled = true;
};
this.validateURL = Zotero.Promise.coroutine(function* () {
if (feedReader) {
feedReader.terminate();
feedReader = null;
}
let url = cleanURL(document.getElementById('feed-url').value);
urlTainted = false;
if (!url) return;
try {
var fr = feedReader = new Zotero.FeedReader(url);
yield fr.process();
let feed = fr.feedProperties;
// Prevent progress if textbox changes triggered another call to
// validateURL / invalidateURL (old session)
if (feedReader !== fr || urlTainted) return;
let title = document.getElementById('feed-title');
if (!data.url && feed.title) {
title.value = feed.title;
}
let ttl = document.getElementById('feed-ttl');
if (!data.url && feed.ttl) {
ttl.value = Math.floor(feed.ttl / 60) || 1;
}
document.getElementById('feed-url').value = url;
urlIsValid = true;
title.disabled = false;
ttl.disabled = false;
document.getElementById('feed-cleanupAfter').disabled = false;
document.documentElement.getButton('accept').disabled = false;
}
catch (e) {
Zotero.debug(e);
}
finally {
if (feedReader === fr) feedReader = null;
}
});
this.accept = function() {
data.url = document.getElementById('feed-url').value;
data.title = document.getElementById('feed-title').value;
data.ttl = document.getElementById('feed-ttl').value * 60;
data.cleanupAfter = document.getElementById('feed-cleanupAfter').value * 1;
return true;
};
this.cancel = function() {
data.cancelled = true;
return true;
};
/*
* Show/hide advanced options
* @param {Boolean} [show] If set, indicates whether the advanced
* options should be shown or not. If omitted, the options toggle
*/
this.toggleAdvancedOptions = function(show) {
var opts = document.getElementById("advanced-options-togglable");
opts.hidden = show !== undefined ? !show : !opts.hidden;
document.getElementById("advanced-options")
.setAttribute("state", opts.hidden ? "closed" : "open");
window.sizeToContent();
};
}

View file

@ -0,0 +1,62 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd" > %zoteroDTD;
]>
<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&zotero.feedSettings.title;" buttons="cancel,accept"
buttonlabelaccept="&zotero.feedSettings.saveButton.label;"
ondialogaccept="Zotero_Feed_Settings.accept()"
ondialogcancel="Zotero_Feed_Settings.cancel()"
id="zotero-feed-settings"
onload="Zotero_Feed_Settings.init()">
<script src="include.js"/>
<script src="feedSettings.js"/>
<grid>
<columns>
<column/>
<column flex="1"/>
</columns>
<rows>
<row>
<hbox align="center">
<label value="&zotero.feedSettings.url.label;" control="feed-url"/>
</hbox>
<textbox id="feed-url" flex="1" size="2"
oninput="Zotero_Feed_Settings.invalidateURL();Zotero_Feed_Settings.validateURL()"
focused="true" newlines="stripsurroundingwhitespace"
style="width: 30em; max-width: 30em"/>
</row>
<row>
<hbox align="center">
<label value="&zotero.feedSettings.title.label;" control="feed-url"/>
</hbox>
<textbox id="feed-title" flex="1" newlines="replacewithspaces"/>
</row>
</rows>
</grid>
<vbox id="advanced-options" class="zotero-advanced-options">
<hbox onclick="Zotero_Feed_Settings.toggleAdvancedOptions()" class="zotero-advanced-options-label">
<dropmarker/>
<hbox align="center">
<label value="&zotero.general.advancedOptions.label;"/>
</hbox>
</hbox>
<vbox id="advanced-options-togglable">
<hbox align="center">
<label value="&zotero.feedSettings.refresh.label1;" control="feed-ttl"/>
<textbox id="feed-ttl" type="number" min="0" increment="1" size="3"/>
<label value="&zotero.feedSettings.refresh.label2;" control="feed-ttl"/>
</hbox>
<hbox align="center">
<label value="&zotero.feedSettings.cleanupAfter.label1;" control="feed-cleanupAfter"/>
<textbox id="feed-cleanupAfter" type="number" min="0" increment="1" size="2"/>
<label value="&zotero.feedSettings.cleanupAfter.label2;" control="feed-cleanupAfter"/>
</hbox>
</vbox>
</vbox>
</dialog>

View file

@ -86,6 +86,10 @@ var ZoteroItemPane = new function() {
_lastItem = item;
// Hide for feed items
document.getElementById('zotero-editpane-tabs').setAttribute('hidden', item.isFeedItem);
document.getElementById('zotero-view-item').classList.add('no-tabs');
if (index == 1) {
var editable = ZoteroPane_Local.canEdit();
_notesButton.hidden = !editable;

View file

@ -52,41 +52,41 @@
<!-- Regular item -->
<tabbox id="zotero-view-tabbox" flex="1" onselect="if (!ZoteroPane_Local.collectionsView.selection || event.originalTarget.localName != 'tabpanels') { return; }; ZoteroItemPane.viewItem(ZoteroPane_Local.getSelectedItems()[0], ZoteroPane_Local.collectionsView.editable ? 'edit' : 'view', this.selectedIndex)">
<tabs>
<tab label="&zotero.tabs.info.label;"/>
<tab label="&zotero.tabs.notes.label;"/>
<tab label="&zotero.tabs.tags.label;"/>
<tab label="&zotero.tabs.related.label;"/>
<tabs id="zotero-editpane-tabs">
<tab id="zotero-editpane-info-tab" label="&zotero.tabs.info.label;"/>
<tab id="zotero-editpane-notes-tab" label="&zotero.tabs.notes.label;"/>
<tab id="zotero-editpane-tags-tab" label="&zotero.tabs.tags.label;"/>
<tab id="zotero-editpane-related-tab" label="&zotero.tabs.related.label;"/>
</tabs>
<tabpanels id="zotero-view-item" flex="1">
<tabpanel>
<zoteroitembox id="zotero-editpane-item-box" flex="1"/>
</tabpanel>
<tabpanel flex="1" orient="vertical">
<vbox flex="1" class="zotero-box">
<hbox align="center">
<label id="zotero-editpane-notes-label"/>
<button id="zotero-editpane-notes-add" label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote(event.shiftKey);"/>
</hbox>
<grid flex="1">
<columns>
<column flex="1"/>
<column/>
</columns>
<rows id="zotero-editpane-dynamic-notes" flex="1"/>
</grid>
</vbox>
</tabpanel>
<tabpanel>
<tagsbox id="zotero-editpane-tags" flex="1"/>
</tabpanel>
<tabpanel>
<relatedbox id="zotero-editpane-related" flex="1"/>
</tabpanel>
</tabpanels>
<tabpanels id="zotero-view-item" flex="1">
<tabpanel>
<zoteroitembox id="zotero-editpane-item-box" flex="1"/>
</tabpanel>
<tabpanel flex="1" orient="vertical">
<vbox flex="1" class="zotero-box">
<hbox align="center">
<label id="zotero-editpane-notes-label"/>
<button id="zotero-editpane-notes-add" label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote(event.shiftKey);"/>
</hbox>
<grid flex="1">
<columns>
<column flex="1"/>
<column/>
</columns>
<rows id="zotero-editpane-dynamic-notes" flex="1"/>
</grid>
</vbox>
</tabpanel>
<tabpanel>
<tagsbox id="zotero-editpane-tags" flex="1"/>
</tabpanel>
<tabpanel>
<relatedbox id="zotero-editpane-related" flex="1"/>
</tabpanel>
</tabpanels>
</tabbox>
<!-- Note item -->

View file

@ -815,7 +815,10 @@ Zotero_Preferences.Keys = {
var rows = document.getElementById('zotero-prefpane-advanced-keys-tab').getElementsByTagName('row');
for (var i=0; i<rows.length; i++) {
// Display the appropriate modifier keys for the platform
rows[i].firstChild.nextSibling.value = Zotero.isMac ? Zotero.getString('general.keys.cmdShift') : Zotero.getString('general.keys.ctrlShift');
let label = rows[i].firstChild.nextSibling;
if (label.className == 'modifier') {
label.value = Zotero.isMac ? Zotero.getString('general.keys.cmdShift') : Zotero.getString('general.keys.ctrlShift');
}
}
var textboxes = document.getElementById('zotero-keys-rows').getElementsByTagName('textbox');

View file

@ -23,7 +23,12 @@
***** END LICENSE BLOCK *****
-->
<!DOCTYPE prefwindow SYSTEM "chrome://zotero/locale/preferences.dtd">
<!DOCTYPE window [
<!ENTITY % zoteroDTD SYSTEM "chrome://zotero/locale/zotero.dtd">
%zoteroDTD;
<!ENTITY % preferencesDTD SYSTEM "chrome://zotero/locale/preferences.dtd">
%preferencesDTD;
]>
<overlay id="zotero-prefpane-advanced-overlay"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
@ -46,16 +51,22 @@
<preference id="pref-keys-toggleTagSelector" name="extensions.zotero.keys.toggleTagSelector" type="string"/>
<preference id="pref-keys-newItem" name="extensions.zotero.keys.newItem" type="string"/>
<preference id="pref-keys-newNote" name="extensions.zotero.keys.newNote" type="string"/>
<preference id="pref-keys-toggleRead" name="extensions.zotero.keys.toggleRead" type="string"/>
<preference id="pref-keys-importFromClipboard" name="extensions.zotero.keys.importFromClipboard" type="string"/>
<preference id="pref-keys-copySelectedItemCitationsToClipboard" name="extensions.zotero.keys.copySelectedItemCitationsToClipboard" type="string"/>
<preference id="pref-keys-copySelectedItemsToClipboard" name="extensions.zotero.keys.copySelectedItemsToClipboard" type="string"/>
<preference id="pref-feeds-sortAscending" name="extensions.zotero.feeds.sortAscending" type="bool"/>
<preference id="pref-feeds-defaultTTL" name="extensions.zotero.feeds.defaultTTL" type="int"/>
<preference id="pref-feeds-defaultCleanupAfter" name="extensions.zotero.feeds.defaultCleanupAfter" type="int"/>
</preferences>
<tabbox id="zotero-prefpane-advanced-tabs">
<tabs>
<tab label="&zotero.preferences.prefpane.general;"/>
<tab label="&zotero.preferences.advanced.filesAndFolders;"/>
<tab label="&zotero.preferences.advanced.keys;"/>
<tab label="&zotero.preferences.advanced.keys;"/>
<tab label="&zotero.preferences.feeds;"/>
</tabs>
<tabpanels id="zotero-prefpane-advanced-tabpanels">
@ -217,37 +228,37 @@
<rows id="zotero-keys-rows">
<row id="zotero-keys-new-item">
<label value="&zotero.preferences.keys.newItem;" control="textbox-newItem"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-newItem" maxlength="1" size="1" preference="pref-keys-newItem"/>
</row>
<row>
<label value="&zotero.preferences.keys.newNote;" control="textbox-newNote"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-newNote" maxlength="1" size="1" preference="pref-keys-newNote"/>
</row>
<row>
<label value="&zotero.preferences.keys.importFromClipboard;" control="textbox-importFromClipboard"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-importFromClipboard" maxlength="1" size="1" preference="pref-keys-importFromClipboard"/>
</row>
<row id="zotero-keys-focus-libraries-pane">
<label value="&zotero.preferences.keys.focusLibrariesPane;" control="textbox-library"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-library" maxlength="1" size="1" preference="pref-keys-library"/>
</row>
<row>
<label value="&zotero.preferences.keys.quicksearch;" control="textbox-quicksearch"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-quicksearch" maxlength="1" size="1" preference="pref-keys-quicksearch"/>
</row>
<row>
<label value="&zotero.preferences.keys.copySelectedItemCitationsToClipboard;" control="textbox-copySelectedItemCitationsToClipboard"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-copySelectedItemCitationsToClipboard" maxlength="1" size="1"
preference="pref-keys-copySelectedItemCitationsToClipboard"
onchange="if (Zotero_Preferences.Export) { Zotero_Preferences.Export.updateQuickCopyInstructions(); }"/>
@ -255,7 +266,7 @@
<row>
<label value="&zotero.preferences.keys.copySelectedItemsToClipboard;" control="textbox-copySelectedItemsToClipboard"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-copySelectedItemsToClipboard" maxlength="1" size="1"
preference="pref-keys-copySelectedItemsToClipboard"
onchange="if (Zotero_Preferences.Export) { Zotero_Preferences.Export.updateQuickCopyInstructions(); }"/>
@ -263,9 +274,15 @@
<row>
<label value="&zotero.preferences.keys.toggleTagSelector;" control="textbox-toggleTagSelector"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-toggleTagSelector" maxlength="1" size="1" preference="pref-keys-toggleTagSelector"/>
</row>
<row>
<label value="&zotero.preferences.keys.toggleRead;" control="textbox-toggleRead"/>
<label/>
<textbox id="textbox-toggleRead" maxlength="1" size="1" preference="pref-keys-toggleRead"/>
</row>
</rows>
</grid>
@ -273,6 +290,40 @@
<separator/>
</tabpanel>
<tabpanel id="zotero-prefpane-advanced-feeds-tab" orient="vertical">
<groupbox>
<hbox>
<hbox align="center">
<label value="&zotero.preferences.feeds.sorting.label1;"/>
<menulist id="feed-sort" preference="pref-feeds-sortAscending">
<menupopup>
<menuitem label="&zotero.preferences.feeds.sorting.newest;" value="false"/>
<menuitem label="&zotero.preferences.feeds.sorting.oldest;" value="true"/>
</menupopup>
</menulist>
<label value="&zotero.preferences.feeds.sorting.label2;"/>
</hbox>
</hbox>
</groupbox>
<groupbox>
<caption label="&zotero.preferences.feeds.feedDefaults;"/>
<hbox>
<hbox align="center">
<label value="&zotero.feedSettings.refresh.label1;"/>
<textbox maxlength="2" size="2" preference="pref-feeds-defaultTTL"/>
<label value="&zotero.feedSettings.refresh.label2;"/>
</hbox>
</hbox>
<hbox>
<hbox align="center">
<label value="&zotero.feedSettings.cleanupAfter.label1;"/>
<textbox maxlength="3" size="3" preference="pref-feeds-defaultCleanupAfter"/>
<label value="&zotero.feedSettings.cleanupAfter.label2;"/>
</hbox>
</hbox>
</groupbox>
</tabpanel>
</tabpanels>
</tabbox>

View file

@ -51,19 +51,19 @@
<rows id="zotero-keys-rows">
<row insertbefore="zotero-keys-new-item">
<label value="&zotero.preferences.keys.openZotero;" control="key-textbox-openZotero"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-openZotero" maxlength="1" size="1" preference="pref-keys-openZotero"/>
</row>
<row insertbefore="zotero-keys-new-item">
<label value="&zotero.preferences.keys.toggleFullscreen;" control="textbox-toggleFullscreen"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-toggleFullscreen" maxlength="1" size="1" preference="pref-keys-toggleFullscreen"/>
</row>
<row insertbefore="zotero-keys-new-item">
<label value="&zotero.preferences.keys.saveToZotero;" control="key-textbox-saveToZotero"/>
<label/>
<label class="modifier"/>
<textbox id="textbox-saveToZotero" maxlength="1" size="1" preference="pref-keys-saveToZotero"/>
</row>
</rows>

View file

@ -43,6 +43,7 @@
<preference id="pref-groups-copyChildFileAttachments" name="extensions.zotero.groups.copyChildFileAttachments" type="bool"/>
<preference id="pref-groups-copyChildLinks" name="extensions.zotero.groups.copyChildLinks" type="bool"/>
<preference id="pref-groups-copyTags" name="extensions.zotero.groups.copyTags" type="bool"/>
</preferences>
<groupbox id="zotero-prefpane-general-groupbox">

View file

@ -683,32 +683,14 @@ Zotero.Attachments = new function(){
return attachmentItem;
});
/**
* @deprecated Use Zotero.Utilities.cleanURL instead
*/
this.cleanAttachmentURI = function (uri, tryHttp) {
uri = uri.trim();
if (!uri) return false;
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
try {
return ios.newURI(uri, null, null).spec // Valid URI if succeeds
} catch (e) {
if (e instanceof Components.Exception
&& e.result == Components.results.NS_ERROR_MALFORMED_URI
) {
if (tryHttp && /\w\.\w/.test(uri)) {
// Assume it's a URL missing "http://" part
try {
return ios.newURI('http://' + uri, null, null).spec;
} catch (e) {}
}
Zotero.debug('cleanAttachmentURI: Invalid URI: ' + uri, 2);
return false;
}
throw e;
}
Zotero.debug("Zotero.Attachments.cleanAttachmentURI() is deprecated -- use Zotero.Utilities.cleanURL");
return Zotero.Utilities.cleanURL(uri, tryHttp);
}

View file

@ -39,6 +39,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
case 'library':
case 'publications':
case 'group':
case 'feed':
return 'L' + this.ref.libraryID;
case 'collection':
@ -57,8 +58,11 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
return 'T' + this.ref.libraryID;
case 'header':
if (this.ref.id == 'group-libraries-header') {
return 'HG';
switch (this.ref.id) {
case 'group-libraries-header':
return "HG";
case 'feed-libraries-header':
return "HF";
}
break;
}
@ -69,7 +73,8 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
Zotero.CollectionTreeRow.prototype.isLibrary = function (includeGlobal)
{
if (includeGlobal) {
return this.type == 'library' || this.type == 'publications' || this.type == 'group';
var global = ['library', 'publications', 'group', 'feed'];
return global.indexOf(this.type) != -1;
}
return this.type == 'library';
}
@ -109,6 +114,10 @@ Zotero.CollectionTreeRow.prototype.isGroup = function() {
return this.type == 'group';
}
Zotero.CollectionTreeRow.prototype.isFeed = function() {
return this.type == 'feed';
}
Zotero.CollectionTreeRow.prototype.isSeparator = function () {
return this.type == 'separator';
}
@ -143,13 +152,13 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () {
if (this.isTrash() || this.isShare() || this.isBucket()) {
return false;
}
if (this.isGroup() || this.isFeed()) {
return this.ref.editable;
}
if (!this.isWithinGroup() || this.isPublications()) {
return true;
}
var libraryID = this.ref.libraryID;
if (this.isGroup()) {
return this.ref.editable;
}
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) {
var type = Zotero.Libraries.get(libraryID).libraryType;
if (type == 'group') {
@ -163,7 +172,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () {
});
Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function () {
if (this.isTrash() || this.isShare()) {
if (this.isTrash() || this.isShare() || this.isFeed()) {
return false;
}
if (!this.isWithinGroup() || this.isPublications()) {

View file

@ -48,8 +48,10 @@ Zotero.CollectionTreeView = function()
'collection',
'search',
'publications',
'feed',
'share',
'group',
'feedItem',
'trash',
'bucket'
],
@ -206,7 +208,34 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function*
}),
added++
);
// TODO: Unify feed and group adding code
// Add feeds
var feeds = Zotero.Feeds.getAll();
if (feeds.length) {
this._addRowToArray(
newRows,
new Zotero.CollectionTreeRow('separator', false),
added++
);
this._addRowToArray(
newRows,
new Zotero.CollectionTreeRow('header', {
id: "feed-libraries-header",
label: Zotero.getString('pane.collections.feedLibraries'),
libraryID: -1
}, 0),
added++
);
for (let feed of feeds) {
this._addRowToArray(
newRows,
new Zotero.CollectionTreeRow('feed', feed),
added++
);
}
}
// Add groups
var groups = Zotero.Groups.getAll();
if (groups.length) {
@ -224,10 +253,10 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function*
}, 0),
added++
);
for (let i = 0, len = groups.length; i < len; i++) {
for (let group of groups) {
this._addRowToArray(
newRows,
new Zotero.CollectionTreeRow('group', groups[i]),
new Zotero.CollectionTreeRow('group', group),
added++
);
}
@ -287,6 +316,13 @@ 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' || action == 'statusChanged')) {
for (let i=0; i<ids.length; i++) {
this._treebox.invalidateRow(this._rowMap['L' + ids[i]]);
}
return;
}
if ((!ids || ids.length == 0) && action != 'refresh' && action != 'redraw') {
return;
}
@ -340,15 +376,16 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
rows.push(this._rowMap['S' + id]);
}
break;
case 'feed':
case 'group':
let row = this.getRowIndexByID("L" + extraData[id].libraryID);
let groupLevel = this.getLevel(row);
let level = this.getLevel(row);
do {
rows.push(row);
row++;
}
while (row < this.rowCount && this.getLevel(row) > groupLevel);
while (row < this.rowCount && this.getLevel(row) > level);
break;
}
}
@ -371,6 +408,20 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
};
this.selection.select(selectedIndex)
}
// Make sure the selection doesn't land on a separator (e.g. deleting last feed)
let index = this.selection.currentIndex;
while (index >= 0 && !this.isSelectable(index)) {
// move up, since we got shifted down
index--;
}
if (index >= 0) {
this.selection.select(index);
} else {
this.selection.clearSelection();
}
}
else if (action == 'modify') {
let row;
@ -443,6 +494,11 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
yield this.reload();
yield this.selectByID(currentTreeRow.id);
break;
case 'feed':
yield this.reload();
yield this.selectByID("L" + id);
break;
}
}
@ -667,6 +723,15 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
switch (collectionType) {
case 'library':
case 'feed':
// Better alternative needed: https://github.com/zotero/zotero/pull/902#issuecomment-183185973
/*
if (treeRow.ref.updating) {
collectionType += '-updating';
} else if (treeRow.ref.lastCheckError) {
collectionType += '-error';
}
*/
break;
case 'trash':
@ -679,6 +744,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';
}
@ -1039,11 +1107,14 @@ Zotero.CollectionTreeView.prototype.deleteSelection = Zotero.Promise.coroutine(f
{
//erase collection from DB:
var treeRow = this.getRow(rows[i]-i);
if (treeRow.isCollection()) {
if (treeRow.isCollection() || treeRow.isFeed()) {
yield treeRow.ref.eraseTx({
deleteItems: true
});
}
if (treeRow.isCollection() || treeRow.isFeed()) {
yield treeRow.ref.erase(deleteItems);
}
else if (treeRow.isSearch()) {
yield Zotero.Searches.erase(treeRow.ref.id);
}
@ -1073,7 +1144,7 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
var isCollection = treeRow.isCollection();
var libraryID = treeRow.ref.libraryID;
if (treeRow.isPublications()) {
if (treeRow.isPublications() || treeRow.isFeed()) {
return false;
}
@ -1268,8 +1339,8 @@ Zotero.CollectionTreeView.prototype._rememberOpenStates = Zotero.Promise.corouti
var open = this.isContainerOpen(i);
// Collections default to closed
if (!open && treeRow.isCollection()) {
// Collections and feeds default to closed
if ((!open && treeRow.isCollection()) || treeRow.isFeed()) {
delete state[treeRow.id];
continue;
}
@ -1368,6 +1439,11 @@ Zotero.CollectionTreeView.prototype.canDropCheck = function (row, orient, dataTr
return false;
}
if (treeRow.isFeed()) {
Zotero.debug("Cannot drop into feeds");
return false;
}
if (dataType == 'zotero/item') {
var ids = data;
var items = Zotero.Items.get(ids);
@ -1398,6 +1474,10 @@ Zotero.CollectionTreeView.prototype.canDropCheck = function (row, orient, dataTr
Zotero.debug("Top-level attachments and notes cannot be added to My Publications");
return false;
}
if(item instanceof Zotero.FeedItem) {
Zotero.debug("FeedItems cannot be added to My Publications");
return false;
}
skip = false;
continue;
}
@ -1773,8 +1853,8 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
// Collection drag between libraries
if (targetLibraryID != droppedCollection.libraryID) {
yield Zotero.DB.executeTransaction(function* () {
function copyCollections(descendents, parentID, addItems) {
for each(var desc in descendents) {
var copyCollections = Zotero.Promise.coroutine(function* (descendents, parentID, addItems) {
for (var desc of descendents) {
// Collections
if (desc.type == 'collection') {
var c = yield Zotero.Collections.getAsync(desc.id);
@ -1792,7 +1872,7 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
// Recursively copy subcollections
if (desc.children.length) {
copyCollections(desc.children, collectionID, addItems);
yield copyCollections(desc.children, collectionID, addItems);
}
}
// Items
@ -1824,7 +1904,7 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
}
}
}
}
});
var collections = [{
id: droppedCollection.id,
@ -1833,10 +1913,10 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
}];
var addItems = {};
copyCollections(collections, targetCollectionID, addItems);
yield copyCollections(collections, targetCollectionID, addItems);
for (var collectionID in addItems) {
var collection = yield Zotero.Collections.getAsync(collectionID);
collection.addItems(addItems[collectionID]);
yield collection.addItems(addItems[collectionID]);
}
// TODO: add subcollections and subitems, if they don't already exist,
@ -1860,8 +1940,25 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
return;
}
var items = yield Zotero.Items.getAsync(ids);
if (items.length == 0) {
return;
}
if (items[0] instanceof Zotero.FeedItem) {
if (!(targetTreeRow.isCollection() || targetTreeRow.isLibrary() || targetTreeRow.isGroup())) {
return;
}
let promises = [];
for (let item of items) {
// No transaction, because most time is spent traversing urls
promises.push(item.translate(targetLibraryID, targetCollectionID))
}
return Zotero.Promise.all(promises);
}
if (targetTreeRow.isPublications()) {
let items = yield Zotero.Items.getAsync(ids);
let io = this._treebox.treeBody.ownerDocument.defaultView
.ZoteroPane.showPublicationsWizard(items);
if (!io) {
@ -1877,11 +1974,6 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
}
yield Zotero.DB.executeTransaction(function* () {
var items = yield Zotero.Items.getAsync(ids);
if (!items) {
return;
}
var newItems = [];
var newIDs = [];
var toMove = [];
@ -2094,6 +2186,8 @@ Zotero.CollectionTreeView.prototype.getCellProperties = function(row, col, prop)
}
else if (treeRow.isPublications()) {
props.push("notwisty");
} else if (treeRow.ref && treeRow.ref.unreadCount) {
props.push('unread');
}
return props.join(" ");

View file

@ -757,11 +757,16 @@ Zotero.DataObject.prototype.isEditable = function () {
Zotero.DataObject.prototype.editCheck = function () {
let library = Zotero.Libraries.get(this.libraryID);
if ((this._objectType == 'collection' || this._objectType == 'search')
&& Zotero.Libraries.get(this.libraryID).libraryType == 'publications') {
&& library.libraryType == 'publications') {
throw new Error(this._ObjectTypePlural + " cannot be added to My Publications");
}
if (library.libraryType == 'feed') {
return;
}
if (!this.isEditable()) {
throw new Error("Cannot edit " + this._objectType + " in read-only library "
+ Zotero.Libraries.getName(this.libraryID));

View file

@ -68,6 +68,10 @@ Zotero.defineProperty(Zotero.DataObjects.prototype, 'table', {
get: function() this._ZDO_table
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'relationsTable', {
get: function() this._ZDO_object + 'Relations'
});
Zotero.defineProperty(Zotero.DataObjects.prototype, 'primaryFields', {
get: function () Object.keys(this._primaryDataSQLParts)
}, {lazy: true});

View file

@ -23,26 +23,40 @@
***** END LICENSE BLOCK *****
*/
/**
* Zotero.Feed, extends Zotero.Library
*
* Custom parameters:
* - name - name of the feed displayed in the collection tree
* - url
* - cleanupAfter - number of days after which read items should be removed
* - refreshInterval - in terms of hours
*
* @param params
* @returns Zotero.Feed
* @constructor
*/
Zotero.Feed = function(params = {}) {
params.libraryType = 'feed';
Zotero.Feed._super.call(this, params);
this._feedCleanupAfter = null;
this._feedRefreshInterval = null;
// Feeds are not editable/filesEditable by the user. Remove the setter
// Feeds are not editable by the user. Remove the setter
this.editable = false;
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')
});
Zotero.Utilities.assignProps(this, params, ['name', 'url', 'refreshInterval',
'cleanupAfter']);
Zotero.Utilities.assignProps(this, params,
['name', 'url', 'refreshInterval', 'cleanupAfter']);
// Return a proxy so that we can disable the object once it's deleted
return new Proxy(this, {
@ -53,22 +67,35 @@ Zotero.Feed = function(params = {}) {
return obj[prop];
}
});
this._feedUnreadCount = null;
this._updating = false;
this._syncedSettings = null;
this._previousURL = null;
}
Zotero.Feed._colToProp = function(c) {
return "_feed" + Zotero.Utilities.capitalize(c);
}
Zotero.extendClass(Zotero.Library, Zotero.Feed);
Zotero.defineProperty(Zotero.Feed, '_unreadCountSQL', {
value: "(SELECT COUNT(*) FROM items I JOIN feedItems FI USING (itemID)"
+ " WHERE I.libraryID=F.libraryID AND FI.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', {
@ -76,8 +103,6 @@ Zotero.defineProperty(Zotero.Feed, '_rowSQL', {
+ " FROM feeds F JOIN libraries L USING (libraryID)"
});
Zotero.extendClass(Zotero.Library, Zotero.Feed);
Zotero.defineProperty(Zotero.Feed.prototype, '_objectType', {
value: 'feed'
});
@ -89,6 +114,12 @@ 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,
});
(function() {
// Create accessors
@ -150,6 +181,7 @@ Zotero.Feed.prototype._set = function (prop, val) {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
throw new Error(invalidUrlError);
}
this._previousURL = this.url;
break;
case '_feedRefreshInterval':
case '_feedCleanupAfter':
@ -178,13 +210,14 @@ Zotero.Feed.prototype._set = function (prop, val) {
Zotero.Feed.prototype._loadDataFromRow = function(row) {
Zotero.Feed._super.prototype._loadDataFromRow.call(this, row);
this._feedName = row._feedName;
this._feedUrl = row._feedUrl;
this._feedLastCheckError = row._feedLastCheckError || null;
this._feedLastCheck = row._feedLastCheck || null;
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* () {
@ -194,7 +227,7 @@ Zotero.Feed.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () {
});
Zotero.defineProperty(Zotero.Feed.prototype, '_childObjectTypes', {
value: Object.freeze(['feedItem'])
value: Object.freeze(['feedItem', 'item'])
});
Zotero.Feed.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
@ -217,7 +250,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++) {
@ -254,20 +287,256 @@ Zotero.Feed.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
});
Zotero.Feed.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
let changedURL = this._changed._feedUrl;
yield Zotero.Feed._super.prototype._finalizeSave.apply(this, arguments);
if (env.isNew) {
Zotero.Feeds.register(this);
} else if (changedURL) {
if (!env.isNew && this._previousURL) {
// Re-register library if URL changed
Zotero.Feeds.unregister(this.libraryID);
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds') || {};
delete syncedFeeds[this._previousURL];
yield Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', syncedFeeds);
}
if (env.isNew || this._previousURL) {
Zotero.Feeds.register(this);
yield this.storeSyncedSettings();
}
this._previousURL = null;
});
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);
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds') || {};
delete syncedFeeds[this.url];
if (Object.keys(syncedFeeds).length == 0) {
yield Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, 'feeds');
} else {
yield Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', syncedFeeds);
}
return Zotero.Feed._super.prototype._finalizeErase.apply(this, arguments);
});
Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) {
let childItemIDs = yield Zotero.FeedItems.getAll(this.id, false, false, true);
yield Zotero.FeedItems.erase(childItemIDs);
yield Zotero.Feed._super.prototype.erase.call(this, options);
});
Zotero.Feed.prototype.getSyncedSettings = function () {
if (!this._syncedSettings) {
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds') || {};
this._syncedSettings = syncedFeeds[this.url];
}
if (!this._syncedSettings) {
this._syncedSettings = {
url: this.url,
name: this.name,
cleanupAfter: this.cleanupAfter,
refreshInterval: this.refreshInterval,
markedAsRead: {}
};
}
return this._syncedSettings;
};
Zotero.Feed.prototype.setSyncedSettings = Zotero.Promise.coroutine(function* (syncedSettings, store=false) {
this._syncedSettings = syncedSettings;
if (store) {
return this.storeSyncedSettings();
}
});
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.storeSyncedSettings = Zotero.Promise.coroutine(function* () {
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds') || {};
syncedFeeds[this.url] = this.getSyncedSettings();
return Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', syncedFeeds);
});
Zotero.Feed.prototype.getExpiredFeedItemIDs = Zotero.Promise.coroutine(function* () {
let sql = "SELECT itemID AS id FROM feedItems "
+ "LEFT JOIN items I USING (itemID) "
+ "WHERE I.libraryID=? "
+ "AND readTime IS NOT NULL "
+ "AND julianday('now', 'utc') - (julianday(readTime, 'utc') + ?) > 0";
return Zotero.DB.columnQueryAsync(sql, [this.id, {int: this.cleanupAfter}]);
});
/**
* Clearing conditions for an item:
* - Has been read at least feed.cleanupAfter earlier AND
* - Does not exist in the RSS feed anymore
*
* If we clear items once they've been read, we may potentially end up
* with empty feeds for those that do not update very frequently.
*/
Zotero.Feed.prototype.clearExpiredItems = Zotero.Promise.coroutine(function* (itemsInFeedIDs) {
itemsInFeedIDs = itemsInFeedIDs || new Set();
try {
// Clear expired items
if (this.cleanupAfter) {
let expiredItems = yield this.getExpiredFeedItemIDs();
let toClear = expiredItems;
if (itemsInFeedIDs.size) {
toClear = [];
for (let id of expiredItems) {
if (!itemsInFeedIDs.has(id)) {
toClear.push(id);
}
}
}
Zotero.debug("Clearing up read feed items...");
if (toClear.length) {
Zotero.debug(toClear.join(', '));
yield Zotero.FeedItems.erase(toClear);
} else {
Zotero.debug("No expired feed items");
}
}
} catch(e) {
Zotero.debug("Error clearing expired feed items");
Zotero.debug(e);
}
return this.storeSyncedSettings();
});
Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
var toSave = [], attachmentsToAdd = [], feedItemIDs = new Set();
if (this._updating) {
return this._updating;
}
let deferred = Zotero.Promise.defer();
this._updating = deferred.promise;
Zotero.Notifier.trigger('statusChanged', 'feed', this.id);
this._set('_feedLastCheckError', null);
try {
let fr = new Zotero.FeedReader(this.url);
yield fr.process();
let itemIterator = new fr.ItemIterator();
let item, processedGUIDs = new Set();
while (item = yield itemIterator.next().value) {
if (processedGUIDs.has(item.guid)) {
Zotero.debug("Feed item " + item.guid + " already processed from feed");
continue;
}
processedGUIDs.add(item.guid);
Zotero.debug("Feed item retrieved:", 5);
Zotero.debug(item, 5);
let feedItem = yield Zotero.FeedItems.getAsyncByGUID(item.guid);
if (feedItem) {
feedItemIDs.add(feedItem.id);
}
if (!feedItem) {
Zotero.debug("Creating new feed item " + item.guid);
feedItem = new Zotero.FeedItem();
feedItem.guid = item.guid;
feedItem.libraryID = this.id;
} else if (!feedItem.isTranslated) {
// TODO: maybe handle enclosed items on update better
item.enclosedItems = [];
// TODO figure out a better GUID collision resolution system
// that works with sync.
if (feedItem.libraryID != this.libraryID) {
let otherFeed = Zotero.Feeds.get(feedItem.libraryID);
Zotero.debug("Feed item " + feedItem.url + " from " + this.url +
" exists in a different feed " + otherFeed.url + ". Skipping");
continue;
}
Zotero.debug("Feed item " + item.guid + " already in library");
Zotero.debug("Updating metadata");
} else {
// Not new and has been translated
Zotero.debug("Feed item " + item.guid + " is not new and has already been translated. Skipping");
continue;
}
for (let enclosedItem of item.enclosedItems) {
enclosedItem.parentItem = feedItem;
attachmentsToAdd.push(enclosedItem);
}
// Delete invalid data
delete item.guid;
delete item.enclosedItems;
feedItem.fromJSON(item);
if (!feedItem.hasChanged()) {
Zotero.debug("Feed item " + feedItem.guid + " has not changed");
continue
}
feedItem.isRead = false;
toSave.push(feedItem);
}
}
catch (e) {
if (e.message) {
Zotero.debug("Error processing feed from " + this.url);
Zotero.debug(e);
}
this._set('_feedLastCheckError', e.message || 'Error processing feed');
}
if (toSave.length) {
yield Zotero.DB.executeTransaction(function* () {
// Save in reverse order
for (let i=toSave.length-1; i>=0; i--) {
yield toSave[i].save();
}
});
this._set('_feedLastUpdate', Zotero.Date.dateToSQL(new Date(), true));
}
for (let attachment of attachmentsToAdd) {
if (attachment.url.indexOf('pdf') != -1 || attachment.contentType.indexOf('pdf') != -1) {
attachment.parentItemID = attachment.parentItem.id;
attachment.title = Zotero.getString('fileTypes.pdf');
yield Zotero.Attachments.linkFromURL(attachment);
}
}
yield this.clearExpiredItems(feedItemIDs);
this._set('_feedLastCheck', Zotero.Date.dateToSQL(new Date(), true));
yield this.saveTx();
yield this.updateUnreadCount();
deferred.resolve();
this._updating = false;
Zotero.Notifier.trigger('statusChanged', 'feed', this.id);
});
Zotero.Feed.prototype.updateFeed = Zotero.Promise.coroutine(function* () {
try {
let result = yield this._updateFeed();
return result;
} finally {
Zotero.Feeds.scheduleNextFeedCheck();
}
});
Zotero.Feed.prototype.updateUnreadCount = Zotero.Promise.coroutine(function* () {
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) {
this._feedUnreadCount = newCount;
Zotero.Notifier.trigger('unreadCountUpdated', 'feed', this.id);
}
});
Zotero.Feed.prototype.updateFromJSON = Zotero.Promise.coroutine(function* (json) {
yield this.updateFeed();
yield Zotero.FeedItems.markAsReadByGUID(Object.keys(json.markedAsRead));
yield this.updateUnreadCount();
});

View file

@ -31,11 +31,12 @@ Zotero.FeedItem = function(itemTypeOrID, params = {}) {
Zotero.FeedItem._super.call(this, itemTypeOrID);
this._feedItemReadTime = null;
this._feedItemTranslatedTime = null;
Zotero.Utilities.assignProps(this, params, ['guid']);
}
};
Zotero.extendClass(Zotero.Item, Zotero.FeedItem)
Zotero.extendClass(Zotero.Item, Zotero.FeedItem);
Zotero.FeedItem.prototype._objectType = 'feedItem';
Zotero.FeedItem.prototype._containerObject = 'feed';
@ -69,6 +70,22 @@ Zotero.defineProperty(Zotero.FeedItem.prototype, 'isRead', {
}
}
});
//
Zotero.defineProperty(Zotero.FeedItem.prototype, 'isTranslated', {
get: function() {
return !!this._feedItemTranslatedTime;
},
set: function(state) {
if (state != !!this._feedItemTranslatedTime) {
if (state) {
this._feedItemTranslatedTime = Zotero.Date.dateToSQL(new Date(), true);
} else {
this._feedItemTranslatedTime = null;
}
this._changed.feedItemData = true;
}
}
});
Zotero.FeedItem.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) {
if (this.guid && !this.id) {
@ -89,6 +106,32 @@ Zotero.FeedItem.prototype.setField = function(field, value) {
return Zotero.FeedItem._super.prototype.setField.apply(this, arguments);
}
Zotero.FeedItem.prototype.fromJSON = function(json) {
// Handle weird formats in feedItems
let dateFields = ['accessDate', 'dateAdded', 'dateModified'];
for (let dateField of dateFields) {
let val = json[dateField];
if (val) {
let d = new Date(val);
if (isNaN(d.getTime())) {
d = Zotero.Date.sqlToDate(val, true);
}
if (!d || isNaN(d.getTime())) {
d = Zotero.Date.strToDate(val);
d = new Date(d.year, d.month, d.day);
Zotero.debug(dateField + " " + JSON.stringify(d), 1);
}
if (isNaN(d.getTime())) {
Zotero.logError("Discarding invalid " + dateField + " '" + json[dateField]
+ "' for item " + this.libraryKey);
delete json[dateField];
continue;
}
json[dateField] = d.toISOString();
}
}
Zotero.FeedItem._super.prototype.fromJSON.apply(this, arguments);
}
Zotero.FeedItem.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this.guid) {
@ -117,27 +160,133 @@ Zotero.FeedItem.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
return proceed;
});
Zotero.FeedItem.prototype.forceSaveTx = function(options) {
let newOptions = {};
Object.assign(newOptions, options || {});
newOptions.skipEditCheck = true;
return this.saveTx(newOptions);
}
Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
if (this._changed.feedItemData || env.isNew) {
var sql = "REPLACE INTO feedItems VALUES (?,?,?)";
yield Zotero.DB.queryAsync(sql, [env.id, this.guid, this._feedItemReadTime]);
var sql = "REPLACE INTO feedItems VALUES (?,?,?,?)";
yield Zotero.DB.queryAsync(sql, [env.id, this.guid, this._feedItemReadTime, this._feedItemTranslatedTime]);
this._clearChanged('feedItemData');
}
});
Zotero.FeedItem.prototype.forceEraseTx = function(options) {
let newOptions = {};
Object.assign(newOptions, options || {});
newOptions.skipEditCheck = true;
return this.eraseTx(newOptions);
}
Zotero.FeedItem.prototype._finalizeErase = Zotero.Promise.coroutine(function* () {
// Set for syncing
let feed = Zotero.Feeds.get(this.libraryID);
let syncedSettings = feed.getSyncedSettings();
delete syncedSettings.markedAsRead[this.guid];
yield feed.setSyncedSettings(syncedSettings);
return Zotero.FeedItem._super.prototype._finalizeErase.apply(this, arguments);
});
Zotero.FeedItem.prototype.toggleRead = Zotero.Promise.coroutine(function* (state) {
state = state !== undefined ? !!state : !this.isRead;
let changed = this.isRead != state;
if (changed) {
this.isRead = state;
// Set for syncing
let feed = Zotero.Feeds.get(this.libraryID);
let syncedSettings = feed.getSyncedSettings();
if (state) {
syncedSettings.markedAsRead[this.guid] = true;
} else {
delete syncedSettings.markedAsRead[this.guid];
}
yield feed.setSyncedSettings(syncedSettings, true);
yield this.saveTx();
yield feed.updateUnreadCount();
}
});
/**
* Uses the item url to translate an existing feed item.
* If libraryID empty, overwrites feed item, otherwise saves
* in the library
* @param libraryID {int} save item in library
* @param collectionID {int} add item to collection
* @return {Promise<FeedItem|Item>} translated feed item
*/
Zotero.FeedItem.prototype.translate = Zotero.Promise.coroutine(function* (libraryID, collectionID) {
if (Zotero.locked) {
Zotero.debug('Zotero locked, skipping feed item translation');
return;
}
let deferred = Zotero.Promise.defer();
let error = function(e) { Zotero.debug(e, 1); deferred.reject(e); };
let translate = new Zotero.Translate.Web();
if (libraryID) {
// Show progress notifications when scraping to a library
var win = Services.wm.getMostRecentWindow("navigator:browser");
translate.clearHandlers("done");
translate.clearHandlers("itemDone");
translate.setHandler("done", win.Zotero_Browser.progress.Translation.doneHandler);
translate.setHandler("itemDone", win.Zotero_Browser.progress.Translation.itemDoneHandler());
let collection;
if (collectionID) {
collection = yield Zotero.Collections.getAsync(collectionID);
}
win.Zotero_Browser.progress.show();
win.Zotero_Browser.progress.Translation.scrapingTo(libraryID, collection);
}
// Load document
let hiddenBrowser = Zotero.HTTP.processDocuments(
this.getField('url'),
item => deferred.resolve(item),
()=>{}, error, true
);
let doc = yield deferred.promise;
// Set translate document
translate.setDocument(doc);
// Load translators
deferred = Zotero.Promise.defer();
translate.setHandler('translators', (me, translators) => deferred.resolve(translators));
translate.getTranslators();
let translators = yield deferred.promise;
if (!translators || !translators.length) {
Zotero.debug("No translators detected for feed item " + this.id + " with URL " + this.getField('url'), 2);
throw new Zotero.Error("No translators detected for feed item " + this.id + " with URL " + this.getField('url'))
}
translate.setTranslator(translators[0]);
deferred = Zotero.Promise.defer();
if (libraryID) {
let result = yield translate.translate({libraryID, collections: collectionID ? [collectionID] : false})
.then(items => items ? items[0] : false);
Zotero.Browser.deleteHiddenBrowser(hiddenBrowser);
return result;
}
// Clear these to prevent saving
translate.clearHandlers('itemDone');
translate.clearHandlers('itemsDone');
translate.setHandler('error', error);
translate.setHandler('itemDone', (_, items) => deferred.resolve(items));
translate.translate({libraryID: false, saveAttachments: false});
let itemData = yield deferred.promise;
Zotero.Browser.deleteHiddenBrowser(hiddenBrowser);
// clean itemData
const deleteFields = ['attachments', 'notes', 'id', 'itemID', 'path', 'seeAlso', 'version', 'dateAdded', 'dateModified'];
for (let field of deleteFields) {
delete itemData[field];
}
this.fromJSON(itemData);
this.isTranslated = true;
yield this.saveTx();
return this;
});

View file

@ -38,12 +38,13 @@ Zotero.FeedItems = new Proxy(function() {
Zotero.defineProperty(Zotero.Items, '_primaryDataSQLParts', {
get: function() {
let obj = zi_primaryDataSQLParts.call(this);
obj.feedItemGUID = "FeI.guid AS feedItemGUID";
obj.feedItemReadTime = "FeI.readTime AS feedItemReadTime";
obj.feedItemGUID = "FI.guid AS feedItemGUID";
obj.feedItemReadTime = "FI.readTime AS feedItemReadTime";
obj.feedItemTranslatedTime = "FI.translatedTime AS feedItemTranslatedTime";
return obj;
}
}, {lazy: true});
Zotero.Items._primaryDataSQLFrom += " LEFT JOIN feedItems FeI ON (FeI.itemID=O.itemID)";
Zotero.Items._primaryDataSQLFrom += " LEFT JOIN feedItems FI ON (FI.itemID=O.itemID)";
let zi_getObjectForRow = Zotero.Items._getObjectForRow;
Zotero.Items._getObjectForRow = function(row) {
@ -57,7 +58,7 @@ Zotero.FeedItems = new Proxy(function() {
this.getIDFromGUID = Zotero.Promise.coroutine(function* (guid) {
if (_idCache[guid] !== undefined) return _idCache[guid];
id = yield Zotero.DB.valueQueryAsync('SELECT itemID FROM feedItems WHERE guid=?', [guid]);
let id = yield Zotero.DB.valueQueryAsync('SELECT itemID FROM feedItems WHERE guid=?', [guid]);
if (!id) return false;
this._setGUIDMapping(guid, id);
@ -94,6 +95,83 @@ Zotero.FeedItems = new Proxy(function() {
return this.getAsync(id);
});
this.getMarkedAsRead = Zotero.Promise.coroutine(function* (libraryID, onlyGUIDs=false) {
let sql = "SELECT " + (onlyGUIDs ? "guid " : "itemID ") +
"FROM feedItems FI " +
"JOIN items I USING (itemID) " +
"WHERE libraryID=? AND readTime IS NOT NULL";
let ids = yield Zotero.DB.columnQueryAsync(sql, [libraryID]);
if (onlyGUIDs) {
return ids;
}
return Zotero.FeedItems.getAsync(ids);
});
/**
* Used on restore from sync
*/
this.markAsReadByGUID = Zotero.Promise.coroutine(function* (guids) {
if (! Array.isArray(guids)) {
throw new Error('guids must be an array in Zotero.FeedItems.toggleReadByID');
}
let ids = [];
Zotero.debug("Marking items as read");
Zotero.debug(guids);
for (let guid of guids) {
let id = yield this.getIDFromGUID(guid);
if (id) {
ids.push(id);
}
}
return this.toggleReadByID(ids, true);
});
this.toggleReadByID = Zotero.Promise.coroutine(function* (ids, state) {
var feedsToUpdate = new Set();
if (!Array.isArray(ids)) {
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 = false;
for (let item of items) {
if (!item.isRead) {
state = true;
break;
}
}
}
yield Zotero.DB.executeTransaction(function() {
for (let i=0; i<items.length; i++) {
items[i].isRead = state;
// Set for syncing
let feed = Zotero.Feeds.get(items[i].libraryID);
let syncedSettings = feed.getSyncedSettings();
if (state) {
syncedSettings.markedAsRead[items[i].guid] = true;
} else {
delete syncedSettings.markedAsRead[items[i].guid];
}
yield feed.setSyncedSettings(syncedSettings);
yield items[i].save({skipEditCheck: true});
feedsToUpdate.add(feed);
}
});
for (let feed of feedsToUpdate) {
yield Zotero.Promise.all([feed.updateUnreadCount(), feed.storeSyncedSettings()]);
}
});
return this;
}.call({}),

View file

@ -23,7 +23,7 @@
***** END LICENSE BLOCK *****
*/
// Add some feed methods, but otherwise proxy to Zotero.Collections
// Mimics Zotero.Libraries
Zotero.Feeds = new function() {
this._cache = null;
@ -68,6 +68,51 @@ Zotero.Feeds = new function() {
delete this._cache.urlByLibraryID[libraryID];
delete this._cache.libraryIDByURL[url];
}
this.init = function () {
return this.scheduleNextFeedCheck();
}
this.restoreFromJSON = Zotero.Promise.coroutine(function* (json, merge=false) {
Zotero.debug("Restoring feeds from remote JSON");
Zotero.debug(json);
if (merge) {
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
for (let url in json) {
if (syncedFeeds[url]) {
syncedFeeds[url].name = json[url].name;
syncedFeeds[url].cleanupAfter = json[url].cleanupAfter;
syncedFeeds[url].refreshInterval = json[url].refreshInterval;
for (let guid in json[url].markedAsRead) {
syncedFeeds[url].markedAsRead[guid] = true;
}
} else {
syncedFeeds[url] = json[url];
}
}
json = syncedFeeds;
}
yield Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', json);
//
let feeds = Zotero.Feeds.getAll();
for (let feed of feeds) {
if (json[feed.url]) {
Zotero.debug("Feed " + feed.url + " is being updated from remote JSON");
yield feed.updateFromJSON(json[feed.url]);
delete json[feed.url];
} else {
Zotero.debug("Feed " + feed.url + " does not exist in remote JSON. Deleting");
yield feed.eraseTx();
}
}
// Because existing json[feed.url] got deleted, `json` now only contains new feeds
for (let url in json) {
Zotero.debug("Feed " + url + " is being created from remote JSON");
let feed = new Zotero.Feed(json[url]);
yield feed.saveTx();
yield feed.updateFromJSON(json[url]);
}
});
this.getByURL = function(urls) {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
@ -78,17 +123,16 @@ Zotero.Feeds = new function() {
asArray = false;
}
let libraryIDs = Array(urls.length);
let feeds = new Array(urls.length);
for (let i=0; i<urls.length; i++) {
let libraryID = this._cache.libraryIDByURL[urls[i]];
if (!libraryID) {
throw new Error('Feed with url ' + urls[i] + ' not registered in feed cache');
return
}
libraryIDs[i] = libraryID;
feeds[i] = Zotero.Libraries.get(libraryID);
}
let feeds = Zotero.Libraries.get(libraryIDs);
return asArray ? feeds : feeds[0];
}
@ -105,9 +149,67 @@ Zotero.Feeds = new function() {
.map(id => Zotero.Libraries.get(id));
}
this.get = function(libraryID) {
let library = Zotero.Libraries.get(libraryID);
return library.isFeed ? library : undefined;
}
this.haveFeeds = function() {
if (!this._cache) throw new Error("Zotero.Feeds cache is not initialized");
return !!Object.keys(this._cache.urlByLibraryID).length
}
let globalFeedCheckDelay = Zotero.Promise.resolve();
this.scheduleNextFeedCheck = Zotero.Promise.coroutine(function* () {
Zotero.debug("Scheduling next feed update");
let sql = "SELECT ( CASE "
+ "WHEN lastCheck IS NULL THEN 0 "
+ "ELSE strftime('%s', lastCheck) + refreshInterval*3600 - strftime('%s', 'now') "
+ "END ) AS nextCheck "
+ "FROM feeds WHERE refreshInterval IS NOT NULL "
+ "ORDER BY nextCheck ASC LIMIT 1";
var nextCheck = yield Zotero.DB.valueQueryAsync(sql);
if (this._nextFeedCheck) {
this._nextFeedCheck.cancel();
this._nextFeedCheck = null;
}
if (nextCheck !== false) {
nextCheck = nextCheck > 0 ? nextCheck * 1000 : 0;
Zotero.debug("Next feed check in " + nextCheck / 60000 + " minutes");
this._nextFeedCheck = Zotero.Promise.delay(nextCheck);
Zotero.Promise.all([this._nextFeedCheck, globalFeedCheckDelay])
.then(() => {
globalFeedCheckDelay = Zotero.Promise.delay(60000); // Don't perform auto-updates more than once per minute
return this.updateFeeds()
})
.catch(e => {
if (e instanceof Zotero.Promise.CancellationError) {
Zotero.debug('Next update check cancelled');
return;
}
throw e;
});
} else {
Zotero.debug("No feeds with auto-update");
}
});
this.updateFeeds = Zotero.Promise.coroutine(function* () {
let sql = "SELECT libraryID AS id FROM feeds "
+ "WHERE refreshInterval IS NOT NULL "
+ "AND ( lastCheck IS NULL "
+ "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(', '));
for (let i=0; i<needUpdate.length; i++) {
let feed = Zotero.Feeds.get(needUpdate[i]);
yield feed._updateFeed();
}
Zotero.debug("All feed updates done");
this.scheduleNextFeedCheck();
});
}

View file

@ -1660,6 +1660,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
let toAdd = Zotero.Utilities.arrayDiff(newCollections, oldCollections);
let toRemove = Zotero.Utilities.arrayDiff(oldCollections, newCollections);
env.collectionsAdded = toAdd;
env.collectionsRemoved = toRemove;
if (toAdd.length) {
for (let i=0; i<toAdd.length; i++) {

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} [asIDs=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, asIDs=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 (asIDs) {
return ids;
}
return this.getAsync(ids);
});

View file

@ -219,8 +219,8 @@ Zotero.Relations = new function () {
// Get all object URIs except merge-tracking ones
let sql = "SELECT " + objectsClass.idColumn + " AS id, predicate, object "
+ "FROM " + type + "Relations "
+ "JOIN relationPredicates USING (predicateID) WHERE predicate != ?";
+ "FROM " + objectsClass.relationsTable
+ " JOIN relationPredicates USING (predicateID) WHERE predicate != ?";
let rows = yield Zotero.DB.queryAsync(sql, [this.replacedItemPredicate]);
for (let i = 0; i < rows.length; i++) {
let row = rows[i];

View file

@ -0,0 +1,521 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2015 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
/**
* Sample feeds:
*
* http://cyber.law.harvard.edu/rss/examples/rss2sample.xml
* http://feeds.feedburner.com/acs/acbcct
* http://www.cell.com/molecular-cell/current.rss
* http://ieeexplore.ieee.org/search/searchresult.jsp?searchField%3DSearch_All%26queryText%3Dwater&searchOrigin=saved_searches&rssFeed=true&rssFeedName=water
* http://www.sciencemag.org/rss/current.xml
* http://rss.sciencedirect.com/publication/science/20925212
* http://www.ncbi.nlm.nih.gov/entrez/eutils/erss.cgi?rss_guid=1fmfIeN4X5Q8HemTZD5Rj6iu6-FQVCn7xc7_IPIIQtS1XiD9bf
* http://export.arxiv.org/rss/astro-ph
* http://fhs.dukejournals.org/rss_feeds/recent.xml
*/
/**
* class Zotero.FeedReader
* Asynchronously reads an ATOM/RSS feed
*
* @param {String} url URL of the feed
*
* @property {Zotero.Promise<Object>} feedProperties An object
* representing feed properties
* @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.
* @method {void} terminate Stops retrieving/parsing the feed. Data parsed up
* to this point is still available.
*/
Zotero.FeedReader = function(url) {
if (!url) throw new Error("Feed URL must be supplied");
this._url = url;
this._feedItems = [Zotero.Promise.defer()];
this._feedProcessed = Zotero.Promise.defer();
let feedFetched = Zotero.Promise.defer();
feedFetched.promise.then(function(feed) {
let info = {};
info.title = feed.title ? feed.title.plainText() : '';
info.subtitle = feed.subtitle ? feed.subtitle.plainText() : '';
if (feed.updated) info.updated = new Date(feed.updated);
// categories: MDN says "not yet implemented"
info.creators = Zotero.FeedReader._processCreators(feed, 'authors', 'author');
// TODO: image as icon
let publicationTitle = Zotero.FeedReader._getFeedField(feed, 'publicationName', 'prism')
|| Zotero.FeedReader._getFeedField(feed, 'pubTitle');
if (publicationTitle) info.publicationTitle = publicationTitle;
let publisher = Zotero.FeedReader._getFeedField(feed, 'publisher', 'dc');
if (publisher) info.publisher = publisher;
let rights = (feed.rights && feed.rights.plainText())
|| Zotero.FeedReader._getFeedField(feed, 'copyright', 'prism')
|| Zotero.FeedReader._getFeedField(feed, 'rights', 'dc')
|| Zotero.FeedReader._getFeedField(feed, 'copyright');
if (rights) info.rights = rights;
let issn = Zotero.FeedReader._getFeedField(feed, 'issn', 'prism');
if (issn) info.ISSN = issn;
let isbn = Zotero.FeedReader._getFeedField(feed, 'isbn', 'prism')
|| Zotero.FeedReader._getFeedField(feed, 'isbn')
if (isbn) info.ISBN = isbn;
let language = Zotero.FeedReader._getFeedField(feed, 'language', 'dc')
|| Zotero.FeedReader._getFeedField(feed, 'language');
if (language) info.language = language;
let ttl = Zotero.FeedReader._getFeedField(feed, 'ttl');
if (ttl) info.ttl = ttl;
this._feedProperties = info;
this._feed = feed;
}.bind(this)).then(function(){
let items = this._feed.items;
if (items && items.length) {
for (let i=0; i<items.length; i++) {
let item = items.queryElementAt(i, Components.interfaces.nsIFeedEntry);
if (!item) continue;
let feedItem = Zotero.FeedReader._getFeedItem(item, this._feedProperties);
if (!feedItem) continue;
let lastItem = this._feedItems[this._feedItems.length - 1];
this._feedItems.push(Zotero.Promise.defer()); // Push a new deferred promise so an iterator has something to return
lastItem.resolve(feedItem);
}
}
this._feedProcessed.resolve();
}.bind(this)).catch(function(e) {
Zotero.debug("Feed processing failed " + e.message);
this._feedProcessed.reject(e);
}.bind(this)).finally(function() {
// Make sure the last promise gets resolved to null
let lastItem = this._feedItems[this._feedItems.length - 1];
lastItem.resolve(null);
}.bind(this));
// Set up asynchronous feed processor
let feedProcessor = Components.classes["@mozilla.org/feed-processor;1"]
.createInstance(Components.interfaces.nsIFeedProcessor);
let feedUrl = Services.io.newURI(url, null, null);
feedProcessor.parseAsync(null, feedUrl);
feedProcessor.listener = {
/*
* MDN suggests that we could use nsIFeedProgressListener to handle the feed
* as it gets loaded, but this is actually not implemented (as of 32.0.3),
* so we have to load the whole feed and handle it in handleResult.
*/
handleResult: (result) => {
if (!result.doc) {
this.terminate("No Feed");
return;
}
let newFeed = result.doc.QueryInterface(Components.interfaces.nsIFeed);
feedFetched.resolve(newFeed);
}
};
Zotero.debug("FeedReader: Fetching feed from " + feedUrl.spec);
this._channel = Services.io.newChannelFromURI2(feedUrl, null,
Services.scriptSecurityManager.getSystemPrincipal(), null,
Ci.nsILoadInfo.SEC_NORMAL, Ci.nsIContentPolicy.TYPE_OTHER);
this._channel.asyncOpen(feedProcessor, null); // Sends an HTTP request
}
/*
* The constructor initiates async feed processing, but _feedProcessed
* needs to be resolved before proceeding.
*/
Zotero.FeedReader.prototype.process = Zotero.Promise.coroutine(function* () {
return this._feedProcessed.promise;
});
/*
* Terminate feed processing at any given time
* @param {String} status Reason for terminating processing
*/
Zotero.FeedReader.prototype.terminate = function(status) {
Zotero.debug("FeedReader: Terminating feed reader (" + status + ")");
// Reject feed promise if not resolved yet
if (this._feedProcessed.promise.isPending()) {
this._feedProcessed.reject(status);
}
// Reject feed item promise if not resolved yet
let lastItem = this._feedItems[this._feedItems.length - 1];
if (lastItem.promise.isPending()) {
lastItem.reject(status);
}
// Close feed connection
if (this._channel.isPending) {
this._channel.cancel(Components.results.NS_BINDING_ABORTED);
}
};
Zotero.defineProperty(Zotero.FeedReader.prototype, 'feedProperties', {
get: function(){
if (!this._feedProperties) {
throw new Error("Feed has not been resolved yet. Try calling FeedReader#process first")
}
return this._feedProperties
}
});
/*
* Feed item iterator
* Each iteration returns a _promise_ for an item. The promise _MUST_ be
* resolved before requesting the next item.
* The last item will always be resolved to `null`, unless the feed processing
* is terminated ahead of time, in which case it will be rejected with the reason
* for termination.
*/
Zotero.defineProperty(Zotero.FeedReader.prototype, 'ItemIterator', {
get: function() {
let items = this._feedItems;
let feedReader = this;
let iterator = function() {
if (!feedReader._feedProperties) {
throw new Error("Feed has not been resolved yet. Try calling FeedReader#process first")
}
this.index = 0;
};
iterator.prototype.next = function() {
let item = items[this.index++];
return {
value: item ? item.promise : null,
done: this.index >= items.length
};
};
iterator.prototype.last = function() {
return items[items.length-1];
}
return iterator;
}
}, {lazy: true});
/*****************************
* Item processing functions *
*****************************/
/**
* Determine item type based on item data
*/
Zotero.FeedReader._guessItemType = function(item) {
// Default to journalArticle
item.itemType = 'journalArticle';
if (item.ISSN) {
return; // journalArticle
}
if (item.ISBN) {
item.itemType = 'bookSection';
return;
}
if (item.publicationType) {
let type = item.publicationType.toLowerCase();
if (type.indexOf('conference') != -1) {
item.itemType = 'conferencePaper';
return;
}
if (type.indexOf('journal') != -1) {
item.itemType = 'journalArticle';
return;
}
if (type.indexOf('book') != -1) {
item.itemType = 'bookSection';
return;
}
}
};
/*
* Fetch creators from given field of a feed entry
*/
Zotero.FeedReader._processCreators = function(feedEntry, field, role) {
let names = [],
nameStr;
try {
let personArr = feedEntry[field]; // Seems like this part can throw if there is no author data in the feed
for (let i=0; i<personArr.length; i++) {
let person = personArr.queryElementAt(i, Components.interfaces.nsIFeedPerson);
if (!person || !person.name) continue;
let name = Zotero.Utilities.cleanTags(Zotero.Utilities.trimInternal(person.name));
if (!name) continue;
let commas = name.split(',').length - 1,
other = name.split(/\s(?:and|&)\s|;/).length - 1,
separators = commas + other;
if (personArr.length == 1 &&
// Has typical name separators
(other || commas > 1
// If only one comma and first part has more than one space,
// it's probably not lastName, firstName
|| (commas == 1 && name.split(/\s*,/)[0].indexOf(' ') != -1)
)
) {
// Probably multiple authors listed in a single field
nameStr = name;
break; // For clarity. personArr.length == 1 anyway
} else {
names.push(name);
}
}
}
catch(e) {
if (e.result != Components.results.NS_ERROR_FAILURE) throw e;
if (field != 'authors') return [];
// ieeexplore places these in "authors"... sigh
nameStr = Zotero.FeedReader._getFeedField(feedEntry, 'authors');
if (nameStr) nameStr = Zotero.Utilities.trimInternal(nameStr);
if (!nameStr) return [];
}
if (nameStr) {
names = nameStr.split(/\s(?:and|&)\s|\s*[,;]\s*/);
}
let creators = [];
for (let i=0; i<names.length; i++) {
let creator = Zotero.Utilities.cleanAuthor(
names[i],
role,
names[i].split(',').length == 2
);
if (!creator.firstName) {
creator.fieldMode = 1;
}
// Sometimes these end up empty when parsing really nasty HTML based fields, so just skip.
if (!creator.firstName && !creator.lastName) {
continue;
}
creators.push(creator);
}
return creators;
}
/*
* Parse feed entry into a Zotero item
*/
Zotero.FeedReader._getFeedItem = function(feedEntry, feedInfo) {
// ID is not required, but most feeds have these and we have to rely on them
// to handle updating properly
if (!feedEntry.id) {
Zotero.debug("FeedReader: Feed item missing an ID");
return;
}
let item = {
guid: feedEntry.id
};
if (feedEntry.title) item.title = Zotero.FeedReader._getRichText(feedEntry.title, 'title');
if (feedEntry.summary) {
item.abstractNote = Zotero.FeedReader._getRichText(feedEntry.summary, 'abstractNote');
if (!item.title) {
// We will probably have to trim this, so let's use plain text to
// avoid splitting inside some markup
let title = Zotero.Utilities.trimInternal(feedEntry.summary.plainText());
let splitAt = title.lastIndexOf(' ', 50);
if (splitAt == -1) splitAt = 50;
item.title = title.substr(0, splitAt);
if (splitAt <= title.length) item.title += '...';
}
}
if (feedEntry.link) item.url = feedEntry.link.spec;
if (feedEntry.rights) item.rights = Zotero.FeedReader._getRichText(feedEntry.rights, 'rights');
item.creators = Zotero.FeedReader._processCreators(feedEntry, 'authors', 'author');
if (!item.creators.length) {
// Use feed authors as item author. Maybe not the best idea.
for (let i=0; i<feedInfo.creators.length; i++) {
if (feedInfo.creators[i].creatorType != 'author') continue;
item.creators.push(feedInfo.creators[i]);
}
}
let contributors = Zotero.FeedReader._processCreators(feedEntry, 'contributors', 'contributor');
if (contributors.length) item.creators = item.creators.concat(contributors);
/** Done with basic metadata, now look for better data **/
let date = Zotero.FeedReader._getFeedField(feedEntry, 'publicationDate', 'prism')
|| Zotero.FeedReader._getFeedField(feedEntry, 'date', 'dc')
|| Zotero.FeedReader._getFeedField(feedEntry, 'pubDate') // RSS
|| Zotero.FeedReader._getFeedField(feedEntry, 'published') // Atom
|| Zotero.FeedReader._getFeedField(feedEntry, 'updated'); // Atom
if (date) item.date = date;
let publicationTitle = Zotero.FeedReader._getFeedField(feedEntry, 'publicationName', 'prism')
|| Zotero.FeedReader._getFeedField(feedEntry, 'source', 'dc')
|| Zotero.FeedReader._getFeedField(feedEntry, 'pubTitle');
if (publicationTitle) item.publicationTitle = publicationTitle;
let publicationType = Zotero.FeedReader._getFeedField(feedEntry, 'pubType');
if (publicationType) item.publicationType = publicationType;
let startPage = Zotero.FeedReader._getFeedField(feedEntry, 'startPage');
let endPage = Zotero.FeedReader._getFeedField(feedEntry, 'endPage');
if (startPage || endPage) {
item.pages = ( startPage || '' )
+ ( endPage && startPage ? '' : '' )
+ ( endPage || '' );
}
let issn = Zotero.FeedReader._getFeedField(feedEntry, 'issn', 'prism');
if (issn) item.ISSN = issn;
let isbn = Zotero.FeedReader._getFeedField(feedEntry, 'isbn', 'prism')
|| Zotero.FeedReader._getFeedField(feedEntry, 'isbn')
if (isbn) item.ISBN = isbn;
let identifier = Zotero.FeedReader._getFeedField(feedEntry, 'identifier', 'dc');
if (identifier) {
let cleanId = Zotero.Utilities.cleanDOI(identifier);
if (cleanId) {
if (!item.DOI) item.DOI = cleanId;
} else if (cleanId = Zotero.Utilities.cleanISBN(identifier)) {
if (!item.ISBN) item.ISBN = cleanId;
} else if (cleanId = Zotero.Utilities.cleanISSN(identifier)) {
if (!item.ISSN) item.ISSN = cleanId;
}
}
let publisher = Zotero.FeedReader._getFeedField(feedEntry, 'publisher', 'dc');
if (publisher) item.publisher = publisher;
let rights = Zotero.FeedReader._getFeedField(feedEntry, 'copyright', 'prism')
|| Zotero.FeedReader._getFeedField(feedEntry, 'rights', 'dc')
|| Zotero.FeedReader._getFeedField(feedEntry, 'copyright');
if (rights) item.rights = rights;
let language = Zotero.FeedReader._getFeedField(feedEntry, 'language', 'dc')
|| Zotero.FeedReader._getFeedField(feedEntry, 'language');
if (language) item.language = language;
/** Incorporate missing values from feed metadata **/
let supplementFields = ['publicationTitle', 'ISSN', 'publisher', 'rights', 'language'];
for (let i=0; i<supplementFields.length; i++) {
let field = supplementFields[i];
if (!item[field] && feedInfo[field]) {
item[field] = feedInfo[field];
}
}
Zotero.FeedReader._guessItemType(item);
item.enclosedItems = Zotero.FeedReader._getEnclosedItems(feedEntry);
return item;
}
/*********************
* Utility functions *
*********************/
/*
* Convert HTML-formatted text to Zotero-compatible formatting
*/
Zotero.FeedReader._getRichText = function(feedText, field) {
let domDiv = Zotero.Utilities.Internal.getDOMDocument().createElement("div");
let domFragment = feedText.createDocumentFragment(domDiv);
return Zotero.Utilities.dom2text(domFragment, field);
};
/*
* Get field value from feed entry by namespace:fieldName
*/
// Properties are stored internally as ns+name, but only some namespaces are
// supported. Others are just "null"
let ns = {
'prism': 'null',
'dc': 'dc:'
}
Zotero.FeedReader._getFeedField = function(feedEntry, field, namespace) {
let prefix = namespace ? ns[namespace] || 'null' : '';
try {
return feedEntry.fields.getPropertyAsAUTF8String(prefix+field);
} catch(e) {}
try {
if (namespace && !ns[namespace]) {
prefix = namespace + ':';
return feedEntry.fields.getPropertyAsAUTF8String(prefix+field);
}
} catch(e) {}
return;
}
Zotero.FeedReader._getEnclosedItems = function(feedEntry) {
var enclosedItems = [];
if (feedEntry.enclosures) {
for (let i = 0; i < feedEntry.enclosures.length; i++) {
let elem = feedEntry.enclosures.queryElementAt(0, Components.interfaces.nsIPropertyBag2);
if (elem.get('url')) {
let enclosedItem = {url: elem.get('url'), contentType: elem.get('type') || ''};
enclosedItems.push(enclosedItem);
}
}
}
return enclosedItems;
}

View file

@ -53,9 +53,9 @@ Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) {
this._refreshPromise = Zotero.Promise.resolve();
this._unregisterID = Zotero.Notifier.registerObserver(
this._unregisterID = Zotero.Notifier.registerObserver(
this,
['item', 'collection-item', 'item-tag', 'share-items', 'bucket'],
['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem'],
'itemTreeView',
50
);
@ -98,6 +98,7 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
}
this._treebox = treebox;
this.setSortColumn();
if (this._ownerDocument.defaultView.ZoteroPane_Local) {
this._ownerDocument.defaultView.ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
@ -259,6 +260,43 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
});
Zotero.ItemTreeView.prototype.setSortColumn = function() {
var dir, col, currentCol, currentDir;
for (let i=0, len=this._treebox.columns.count; i<len; i++) {
let column = this._treebox.columns.getColumnAt(i);
if (column.element.getAttribute('sortActive')) {
currentCol = column;
currentDir = column.element.getAttribute('sortDirection');
column.element.removeAttribute('sortActive');
column.element.removeAttribute('sortDirection');
break;
}
}
let colID = Zotero.Prefs.get('itemTree.sortColumnID');
// Restore previous sort setting (feed -> non-feed)
if (! this.collectionTreeRow.isFeed() && colID) {
col = this._treebox.columns.getNamedColumn(colID);
dir = Zotero.Prefs.get('itemTree.sortDirection');
Zotero.Prefs.clear('itemTree.sortColumnID');
Zotero.Prefs.clear('itemTree.sortDirection');
// No previous sort setting stored, so store it (non-feed -> feed)
} else if (this.collectionTreeRow.isFeed() && !colID && currentCol) {
Zotero.Prefs.set('itemTree.sortColumnID', currentCol.id);
Zotero.Prefs.set('itemTree.sortDirection', currentDir);
// Retain current sort setting (non-feed -> non-feed)
} else {
col = currentCol;
dir = currentDir;
}
if (col) {
col.element.setAttribute('sortActive', true);
col.element.setAttribute('sortDirection', dir);
}
}
/**
* Reload the rows from the data access methods
* (doesn't call the tree.invalidate methods, etc.)
@ -391,6 +429,14 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
return;
}
// FeedItem may have changed read/unread state
if (type == 'feedItem' && action == 'modify') {
for (let i=0; i<ids.length; i++) {
this._treebox.invalidateRow(this._itemRowMap[ids[i]]);
}
return;
}
// Clear item type icon and tag colors when a tag is added to or removed from an item
if (type == 'item-tag') {
// TODO: Only update if colored tag changed?
@ -526,12 +572,9 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
// Since a remove involves shifting of rows, we have to do it in order,
// so sort the ids by row
var rows = [];
let push = action == 'delete' || action == 'trash';
for (var i=0, len=ids.length; i<len; i++) {
let push = false;
if (action == 'delete' || action == 'trash') {
push = true;
}
else {
if (!push) {
push = !collectionTreeRow.ref.hasItem(ids[i]);
}
// Row might already be gone (e.g. if this is a child and
@ -567,7 +610,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
}
}
}
else if (action == 'modify')
else if (type == 'item' && action == 'modify')
{
// Clear row caches
var items = yield Zotero.Items.getAsync(ids);
@ -685,7 +728,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
}
}
}
else if(action == 'add')
else if(type == 'item' && action == 'add')
{
let items = yield Zotero.Items.getAsync(ids);
@ -1223,6 +1266,9 @@ Zotero.ItemTreeView.prototype.isSorted = function()
}
Zotero.ItemTreeView.prototype.cycleHeader = function (column) {
if (this.collectionTreeRow.isFeed()) {
return;
}
for(var i=0, len=this._treebox.columns.count; i<len; i++)
{
col = this._treebox.columns.getColumnAt(i);
@ -2082,9 +2128,12 @@ Zotero.ItemTreeView.prototype.getSortedItems = function(asIDs) {
Zotero.ItemTreeView.prototype.getSortField = function() {
var column = this._treebox.columns.getSortedColumn()
if (this.collectionTreeRow.isFeed()) {
return 'id';
}
var column = this._treebox.columns.getSortedColumn();
if (!column) {
column = this._treebox.columns.getFirstColumn()
column = this._treebox.columns.getFirstColumn();
}
// zotero-items-column-_________
return column.id.substring(20);
@ -2128,6 +2177,9 @@ Zotero.ItemTreeView.prototype.getSortFields = function () {
* Returns 'ascending' or 'descending'
*/
Zotero.ItemTreeView.prototype.getSortDirection = function() {
if (this.collectionTreeRow.isFeed()) {
return Zotero.Prefs.get('feeds.sortAscending') ? 'ascending' : 'descending';
}
var column = this._treebox.columns.getSortedColumn();
if (!column) {
return 'ascending';
@ -2432,6 +2484,8 @@ Zotero.ItemTreeView.prototype.onDragStart = function (event) {
}
// Get Quick Copy format for current URL
// TODO: Fix this
/** Currently broken
var url = this._ownerDocument.defaultView.content && this._ownerDocument.defaultView.content.location ?
this._ownerDocument.defaultView.content.location.href : null;
var format = Zotero.QuickCopy.getFormatFromURL(url);
@ -2470,6 +2524,7 @@ Zotero.ItemTreeView.prototype.onDragStart = function (event) {
Zotero.debug(e);
Components.utils.reportError(e + " with '" + format.id + "'");
}
*/
};
@ -3051,56 +3106,25 @@ Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) {
// Mark items not matching search as context rows, displayed in gray
if (this._searchMode && !this._searchItemIDs[itemID]) {
// <=Fx21
if (prop) {
var aServ = Components.classes["@mozilla.org/atom-service;1"].
getService(Components.interfaces.nsIAtomService);
prop.AppendElement(aServ.getAtom("contextRow"));
}
// Fx22+
else {
props.push("contextRow");
}
props.push("contextRow");
}
// Mark hasAttachment column, which needs special image handling
if (col.id == 'zotero-items-column-hasAttachment') {
// <=Fx21
if (prop) {
var aServ = Components.classes["@mozilla.org/atom-service;1"].
getService(Components.interfaces.nsIAtomService);
prop.AppendElement(aServ.getAtom("hasAttachment"));
}
// Fx22+
else {
props.push("hasAttachment");
}
props.push("hasAttachment");
// Don't show pie for open parent items, since we show it for the
// child item
if (this.isContainer(row) && this.isContainerOpen(row)) {
return props.join(" ");
}
var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref);
//var num = Math.round(new Date().getTime() % 10000 / 10000 * 64);
if (num !== false) {
// <=Fx21
if (prop) {
if (!aServ) {
var aServ = Components.classes["@mozilla.org/atom-service;1"].
getService(Components.interfaces.nsIAtomService);
}
prop.AppendElement(aServ.getAtom("pie"));
prop.AppendElement(aServ.getAtom("pie" + num));
}
// Fx22+
else {
props.push("pie", "pie" + num);
}
if (!this.isContainer(row) || !this.isContainerOpen(row)) {
var num = Zotero.Sync.Storage.getItemDownloadImageNumber(treeRow.ref);
//var num = Math.round(new Date().getTime() % 10000 / 10000 * 64);
if (num !== false) props.push("pie", "pie" + num);
}
}
// Style unread items in feeds
if (treeRow.ref.isFeedItem && !treeRow.ref.isRead) props.push('unread');
return props.join(" ");
}

View file

@ -95,8 +95,9 @@ Zotero.Notifier = new function(){
* Possible values:
*
* event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
* 'remove' (ci, it), 'refresh', 'redraw', 'trash'
* type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group', 'relation'
* 'remove' (ci, it), 'refresh', 'redraw', 'trash', 'unreadCountUpdated'
* type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag',
* 'group', 'relation', 'feed', 'feedItem'
* ids - single id or array of ids
*
* Notes:

View file

@ -365,6 +365,110 @@ Zotero.ProgressWindow = function(_window = null) {
this._hbox.style.filter = "";
});
this.Translation = {};
this.Translation.operationInProgress = function() {
var desc = Zotero.localeJoin([
Zotero.getString('general.operationInProgress'),
Zotero.getString('general.operationInProgress.waitUntilFinishedAndTryAgain')
]);
self.Translation._scrapeError(desc);
};
this.Translation.cannotEditCollection = function() {
var desc = Zotero.getString('save.error.cannotMakeChangesToCollection');
self.Translation._scrapeError(desc);
};
this.Translation.cannotAddToPublications = function () {
var desc = Zotero.getString('save.error.cannotAddToMyPublications');
self.Translation._scrapeError(desc);
};
this.Translation.cannotAddToFeed = function() {
var desc = Zotero.getString('save.error.cannotAddToFeed');
self.Translation._scrapeError(desc);
};
this.Translation.scrapingTo = function(libraryID, collection) {
if(Zotero.isConnector) {
Zotero.Connector.callMethod("getSelectedCollection", {}, function(response, status) {
if(status !== 200) {
self.changeHeadline(Zotero.getString("ingester.scraping"));
} else {
self.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");
}
self.changeHeadline(Zotero.getString("ingester.scrapingTo"),
"chrome://zotero/skin/treesource-"+(collection ? "collection" : "library")+".png",
name+"\u2026");
}
};
this.Translation.doneHandler = function(obj, returnValue) {
if(!returnValue) {
// Include link to translator troubleshooting page
var url = "https://www.zotero.org/support/troubleshooting_translator_issues";
var linkText = '<a href="' + url + '" tooltiptext="' + url + '">'
+ Zotero.getString('ingester.scrapeErrorDescription.linkText') + '</a>';
var desc = Zotero.getString("ingester.scrapeErrorDescription", linkText)
self.Translation._scrapeError(desc);
} else {
self.startCloseTimer();
}
};
this.Translation.itemDoneHandler = function(_attachmentsMap) {
_attachmentsMap = _attachmentsMap || new WeakMap();
return function(obj, dbItem, item) {
self.show();
var itemProgress = new self.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 self.ItemProgress(
Zotero.Utilities.determineAttachmentIcon(attachment),
attachment.title, itemProgress));
}
}
};
this.Translation.attachmentProgressHandler = function(_attachmentsMap) {
_attachmentsMap = _attachmentsMap || new WeakMap();
return 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.Translation._scrapeError = function(description) {
self.changeHeadline(Zotero.getString("ingester.scrapeError"));
self.addDescription(description);
self.show();
self.startCloseTimer(8000)
}
function _onWindowLoaded() {
_windowLoading = false;
_windowLoaded = true;

View file

@ -2166,9 +2166,6 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("DROP TABLE tagsOld");
yield Zotero.DB.queryAsync("DROP TABLE librariesOld");
// Feeds
yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n libraryID INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n url TEXT NOT NULL UNIQUE,\n lastUpdate TIMESTAMP,\n lastCheck TIMESTAMP,\n lastCheckError TEXT,\n cleanupAfter INT,\n refreshInterval INT,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("CREATE TABLE feedItems (\n itemID INTEGER PRIMARY KEY,\n guid TEXT NOT NULL UNIQUE,\n readTime TIMESTAMP,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
}
if (i == 81) {
@ -2192,6 +2189,14 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 98, NULL, 8)");
yield Zotero.DB.queryAsync("INSERT INTO itemTypeFields VALUES (17, 42, NULL, 9)");
}
if (i == 83) {
// Feeds
yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS feeds");
yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS feedItems");
yield Zotero.DB.queryAsync("CREATE TABLE feeds (\n libraryID INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n url TEXT NOT NULL UNIQUE,\n lastUpdate TIMESTAMP,\n lastCheck TIMESTAMP,\n lastCheckError TEXT,\n cleanupAfter INT,\n refreshInterval INT,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("CREATE TABLE feedItems (\n itemID INTEGER PRIMARY KEY,\n guid TEXT NOT NULL UNIQUE,\n readTime TIMESTAMP,\n translatedTime TIMESTAMP,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
}
}
yield _updateDBVersion('userdata', toVersion);

View file

@ -21,6 +21,7 @@ Zotero.Sync.Storage.Local = {
switch (libraryType) {
case 'user':
case 'publications':
case 'feed':
return Zotero.Prefs.get("sync.storage.protocol") == 'webdav' ? 'webdav' : 'zfs';
case 'group':

View file

@ -879,6 +879,7 @@ Zotero.Sync.Data.Local = {
Zotero.debug("SAVING " + json.key + " WITH SYNCED");
Zotero.debug(obj.version);
yield obj.save({
skipEditCheck: true,
skipDateModifiedUpdate: true,
skipSelect: true,
errorHandler: function (e) {

View file

@ -242,11 +242,12 @@ Zotero.Sync.Runner_Module = function (options = {}) {
// Prompt if library empty and there is no userID stored
this.checkEmptyLibrary = Zotero.Promise.coroutine(function* (keyInfo) {
let library = Zotero.Libraries.userLibrary;
let feeds = Zotero.Feeds.getAll();
let userID = Zotero.Users.getCurrentUserID();
if (!userID) {
let hasItems = yield library.hasItems();
if (!hasItems) {
if (!hasItems && feeds.length <= 0) {
let ps = Services.prompt;
let index = ps.confirmEx(
null,

View file

@ -1212,7 +1212,7 @@ Zotero.Translate.Base.prototype = {
if(!translators.length) {
me.complete(false, "Could not find an appropriate translator");
} else {
me.setTranslator(translators);
me.setTranslator(translators[0]);
deferred.resolve(Zotero.Translate.Base.prototype.translate.call(me, options));
}
});

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";
@ -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

@ -252,6 +252,38 @@ Zotero.Utilities = {
var x = x.replace(/^[\x00-\x27\x29-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\s]+/, "");
return x.replace(/[\x00-\x28\x2A-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\s]+$/, "");
},
/**
* Cleans a http url string
* @param url {String}
* @params tryHttp {Boolean} Attempt prepending 'http://' to the url
* @returns {String}
*/
cleanURL: function(url, tryHttp=false) {
url = url.trim();
if (!url) return false;
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
try {
return ios.newURI(url, null, null).spec; // Valid URI if succeeds
} catch (e) {
if (e instanceof Components.Exception
&& e.result == Components.results.NS_ERROR_MALFORMED_URI
) {
if (tryHttp && /\w\.\w/.test(url)) {
// Assume it's a URL missing "http://" part
try {
return ios.newURI('http://' + url, null, null).spec;
} catch (e) {}
}
Zotero.debug('cleanURL: Invalid URI: ' + url, 2);
return false;
}
throw e;
}
},
/**
* Eliminates HTML tags, replacing &lt;br&gt;s with newlines

View file

@ -626,7 +626,8 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
yield Zotero.Searches.init();
yield Zotero.Creators.init();
yield Zotero.Groups.init();
yield Zotero.Relations.init()
yield Zotero.Relations.init();
yield Zotero.Feeds.init();
let libraryIDs = Zotero.Libraries.getAll().map(x => x.libraryID);
for (let libraryID of libraryIDs) {

View file

@ -501,9 +501,9 @@ var ZoteroPane = new function()
}
}
function handleKeyUp(event, from) {
if (from == 'zotero-pane') {
function handleKeyUp(event) {
var from = event.originalTarget.id;
if (from == 'zotero-items-tree') {
if ((Zotero.isWin && event.keyCode == 17) ||
(!Zotero.isWin && event.keyCode == 18)) {
if (this.highlightTimer) {
@ -511,6 +511,7 @@ var ZoteroPane = new function()
this.highlightTimer = null;
}
ZoteroPane_Local.collectionsView.setHighlightedRows();
return;
}
}
}
@ -552,6 +553,11 @@ var ZoteroPane = new function()
event.preventDefault();
return;
}
var key = String.fromCharCode(event.which);
if (key) {
var command = Zotero.Keys.getCommand(key);
}
if (from == 'zotero-collections-tree') {
if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) ||
@ -565,7 +571,7 @@ var ZoteroPane = new function()
else if (from == 'zotero-items-tree') {
// Focus TinyMCE explicitly on tab key, since the normal focusing
// doesn't work right
if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) {
if (!event.shiftKey && event.keyCode == String.fromCharCode(event.which)) {
var deck = document.getElementById('zotero-item-pane-content');
if (deck.selectedPanel.id == 'zotero-view-note') {
setTimeout(function () {
@ -595,12 +601,19 @@ var ZoteroPane = new function()
//event.stopPropagation();
return;
}
}
var key = String.fromCharCode(event.which);
if (!key) {
Zotero.debug('No key');
return;
else if (command == 'toggleRead') {
// Toggle read/unread
let row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
if (!row || !row.isFeed()) return;
if(itemReadTimeout) {
itemReadTimeout.cancel();
itemReadTimeout = null;
}
let itemIDs = this.getSelectedItems(true);
Zotero.FeedItems.toggleReadByID(itemIDs);
return;
}
}
// Ignore modifiers other than Ctrl-Shift/Cmd-Shift
@ -608,12 +621,16 @@ var ZoteroPane = new function()
return;
}
var command = Zotero.Keys.getCommand(key);
if (!key) {
Zotero.debug('No key');
return;
}
if (!command) {
return;
}
Zotero.debug(command);
Zotero.debug('Keyboard shortcut: ', command);
// Errors don't seem to make it out otherwise
try {
@ -839,6 +856,20 @@ var ZoteroPane = new function()
return collection.saveTx();
});
this.newFeed = Zotero.Promise.coroutine(function* () {
let data = {};
window.openDialog('chrome://zotero/content/feedSettings.xul',
null, 'centerscreen, modal', data);
if (!data.cancelled) {
let feed = new Zotero.Feed();
feed.url = data.url;
feed.name = data.title;
feed.refreshInterval = data.ttl;
feed.cleanupAfter = data.cleanupAfter;
yield feed.saveTx();
yield feed.updateFeed();
}
});
this.newGroup = function () {
this.loadURI(Zotero.Groups.addGroupURL);
@ -1348,6 +1379,14 @@ var ZoteroPane = new function()
yield ZoteroItemPane.viewItem(item, 'view', pane);
tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
}
if (item.isFeedItem) {
// Too slow for now
// if (!item.isTranslated) {
// item.translate();
// }
this.startItemReadTimeout(item.id);
}
}
}
// Zero or multiple items selected
@ -1718,7 +1757,7 @@ var ZoteroPane = new function()
return;
}
if (!this.canEdit()) {
if (!this.canEdit() && !collectionTreeRow.isFeed()) {
this.displayCannotEditLibraryMessage();
return;
}
@ -1729,48 +1768,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);
}
}
}
@ -1844,7 +1875,7 @@ var ZoteroPane = new function()
}
if (this.collectionsView.selection.count > 0) {
var row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex);
var row = this.collectionsView.selectedTreeRow;
if (row.isCollection()) {
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
@ -1886,6 +1917,48 @@ var ZoteroPane = new function()
}
}
});
this.toggleSelectedItemsRead = function() {
return Zotero.FeedItems.toggleReadByID(this.getSelectedItems(true));
};
this.markFeedRead = Zotero.Promise.coroutine(function* () {
if (!this.collectionsView.selection.count) return;
let feed = this.collectionsView.selectedTreeRow.ref;
let feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID, true, false, true);
yield Zotero.FeedItems.toggleReadByID(feedItemIDs, true);
});
this.editSelectedFeed = Zotero.Promise.coroutine(function* () {
if (!this.collectionsView.selection.count) return;
let feed = this.collectionsView.selectedTreeRow.ref;
let data = {
url: feed.url,
title: feed.name,
ttl: feed.refreshInterval,
cleanAfter: feed.cleanupAfter
};
window.openDialog('chrome://zotero/content/feedSettings.xul',
null, 'centerscreen, modal', data);
if (data.cancelled) return;
feed.name = data.title;
feed.refreshInterval = data.ttl;
feed.cleanupAfter = data.cleanAfter;
yield feed.saveTx();
});
this.refreshFeed = function() {
if (!this.collectionsView.selection.count) return;
let feed = this.collectionsView.selectedTreeRow.ref;
return feed.updateFeed();
}
this.copySelectedItemsToClipboard = function (asCitations) {
@ -2156,10 +2229,14 @@ var ZoteroPane = new function()
"newCollection",
"newSavedSearch",
"newSubcollection",
"newFeed",
"refreshFeed",
"sep1",
"showDuplicates",
"showUnfiled",
"editSelectedCollection",
"markReadFeed",
"editSelectedFeed",
"deleteCollection",
"deleteCollectionAndItems",
"sep2",
@ -2215,6 +2292,22 @@ var ZoteroPane = new function()
menu.childNodes[m.createBibCollection].setAttribute('label', Zotero.getString('pane.collections.menu.createBib.collection'));
menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection'));
}
else if (collectionTreeRow.isFeed()) {
show = [
m.refreshFeed,
m.sep1,
m.markReadFeed,
m.editSelectedFeed,
m.deleteCollectionAndItems
];
if (collectionTreeRow.ref.unreadCount == 0) {
disable.push(m.markReadFeed);
}
// Adjust labels
menu.childNodes[m.deleteCollectionAndItems].setAttribute('label', Zotero.getString('pane.collections.menu.delete.feedAndItems'));
}
else if (collectionTreeRow.isSearch()) {
show = [
m.editSelectedCollection,
@ -2312,6 +2405,7 @@ var ZoteroPane = new function()
'addNote',
'addAttachments',
'sep2',
'toggleRead',
'duplicateItem',
'deleteItem',
'restoreToLibrary',
@ -2357,11 +2451,13 @@ var ZoteroPane = new function()
else if (collectionTreeRow.isPublications()) {
show.push(m.deleteFromLibrary);
}
else {
else if (!collectionTreeRow.isFeed()) {
show.push(m.moveToTrash);
}
show.push(m.sep3, m.exportItems, m.createBib, m.loadReport);
if(!collectionTreeRow.isFeed()) {
show.push(m.sep3, m.exportItems, m.createBib, m.loadReport);
}
if (this.itemsView.selection.count > 0) {
// Multiple items selected
@ -2370,6 +2466,8 @@ var ZoteroPane = new function()
var items = this.getSelectedItems();
var canMerge = true, canIndex = true, canRecognize = true, canRename = true;
var canMarkRead = collectionTreeRow.isFeed();
var markUnread = true;
if (!Zotero.Fulltext.pdfConverterIsRegistered()) {
canIndex = false;
@ -2377,7 +2475,7 @@ var ZoteroPane = new function()
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (canMerge && !item.isRegularItem() || collectionTreeRow.isDuplicates()) {
if (canMerge && !item.isRegularItem() || item.isFeedItem || collectionTreeRow.isDuplicates()) {
canMerge = false;
}
@ -2393,6 +2491,10 @@ var ZoteroPane = new function()
if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
canRename = false;
}
if(canMarkRead && markUnread && !item.isRead) {
markUnread = false;
}
}
if (canMerge) {
@ -2407,10 +2509,19 @@ var ZoteroPane = new function()
show.push(m.recognizePDF);
}
if (canMarkRead) {
show.push(m.toggleRead);
if (markUnread) {
menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsUnread'));
} else {
menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsRead'));
}
}
var canCreateParent = true;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (!item.isTopLevelItem() || !item.isAttachment()) {
if (!item.isTopLevelItem() || !item.isAttachment() || item.isFeedItem) {
canCreateParent = false;
break;
}
@ -2458,7 +2569,7 @@ var ZoteroPane = new function()
show.push(m.showInLibrary, m.sep1);
}
if (item.isRegularItem()) {
if (item.isRegularItem() && !item.isFeedItem) {
show.push(m.addNote, m.addAttachments, m.sep2);
}
@ -2493,6 +2604,14 @@ var ZoteroPane = new function()
show.push(m.sep4);
}
}
else if (item.isFeedItem) {
show.push(m.toggleRead);
if (item.isRead) {
menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsUnread'));
} else {
menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsRead'));
}
}
else {
show.push(m.duplicateItem);
}
@ -2522,13 +2641,14 @@ var ZoteroPane = new function()
m.moveToTrash, m.deleteFromLibrary, m.exportItems, m.createBib, m.loadReport);
}
if (!collectionTreeRow.editable || collectionTreeRow.isPublications()) {
if ((!collectionTreeRow.editable || collectionTreeRow.isPublications()) && !collectionTreeRow.isFeed()) {
for (let i in m) {
// Still show export/bib/report for non-editable views
// Still allow export/bib/report/read for non-editable views
switch (i) {
case 'exportItems':
case 'createBib':
case 'loadReport':
case 'toggleRead':
continue;
}
if (isTrash) {
@ -4198,6 +4318,45 @@ var ZoteroPane = new function()
});
let itemReadTimeout;
this.startItemReadTimeout = function(feedItemID) {
if (itemReadTimeout) {
itemReadTimeout.cancel();
itemReadTimeout = null;
}
let feedItem;
itemReadTimeout = Zotero.FeedItems.getAsync(feedItemID)
.then(function(newFeedItem) {
if (!newFeedItem) {
throw new Zotero.Promise.CancellationError('Not a FeedItem');
} else if(newFeedItem.isRead) {
throw new Zotero.Promise.CancellationError('FeedItem already read.');
}
feedItem = newFeedItem;
})
.delay(3000)
.then(() => {
itemReadTimeout = null;
// Check to make sure we're still on the same item
if (this.itemsView.selection.count !== 1) return;
let row = this.itemsView.getRow(this.itemsView.selection.currentIndex);
if (!row || !row.ref || !row.ref.id == feedItemID) return;
return feedItem.toggleRead(true);
})
.catch(function(e) {
if (e instanceof Zotero.Promise.CancellationError) {
Zotero.debug(e.message);
return;
}
Zotero.debug(e, 1);
});
}
function reportErrors() {
var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher);

View file

@ -58,6 +58,7 @@
<command id="cmd_zotero_createTimeline" oncommand="Zotero_Timeline_Interface.loadTimeline();"/>
<command id="cmd_zotero_rtfScan" oncommand="window.openDialog('chrome://zotero/content/rtfScan.xul', 'rtfScan', 'chrome,centerscreen')"/>
<command id="cmd_zotero_newCollection" oncommand="ZoteroPane_Local.newCollection()"/>
<command id="cmd_zotero_newFeed" oncommand="ZoteroPane_Local.newFeed()"/>
<command id="cmd_zotero_newSavedSearch" oncommand="ZoteroPane_Local.newSearch()"/>
<command id="cmd_zotero_newStandaloneNote" oncommand="ZoteroPane_Local.newNote(event.shiftKey);"/>
<command id="cmd_zotero_newChildNote" oncommand="ZoteroPane_Local.newChildNote(event.shiftKey);"/>
@ -102,7 +103,12 @@
<toolbar id="zotero-toolbar" class="toolbar toolbar-primary">
<hbox id="zotero-collections-toolbar" align="center">
<toolbarbutton id="zotero-tb-collection-add" class="zotero-tb-button" tooltiptext="&zotero.toolbar.newCollection.label;" command="cmd_zotero_newCollection"/>
<toolbarbutton id="zotero-tb-group-add" class="zotero-tb-button" tooltiptext="&zotero.toolbar.newGroup;" oncommand="ZoteroPane_Local.newGroup()"/>
<toolbarbutton id="zotero-tb-library-add-menu" class="zotero-tb-button" tooltiptext="&zotero.toolbar.newLibrary.label;" type="menu">
<menupopup id="zotero-tb-library-add-popup">
<menuitem id="zotero-tb-group-add" label="&zotero.toolbar.newGroup;" oncommand="ZoteroPane_Local.newGroup()"/>
<menuitem id="zotero-tb-feed-add" label="&zotero.toolbar.newFeed.label;" command="cmd_zotero_newFeed"/>
</menupopup>
</toolbarbutton>
<spacer flex="1"/>
<toolbarbutton id="zotero-tb-actions-menu" class="zotero-tb-button" tooltiptext="&zotero.toolbar.actions.label;" type="menu">
<menupopup id="zotero-tb-actions-popup">
@ -239,10 +245,14 @@
<menuitem class="menuitem-iconic zotero-menuitem-new-collection" label="&zotero.toolbar.newCollection.label;" command="cmd_zotero_newCollection"/>
<menuitem class="menuitem-iconic zotero-menuitem-new-saved-search" label="&zotero.toolbar.newSavedSearch.label;" command="cmd_zotero_newSavedSearch"/>
<menuitem class="menuitem-iconic zotero-menuitem-new-collection" label="&zotero.toolbar.newSubcollection.label;" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection().key)"/>
<menuitem class="menuitem-iconic zotero-menuitem-new-feed" label="&zotero.toolbar.newFeed.label;" command="cmd_zotero_newFeed"/>
<menuitem class="menuitem-iconic zotero-menuitem-refresh-feed" label="&zotero.toolbar.refreshFeed.label;" oncommand="ZoteroPane_Local.refreshFeed();"/>
<menuseparator/>
<menuitem class="menuitem-iconic zotero-menuitem-show-duplicates" label="&zotero.toolbar.duplicate.label;" oncommand="ZoteroPane_Local.setVirtual(ZoteroPane_Local.getSelectedLibraryID(), 'duplicates', true)"/>
<menuitem class="menuitem-iconic zotero-menuitem-show-unfiled" label="&zotero.collections.showUnfiledItems;" oncommand="ZoteroPane_Local.setVirtual(ZoteroPane_Local.getSelectedLibraryID(), 'unfiled', true)"/>
<menuitem class="menuitem-iconic zotero-menuitem-edit-collection" oncommand="ZoteroPane_Local.editSelectedCollection();"/>
<menuitem class="menuitem-iconic zotero-menuitem-mark-read-feed" label="&zotero.toolbar.markFeedRead.label;" oncommand="ZoteroPane_Local.markFeedRead();"/>
<menuitem class="menuitem-iconic zotero-menuitem-edit-feed" label="&zotero.toolbar.editFeed.label;" oncommand="ZoteroPane_Local.editSelectedFeed();"/>
<menuitem class="menuitem-iconic zotero-menuitem-delete-collection" oncommand="ZoteroPane_Local.deleteSelectedCollection();"/>
<menuitem class="menuitem-iconic zotero-menuitem-move-to-trash" oncommand="ZoteroPane_Local.deleteSelectedCollection(true);"/>
<menuseparator/>
@ -270,6 +280,7 @@
</menupopup>
</menu>
<menuseparator/>
<menuitem class="menuitem-iconic zotero-menuitem-toggle-read-item" oncommand="ZoteroPane_Local.toggleSelectedItemsRead();"/>
<menuitem class="menuitem-iconic zotero-menuitem-duplicate-item" label="&zotero.items.menu.duplicateItem;" oncommand="ZoteroPane_Local.duplicateSelectedItem().done();"/>
<menuitem class="menuitem-iconic zotero-menuitem-delete-collection" oncommand="ZoteroPane_Local.deleteSelectedItems();"/>
<menuitem class="menuitem-iconic zotero-menuitem-restore-to-library" label="&zotero.items.menu.restoreToLibrary;" oncommand="ZoteroPane_Local.restoreSelectedItems();"/>