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:
commit
6c43e75d26
63 changed files with 3349 additions and 501 deletions
|
@ -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++) {
|
||||
|
|
|
@ -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({
|
||||
|
|
169
chrome/content/zotero/feedSettings.js
Normal file
169
chrome/content/zotero/feedSettings.js
Normal 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();
|
||||
};
|
||||
}
|
62
chrome/content/zotero/feedSettings.xul
Normal file
62
chrome/content/zotero/feedSettings.xul
Normal 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>
|
|
@ -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;
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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(" ");
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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({}),
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
521
chrome/content/zotero/xpcom/feedReader.js
Normal file
521
chrome/content/zotero/xpcom/feedReader.js
Normal 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;
|
||||
}
|
|
@ -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(" ");
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 <br>s with newlines
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();"/>
|
||||
|
|
|
@ -39,6 +39,13 @@
|
|||
<!ENTITY zotero.preferences.groups.childLinks "child links">
|
||||
<!ENTITY zotero.preferences.groups.tags "tags">
|
||||
|
||||
<!ENTITY zotero.preferences.feeds "Feeds">
|
||||
<!ENTITY zotero.preferences.feeds.sorting.label1 "Sorting:">
|
||||
<!ENTITY zotero.preferences.feeds.sorting.label2 "Item First">
|
||||
<!ENTITY zotero.preferences.feeds.sorting.newest "Newest">
|
||||
<!ENTITY zotero.preferences.feeds.sorting.oldest "Oldest">
|
||||
<!ENTITY zotero.preferences.feeds.feedDefaults "Feed Defaults">
|
||||
|
||||
<!ENTITY zotero.preferences.openurl.caption "OpenURL">
|
||||
|
||||
<!ENTITY zotero.preferences.openurl.search "Search for resolvers">
|
||||
|
@ -133,6 +140,7 @@
|
|||
<!ENTITY zotero.preferences.keys.quicksearch "Quick Search">
|
||||
<!ENTITY zotero.preferences.keys.newItem "Create a New Item">
|
||||
<!ENTITY zotero.preferences.keys.newNote "Create a New Note">
|
||||
<!ENTITY zotero.preferences.keys.toggleRead "Mark Item Read/Unread">
|
||||
<!ENTITY zotero.preferences.keys.toggleTagSelector "Toggle Tag Selector">
|
||||
<!ENTITY zotero.preferences.keys.copySelectedItemCitationsToClipboard "Copy Selected Item Citations to Clipboard">
|
||||
<!ENTITY zotero.preferences.keys.copySelectedItemsToClipboard "Copy Selected Items to Clipboard">
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<!ENTITY zotero.general.cancel "Cancel">
|
||||
<!ENTITY zotero.general.refresh "Refresh">
|
||||
<!ENTITY zotero.general.saveAs "Save As…">
|
||||
<!ENTITY zotero.general.advancedOptions.label "Advanced Options">
|
||||
|
||||
<!ENTITY zotero.errorReport.title "Zotero Error Report">
|
||||
<!ENTITY zotero.errorReport.unrelatedMessages "This may include messages unrelated to Zotero.">
|
||||
|
@ -102,7 +103,12 @@
|
|||
<!ENTITY zotero.toolbar.newItemFromPage.label "Create Web Page Item from Current Page">
|
||||
<!ENTITY zotero.toolbar.lookup.label "Add Item(s) by Identifier">
|
||||
<!ENTITY zotero.toolbar.removeItem.label "Remove Item…">
|
||||
<!ENTITY zotero.toolbar.newLibrary.label "New Library…">
|
||||
<!ENTITY zotero.toolbar.newCollection.label "New Collection…">
|
||||
<!ENTITY zotero.toolbar.newFeed.label "New Feed…">
|
||||
<!ENTITY zotero.toolbar.refreshFeed.label "Refresh Feed">
|
||||
<!ENTITY zotero.toolbar.editFeed.label "Edit Feed…">
|
||||
<!ENTITY zotero.toolbar.markFeedRead.label "Mark Feed As Read">
|
||||
<!ENTITY zotero.toolbar.newGroup "New Group…">
|
||||
<!ENTITY zotero.toolbar.newSubcollection.label "New Subcollection…">
|
||||
<!ENTITY zotero.toolbar.newSavedSearch.label "New Saved Search…">
|
||||
|
@ -254,6 +260,16 @@
|
|||
<!ENTITY zotero.proxy.recognized.disable.label "Do not automatically redirect requests through previously recognized proxies">
|
||||
<!ENTITY zotero.proxy.recognized.ignore.label "Ignore">
|
||||
|
||||
<!ENTITY zotero.feedSettings.title "Feed Settings">
|
||||
<!ENTITY zotero.feedSettings.saveButton.label "Save">
|
||||
<!ENTITY zotero.feedSettings.url.label "URL:">
|
||||
<!ENTITY zotero.feedSettings.title.label "Title:">
|
||||
<!ENTITY zotero.feedSettings.refresh.label1 "Refresh Interval:">
|
||||
<!ENTITY zotero.feedSettings.refresh.label2 "hour(s)">
|
||||
<!ENTITY zotero.feedSettings.cleanupAfter.label1 "Remove read articles after ">
|
||||
<!ENTITY zotero.feedSettings.cleanupAfter.label2 "day(s)">
|
||||
|
||||
|
||||
<!ENTITY zotero.recognizePDF.recognizing.label "Retrieving Metadata…">
|
||||
<!ENTITY zotero.recognizePDF.cancel.label "Cancel">
|
||||
<!ENTITY zotero.recognizePDF.pdfName.label "PDF Name">
|
||||
|
|
|
@ -161,6 +161,8 @@ pane.collections.delete = Are you sure you want to delete the selected collect
|
|||
pane.collections.delete.keepItems = Items within this collection will not be deleted.
|
||||
pane.collections.deleteWithItems.title = Delete Collection and Items
|
||||
pane.collections.deleteWithItems = Are you sure you want to delete the selected collection and move all items within it to the Trash?
|
||||
pane.feed.deleteWithItems.title = Unsubscribe
|
||||
pane.feed.deleteWithItems = Are you sure you want to unsubscribe from this feed?
|
||||
|
||||
pane.collections.deleteSearch.title = Delete Search
|
||||
pane.collections.deleteSearch = Are you sure you want to delete the selected search?
|
||||
|
@ -174,23 +176,31 @@ pane.collections.library = My Library
|
|||
pane.collections.publications = My Publications
|
||||
pane.collections.feeds = Feeds
|
||||
pane.collections.groupLibraries = Group Libraries
|
||||
pane.collections.feedLibraries = Feeds
|
||||
pane.collections.trash = Trash
|
||||
pane.collections.untitled = Untitled
|
||||
pane.collections.unfiled = Unfiled Items
|
||||
pane.collections.duplicate = Duplicate Items
|
||||
|
||||
pane.collections.menu.rename.collection = Rename Collection…
|
||||
pane.collections.menu.edit.savedSearch = Edit Saved Search
|
||||
pane.collections.menu.edit.savedSearch = Edit Saved Search…
|
||||
pane.collections.menu.edit.feed = Edit Feed…
|
||||
pane.collections.menu.delete.collection = Delete Collection…
|
||||
pane.collections.menu.delete.collectionAndItems = Delete Collection and Items…
|
||||
pane.collections.menu.delete.savedSearch = Delete Saved Search…
|
||||
pane.collections.menu.delete.feedAndItems = Unsubscribe From Feed…
|
||||
pane.collections.menu.export.collection = Export Collection…
|
||||
pane.collections.menu.export.savedSearch = Export Saved Search…
|
||||
pane.collections.menu.export.feed = Export Feed…
|
||||
pane.collections.menu.createBib.collection = Create Bibliography From Collection…
|
||||
pane.collections.menu.createBib.savedSearch = Create Bibliography From Saved Search…
|
||||
pane.collections.menu.createBib.feed = Create Bibliography From Feed…
|
||||
|
||||
pane.collections.menu.generateReport.collection = Generate Report from Collection…
|
||||
pane.collections.menu.generateReport.savedSearch = Generate Report from Saved Search…
|
||||
pane.collections.menu.generateReport.feed = Generate Report from Feed…
|
||||
|
||||
pane.collections.menu.refresh.feed = Refresh Feed
|
||||
|
||||
pane.tagSelector.rename.title = Rename Tag
|
||||
pane.tagSelector.rename.message = Please enter a new name for this tag.\n\nThe tag will be changed in all associated items.
|
||||
|
@ -261,6 +271,8 @@ pane.item.duplicates.writeAccessRequired = Library write access is required to m
|
|||
pane.item.duplicates.onlyTopLevel = Only top-level full items can be merged.
|
||||
pane.item.duplicates.onlySameItemType = Merged items must all be of the same item type.
|
||||
|
||||
pane.item.markAsRead = Mark As Read
|
||||
pane.item.markAsUnread = Mark As Unread
|
||||
pane.item.changeType.title = Change Item Type
|
||||
pane.item.changeType.text = Are you sure you want to change the item type?\n\nThe following fields will be lost:
|
||||
pane.item.defaultFirstName = first
|
||||
|
@ -494,6 +506,7 @@ save.link.error = An error occurred while saving this link.
|
|||
save.error.cannotMakeChangesToCollection = You cannot make changes to the currently selected collection.
|
||||
save.error.cannotAddFilesToCollection = You cannot add files to the currently selected collection.
|
||||
save.error.cannotAddToMyPublications = You cannot save items directly to My Publications. To add items to My Publications, drag them from another library.
|
||||
save.error.cannotAddToFeed = You cannot save items to feeds.
|
||||
|
||||
ingester.saveToZotero = Save to Zotero
|
||||
ingester.saveToZoteroUsing = Save to Zotero using "%S"
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
padding: 1.5em .25em .25em;
|
||||
}
|
||||
|
||||
#zotero-view-item.no-tabs
|
||||
{
|
||||
padding: .25em .25em .25em;
|
||||
}
|
||||
|
||||
#zotero-view-item > tabpanel > *
|
||||
{
|
||||
overflow: auto;
|
||||
|
|
|
@ -215,6 +215,12 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
/* Style unread items/collections in bold */
|
||||
#zotero-items-tree treechildren::-moz-tree-cell-text(unread),
|
||||
#zotero-collections-tree treechildren::-moz-tree-cell-text(unread) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#zotero-items-pane
|
||||
{
|
||||
min-width: 290px;
|
||||
|
@ -278,7 +284,12 @@
|
|||
list-style-image: url('chrome://zotero/skin/toolbar-collection-add.png');
|
||||
}
|
||||
|
||||
#zotero-tb-group-add
|
||||
.zotero-menuitem-new-feed
|
||||
{
|
||||
list-style-image: url('chrome://zotero/skin/toolbar-feed-add.png');
|
||||
}
|
||||
|
||||
#zotero-tb-library-add-menu
|
||||
{
|
||||
list-style-image: url('chrome://zotero/skin/group_add.png');
|
||||
}
|
||||
|
@ -368,6 +379,16 @@
|
|||
list-style-image: url('chrome://zotero/skin/toolbar-collection-edit.png');
|
||||
}
|
||||
|
||||
.zotero-menuitem-edit-feed
|
||||
{
|
||||
list-style-image: url('chrome://zotero/skin/toolbar-feed-edit.png');
|
||||
}
|
||||
|
||||
.zotero-menuitem-refresh-feed
|
||||
{
|
||||
list-style-image: url('chrome://zotero/skin/arrow_refresh.png');
|
||||
}
|
||||
|
||||
.zotero-menuitem-delete-collection
|
||||
{
|
||||
list-style-image: url('chrome://zotero/skin/toolbar-collection-delete.png');
|
||||
|
|
BIN
chrome/skin/default/zotero/toolbar-feed-add.png
Normal file
BIN
chrome/skin/default/zotero/toolbar-feed-add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 763 B |
BIN
chrome/skin/default/zotero/toolbar-feed-edit.png
Normal file
BIN
chrome/skin/default/zotero/toolbar-feed-edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 801 B |
BIN
chrome/skin/default/zotero/treesource-feed-error.png
Normal file
BIN
chrome/skin/default/zotero/treesource-feed-error.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
chrome/skin/default/zotero/treesource-feed-updating.png
Normal file
BIN
chrome/skin/default/zotero/treesource-feed-updating.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
chrome/skin/default/zotero/treesource-feed.png
Normal file
BIN
chrome/skin/default/zotero/treesource-feed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 691 B |
BIN
chrome/skin/default/zotero/treesource-feedLibrary.png
Normal file
BIN
chrome/skin/default/zotero/treesource-feedLibrary.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
|
@ -441,6 +441,18 @@ label.zotero-text-link {
|
|||
max-width: 29.5em;
|
||||
}
|
||||
|
||||
.zotero-advanced-options>.zotero-advanced-options-label>dropmarker {
|
||||
list-style-image: url("chrome://browser/skin/toolbarbutton-dropdown-arrow.png");
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.zotero-advanced-options[state=open]>.zotero-advanced-options-label>dropmarker {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#zotero-feed-settings grid hbox:first-child {
|
||||
-moz-box-pack: end;
|
||||
}
|
||||
|
||||
/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */
|
||||
@media (min-resolution: 1.5dppx) {
|
||||
|
|
|
@ -85,6 +85,7 @@ const xpcomFilesLocal = [
|
|||
'data/tags',
|
||||
'db',
|
||||
'duplicates',
|
||||
'feedReader',
|
||||
'fulltext',
|
||||
'id',
|
||||
'integration',
|
||||
|
|
|
@ -52,6 +52,10 @@ pref("extensions.zotero.groups.copyChildFileAttachments", true);
|
|||
pref("extensions.zotero.groups.copyChildNotes", true);
|
||||
pref("extensions.zotero.groups.copyTags", true);
|
||||
|
||||
pref("extensions.zotero.feeds.sortAscending", false);
|
||||
pref("extensions.zotero.feeds.defaultTTL", 1);
|
||||
pref("extensions.zotero.feeds.defaultCleanupAfter", 2);
|
||||
|
||||
pref("extensions.zotero.backup.numBackups", 2);
|
||||
pref("extensions.zotero.backup.interval", 1440);
|
||||
|
||||
|
@ -80,6 +84,7 @@ pref("extensions.zotero.keys.copySelectedItemCitationsToClipboard", 'A');
|
|||
pref("extensions.zotero.keys.copySelectedItemsToClipboard", 'C');
|
||||
pref("extensions.zotero.keys.toggleTagSelector", 'T');
|
||||
pref("extensions.zotero.keys.sync", 'Y');
|
||||
pref("extensions.zotero.keys.toggleRead", '`');
|
||||
|
||||
// Fulltext indexing
|
||||
pref("extensions.zotero.fulltext.textMaxLength", 500000);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-- 82
|
||||
-- 83
|
||||
|
||||
-- Copyright (c) 2009 Center for History and New Media
|
||||
-- George Mason University, Fairfax, Virginia, USA
|
||||
|
@ -212,6 +212,7 @@ CREATE TABLE feedItems (
|
|||
itemID INTEGER PRIMARY KEY,
|
||||
guid TEXT NOT NULL UNIQUE,
|
||||
readTime TIMESTAMP,
|
||||
translatedTime TIMESTAMP,
|
||||
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ var ZoteroUnit = Components.classes["@mozilla.org/commandlinehandler/general-sta
|
|||
|
||||
var dump = ZoteroUnit.dump;
|
||||
|
||||
// Mocha HTML reporter doesn't show deepEqual diffs, so we change this.
|
||||
chai.config.truncateThreshold = 0
|
||||
|
||||
function quit(failed) {
|
||||
// Quit with exit status
|
||||
if(!failed) {
|
||||
|
|
|
@ -309,6 +309,24 @@ var createGroup = Zotero.Promise.coroutine(function* (props = {}) {
|
|||
return group;
|
||||
});
|
||||
|
||||
var createFeed = Zotero.Promise.coroutine(function* (props = {}) {
|
||||
var feed = new Zotero.Feed;
|
||||
feed.name = props.name || "Test " + Zotero.Utilities.randomString();
|
||||
feed.description = props.description || "";
|
||||
feed.url = props.url || 'http://www.' + Zotero.Utilities.randomString() + '.com/feed.rss';
|
||||
feed.refreshInterval = props.refreshInterval || 12;
|
||||
feed.cleanupAfter = props.cleanupAfter || 2;
|
||||
yield feed.saveTx();
|
||||
return feed;
|
||||
});
|
||||
|
||||
var clearFeeds = Zotero.Promise.coroutine(function* () {
|
||||
let feeds = Zotero.Feeds.getAll();
|
||||
for (let i=0; i<feeds.length; i++) {
|
||||
yield feeds[i].eraseTx();
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Data objects
|
||||
//
|
||||
|
@ -371,11 +389,7 @@ function createUnsavedDataObject(objectType, params = {}) {
|
|||
|
||||
var createDataObject = Zotero.Promise.coroutine(function* (objectType, params = {}, saveOptions) {
|
||||
var obj = createUnsavedDataObject(objectType, params);
|
||||
if (objectType == 'feedItem') {
|
||||
yield obj.forceSaveTx(saveOptions);
|
||||
} else {
|
||||
yield obj.saveTx(saveOptions);
|
||||
}
|
||||
yield obj.saveTx(saveOptions);
|
||||
return obj;
|
||||
});
|
||||
|
||||
|
@ -439,6 +453,14 @@ function getTestDataDirectory() {
|
|||
QueryInterface(Components.interfaces.nsIFileURL).file;
|
||||
}
|
||||
|
||||
function getTestDataUrl(path) {
|
||||
path = path.split('/');
|
||||
if (path[0].length == 0) {
|
||||
path.splice(0, 1);
|
||||
}
|
||||
return "resource://zotero-unit-tests/data/" + path.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an absolute path to an empty temporary directory
|
||||
* (i.e., test/tests/data)
|
||||
|
|
|
@ -292,6 +292,13 @@ describe("Zotero.CollectionTreeView", function() {
|
|||
spy.restore();
|
||||
}
|
||||
})
|
||||
|
||||
it("should select a new feed", function* () {
|
||||
var feed = yield createFeed();
|
||||
// Library should still be selected
|
||||
assert.equal(cv.getSelectedLibraryID(), feed.id);
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("#drop()", function () {
|
||||
|
@ -602,72 +609,106 @@ describe("Zotero.CollectionTreeView", function() {
|
|||
|
||||
// TODO: Check deeper subcollection open states
|
||||
})
|
||||
})
|
||||
|
||||
it("should move a subcollection and its subcollection up under another collection", function* () {
|
||||
var collectionA = yield createDataObject('collection', { name: "A" }, { skipSelect: true });
|
||||
var collectionB = yield createDataObject('collection', { name: "B", parentKey: collectionA.key });
|
||||
var collectionC = yield createDataObject('collection', { name: "C", parentKey: collectionB.key });
|
||||
var collectionD = yield createDataObject('collection', { name: "D" }, { skipSelect: true });
|
||||
var collectionE = yield createDataObject('collection', { name: "E" }, { skipSelect: true });
|
||||
var collectionF = yield createDataObject('collection', { name: "F" }, { skipSelect: true });
|
||||
var collectionG = yield createDataObject('collection', { name: "G", parentKey: collectionE.key });
|
||||
var collectionH = yield createDataObject('collection', { name: "H", parentKey: collectionG.key });
|
||||
|
||||
var colIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
||||
var colIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
||||
var colIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
||||
var colIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
||||
var colIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
||||
var colIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
||||
var colIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
||||
var colIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
||||
|
||||
yield cv.selectCollection(collectionG.id);
|
||||
|
||||
// Add observer to wait for collection add
|
||||
var deferred = Zotero.Promise.defer();
|
||||
var observerID = Zotero.Notifier.registerObserver({
|
||||
notify: function (event, type, ids, extraData) {
|
||||
if (type == 'collection' && event == 'modify' && ids[0] == collectionG.id) {
|
||||
setTimeout(function () {
|
||||
deferred.resolve();
|
||||
}, 50);
|
||||
|
||||
it("should move a subcollection and its subcollection up under another collection", function* () {
|
||||
var collectionA = yield createDataObject('collection', { name: "A" }, { skipSelect: true });
|
||||
var collectionB = yield createDataObject('collection', { name: "B", parentKey: collectionA.key });
|
||||
var collectionC = yield createDataObject('collection', { name: "C", parentKey: collectionB.key });
|
||||
var collectionD = yield createDataObject('collection', { name: "D" }, { skipSelect: true });
|
||||
var collectionE = yield createDataObject('collection', { name: "E" }, { skipSelect: true });
|
||||
var collectionF = yield createDataObject('collection', { name: "F" }, { skipSelect: true });
|
||||
var collectionG = yield createDataObject('collection', { name: "G", parentKey: collectionE.key });
|
||||
var collectionH = yield createDataObject('collection', { name: "H", parentKey: collectionG.key });
|
||||
|
||||
var colIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
||||
var colIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
||||
var colIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
||||
var colIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
||||
var colIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
||||
var colIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
||||
var colIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
||||
var colIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
||||
|
||||
yield cv.selectCollection(collectionG.id);
|
||||
|
||||
// Add observer to wait for collection add
|
||||
var deferred = Zotero.Promise.defer();
|
||||
var observerID = Zotero.Notifier.registerObserver({
|
||||
notify: function (event, type, ids, extraData) {
|
||||
if (type == 'collection' && event == 'modify' && ids[0] == collectionG.id) {
|
||||
setTimeout(function () {
|
||||
deferred.resolve();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'collection', 'test');
|
||||
|
||||
yield Zotero.Promise.delay(2000);
|
||||
|
||||
yield drop(
|
||||
'collection',
|
||||
{
|
||||
row: colIndexD,
|
||||
orient: 0
|
||||
},
|
||||
[collectionG.id],
|
||||
deferred.promise
|
||||
);
|
||||
|
||||
Zotero.Notifier.unregisterObserver(observerID);
|
||||
|
||||
var newColIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
||||
var newColIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
||||
var newColIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
||||
var newColIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
||||
var newColIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
||||
var newColIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
||||
var newColIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
||||
var newColIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
||||
|
||||
assert.isFalse(cv.isContainerOpen(newColIndexE));
|
||||
assert.isTrue(cv.isContainerEmpty(newColIndexE));
|
||||
assert.isTrue(cv.isContainerOpen(newColIndexD));
|
||||
assert.isFalse(cv.isContainerEmpty(newColIndexD));
|
||||
assert.equal(newColIndexD, newColIndexG - 1);
|
||||
assert.equal(newColIndexG, newColIndexH - 1);
|
||||
|
||||
// TODO: Check deeper subcollection open states
|
||||
}, 'collection', 'test');
|
||||
|
||||
yield Zotero.Promise.delay(2000);
|
||||
|
||||
yield drop(
|
||||
'collection',
|
||||
{
|
||||
row: colIndexD,
|
||||
orient: 0
|
||||
},
|
||||
[collectionG.id],
|
||||
deferred.promise
|
||||
);
|
||||
|
||||
Zotero.Notifier.unregisterObserver(observerID);
|
||||
|
||||
var newColIndexA = cv.getRowIndexByID('C' + collectionA.id);
|
||||
var newColIndexB = cv.getRowIndexByID('C' + collectionB.id);
|
||||
var newColIndexC = cv.getRowIndexByID('C' + collectionC.id);
|
||||
var newColIndexD = cv.getRowIndexByID('C' + collectionD.id);
|
||||
var newColIndexE = cv.getRowIndexByID('C' + collectionE.id);
|
||||
var newColIndexF = cv.getRowIndexByID('C' + collectionF.id);
|
||||
var newColIndexG = cv.getRowIndexByID('C' + collectionG.id);
|
||||
var newColIndexH = cv.getRowIndexByID('C' + collectionH.id);
|
||||
|
||||
assert.isFalse(cv.isContainerOpen(newColIndexE));
|
||||
assert.isTrue(cv.isContainerEmpty(newColIndexE));
|
||||
assert.isTrue(cv.isContainerOpen(newColIndexD));
|
||||
assert.isFalse(cv.isContainerEmpty(newColIndexD));
|
||||
assert.equal(newColIndexD, newColIndexG - 1);
|
||||
assert.equal(newColIndexG, newColIndexH - 1);
|
||||
|
||||
// TODO: Check deeper subcollection open states
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("with feed items", function () {
|
||||
it('should add a translated feed item recovered from an URL', function* (){
|
||||
var feed = yield createFeed();
|
||||
var collection = yield createDataObject('collection', false, { skipSelect: true });
|
||||
var url = getTestDataUrl('metadata/journalArticle-single.html');
|
||||
var feedItem = yield createDataObject('feedItem', {libraryID: feed.libraryID}, { skipSelect: true });
|
||||
feedItem.setField('url', url);
|
||||
yield feedItem.saveTx();
|
||||
var translateFn = sinon.spy(feedItem, 'translate');
|
||||
|
||||
// Add observer to wait for collection add
|
||||
var deferred = Zotero.Promise.defer();
|
||||
var itemIds;
|
||||
|
||||
var ids = (yield drop('item', 'C' + collection.id, [feedItem.id])).ids;
|
||||
|
||||
// Check that the translated item was the one that was created after drag
|
||||
var item;
|
||||
yield translateFn.returnValues[0].then(function(i) {
|
||||
item = i;
|
||||
assert.equal(item.id, ids[0]);
|
||||
});
|
||||
|
||||
yield cv.selectCollection(collection.id);
|
||||
yield waitForItemsLoad(win);
|
||||
|
||||
var itemsView = win.ZoteroPane.itemsView;
|
||||
assert.equal(itemsView.rowCount, 1);
|
||||
var treeRow = itemsView.getRow(0);
|
||||
assert.equal(treeRow.ref.id, item.id);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
38
test/tests/data/feed.rss
Normal file
38
test/tests/data/feed.rss
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- Lifted from http://cyber.law.harvard.edu/rss/examples/rss2sample.xml -->
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Liftoff News</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/</link>
|
||||
<description>Liftoff to Space Exploration.</description>
|
||||
<language>en-us</language>
|
||||
<pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
|
||||
<lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>
|
||||
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
|
||||
<generator>Weblog Editor 2.0</generator>
|
||||
<managingEditor>editor@example.com</managingEditor>
|
||||
<webMaster>webmaster@example.com</webMaster>
|
||||
<item>
|
||||
<title>Star City</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
|
||||
<description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description>
|
||||
<pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
|
||||
<enclosure url="http://www.example.com/example.pdf" type="application/pdf" />
|
||||
</item>
|
||||
<item>
|
||||
<title>The Engine That Does More</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
|
||||
<description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description>
|
||||
<pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
|
||||
</item>
|
||||
<item>
|
||||
<title>Astronauts' Dirty Laundry</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
|
||||
<description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description>
|
||||
<pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
89
test/tests/data/feedDetailed.rss
Normal file
89
test/tests/data/feedDetailed.rss
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns="http://purl.org/rss/1.0/"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:prism="http://prismstandard.org/namespaces/1.2/basic/"
|
||||
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||
xsi:schemaLocation="http://www.w3.org/1999/02/22-rdf-syntax-ns# uri:atypon.com:cms:schema:rdf.xsd">
|
||||
<channel rdf:about="http://www.example.com/feed.rss">
|
||||
<title>Feed</title>
|
||||
<description>Feed Description</description>
|
||||
<link>http://www.example.com/feed.rss</link>
|
||||
<dc:publisher>Publisher</dc:publisher>
|
||||
<dc:language>en</dc:language>
|
||||
<dc:rights>©2016 Published by Publisher</dc:rights>
|
||||
<dc:creator>Feed Author</dc:creator>
|
||||
<prism:publicationName>Publication</prism:publicationName>
|
||||
<prism:issn>0000-0000</prism:issn>
|
||||
<prism:publicationDate>2016-01-07-08:00</prism:publicationDate>
|
||||
<prism:copyright>©2016 Published by Publisher</prism:copyright>
|
||||
<prism:rightsAgent>rights@example.com</prism:rightsAgent>
|
||||
<ttl>60</ttl>
|
||||
<items>
|
||||
<rdf:Seq>
|
||||
<rdf:li rdf:resource="http://www.example.com/item1"/>
|
||||
<rdf:li rdf:resource="http://www.example.com/item2"/>
|
||||
<rdf:li rdf:resource="http://www.example.com/item3"/>
|
||||
</rdf:Seq>
|
||||
</items>
|
||||
</channel>
|
||||
<item rdf:about="http://www.example.com/item1">
|
||||
<title>Title 1</title>
|
||||
<link>http://www.example.com/item1</link>
|
||||
<description>Description 1</description>
|
||||
<language>en</language>
|
||||
<startPage>10</startPage>
|
||||
<endPage>20</endPage>
|
||||
<dc:title>Title 1</dc:title>
|
||||
<dc:creator>Author1 A.T. Rohtua, Author2 A. Auth, Author3 Autho</dc:creator>
|
||||
<dc:contributor>Contributor1 A.T. Rotubirtnoc, Contributor2 C. Contrib, Contributor3 Contr</dc:contributor>
|
||||
<dc:publisher>Publisher</dc:publisher>
|
||||
<dc:source>Feed</dc:source>
|
||||
<dc:date>2016-01-07</dc:date>
|
||||
<dc:doi>10.1000/182</dc:doi>
|
||||
<prism:issn>0000-0000</prism:issn>
|
||||
<prism:publicationName>Publication</prism:publicationName>
|
||||
<prism:publicationDate>2016-01-07</prism:publicationDate>
|
||||
<prism:section>Article</prism:section>
|
||||
</item>
|
||||
<item rdf:about="http://www.example.com/item2">
|
||||
<title>Title 2</title>
|
||||
<link>http://www.example.com/item2</link>
|
||||
<description>Description 2</description>
|
||||
<language>en</language>
|
||||
<startPage>10</startPage>
|
||||
<endPage>20</endPage>
|
||||
<dc:title>Title 2</dc:title>
|
||||
<dc:creator>Author1 A.T. Rohtua, Author2 A. Auth, Author3 Autho</dc:creator>
|
||||
<dc:contributor>Contributor1 A.T. Rotubirtnoc, Contributor2 C. Contrib, Contributor3 Contr</dc:contributor>
|
||||
<dc:publisher>Publisher</dc:publisher>
|
||||
<dc:source>Feed</dc:source>
|
||||
<dc:date>2016-01-07</dc:date>
|
||||
<dc:doi>10.1000/182</dc:doi>
|
||||
<prism:issn>0000-0000</prism:issn>
|
||||
<prism:publicationName>Publication</prism:publicationName>
|
||||
<prism:publicationDate>2016-01-07</prism:publicationDate>
|
||||
<prism:section>Article</prism:section>
|
||||
</item>
|
||||
<item rdf:about="http://www.example.com/item3">
|
||||
<title>Title 3</title>
|
||||
<link>http://www.example.com/item3</link>
|
||||
<description>Description 3</description>
|
||||
<language>en</language>
|
||||
<pubType>Some Publication</pubType>
|
||||
<startPage>10</startPage>
|
||||
<endPage>20</endPage>
|
||||
<dc:title>Title 3</dc:title>
|
||||
<dc:creator>Author1 A.T. Rohtua, Author2 A. Auth, Author3 Autho</dc:creator>
|
||||
<dc:contributor>Contributor1 A.T. Rotubirtnoc, Contributor2 C. Contrib, Contributor3 Contr</dc:contributor>
|
||||
<dc:publisher>Publisher</dc:publisher>
|
||||
<dc:source>Feed</dc:source>
|
||||
<dc:date>2016-01-07</dc:date>
|
||||
<dc:doi>10.1000/182</dc:doi>
|
||||
<prism:issn>0000-0000</prism:issn>
|
||||
<prism:publicationName>Publication</prism:publicationName>
|
||||
<prism:publicationDate>2016-01-07</prism:publicationDate>
|
||||
<prism:section>Article</prism:section>
|
||||
</item>
|
||||
</rdf:RDF>
|
42
test/tests/data/feedModified.rss
Normal file
42
test/tests/data/feedModified.rss
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0"?>
|
||||
<!-- Lifted from http://cyber.law.harvard.edu/rss/examples/rss2sample.xml -->
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Liftoff News</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/</link>
|
||||
<description>Liftoff to Space Exploration.</description>
|
||||
<language>en-us</language>
|
||||
<pubDate>Tue, 03 Jun 2037 09:39:21 GMT</pubDate>
|
||||
<lastBuildDate>Tue, 03 Jun 2037 09:39:21 GMT</lastBuildDate>
|
||||
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
|
||||
<generator>Weblog Editor 2.0</generator>
|
||||
<managingEditor>editor@example.com</managingEditor>
|
||||
<webMaster>webmaster@example.com</webMaster>
|
||||
<item>
|
||||
<description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st.</description>
|
||||
<pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>
|
||||
</item>
|
||||
<item>
|
||||
<title>Star City (Updated)</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
|
||||
<description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>.</description>
|
||||
<pubDate>Tue, 03 Jun 2037 09:39:21 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
|
||||
</item>
|
||||
<item>
|
||||
<title>The Engine That Does More</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
|
||||
<description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description>
|
||||
<pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
|
||||
</item>
|
||||
<item>
|
||||
<title>Astronauts' Dirty Laundry</title>
|
||||
<link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
|
||||
<description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description>
|
||||
<pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
|
||||
<guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
|
@ -1,12 +1,12 @@
|
|||
describe("Zotero.FeedItem", function () {
|
||||
let feed, libraryID;
|
||||
before(function* () {
|
||||
feed = new Zotero.Feed({ name: 'Test ' + Zotero.randomString(), url: 'http://' + Zotero.randomString() + '.com/' });
|
||||
feed = yield createFeed({ name: 'Test ' + Zotero.randomString(), url: 'http://' + Zotero.randomString() + '.com/' });
|
||||
yield feed.saveTx();
|
||||
libraryID = feed.libraryID;
|
||||
});
|
||||
after(function() {
|
||||
return feed.eraseTx();
|
||||
return clearFeeds();
|
||||
});
|
||||
|
||||
it("should be an instance of Zotero.Item", function() {
|
||||
|
@ -23,11 +23,11 @@ describe("Zotero.FeedItem", function () {
|
|||
it("should accept required fields as arguments", function* () {
|
||||
let guid = Zotero.randomString();
|
||||
let feedItem = new Zotero.FeedItem();
|
||||
yield assert.isRejected(feedItem.forceSaveTx());
|
||||
yield assert.isRejected(feedItem.saveTx());
|
||||
|
||||
feedItem = new Zotero.FeedItem('book', { guid });
|
||||
feedItem.libraryID = libraryID;
|
||||
yield assert.isFulfilled(feedItem.forceSaveTx());
|
||||
yield assert.isFulfilled(feedItem.saveTx());
|
||||
|
||||
assert.equal(feedItem.itemTypeID, Zotero.ItemTypes.getID('book'));
|
||||
assert.equal(feedItem.guid, guid);
|
||||
|
@ -83,49 +83,60 @@ describe("Zotero.FeedItem", function () {
|
|||
expectedTimestamp = Date.now();
|
||||
feedItem.isRead = true;
|
||||
yield Zotero.Promise.delay(2001);
|
||||
yield feedItem.forceSaveTx();
|
||||
yield feedItem.saveTx();
|
||||
|
||||
readTime = yield Zotero.DB.valueQueryAsync('SELECT readTime FROM feedItems WHERE itemID=?', feedItem.id);
|
||||
readTime = Zotero.Date.sqlToDate(readTime, true).getTime();
|
||||
assert.closeTo(readTime, expectedTimestamp, 2000, 'read timestamp is correct in the DB');
|
||||
});
|
||||
});
|
||||
describe("#fromJSON()", function() {
|
||||
it("should attempt to parse non ISO-8601 dates", function* () {
|
||||
var json = {
|
||||
itemType: "journalArticle",
|
||||
accessDate: "2015-06-07 20:56:00",
|
||||
dateAdded: "18-20 June 2015", // magically parsed by `new Date()`
|
||||
dateModified: "07/06/2015", // US
|
||||
};
|
||||
var item = new Zotero.FeedItem;
|
||||
item.fromJSON(json);
|
||||
assert.strictEqual(item.getField('accessDate'), '2015-06-07 20:56:00');
|
||||
assert.strictEqual(item.getField('dateAdded'), '2015-06-18 20:00:00');
|
||||
// sets a timezone specific hour when new Date parses from strings without hour specified.
|
||||
assert.strictEqual(item.getField('dateModified'), Zotero.Date.dateToSQL(new Date(2015, 6, 6), true));
|
||||
})
|
||||
});
|
||||
describe("#save()", function() {
|
||||
it("should require edit check override", function* () {
|
||||
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
|
||||
feedItem.libraryID = feed.libraryID;
|
||||
yield assert.isRejected(feedItem.saveTx(), /^Error: Cannot edit feedItem in read-only library/);
|
||||
});
|
||||
it("should require feed being set", function* () {
|
||||
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
|
||||
// Defaults to user library ID
|
||||
yield assert.isRejected(feedItem.forceSaveTx(), /^Error: Cannot add /);
|
||||
yield assert.isRejected(feedItem.saveTx(), /^Error: Cannot add /);
|
||||
});
|
||||
it("should require GUID being set", function* () {
|
||||
let feedItem = new Zotero.FeedItem('book');
|
||||
feedItem.libraryID = feed.libraryID;
|
||||
yield assert.isRejected(feedItem.forceSaveTx(), /^Error: GUID must be set before saving FeedItem$/);
|
||||
yield assert.isRejected(feedItem.saveTx(), /^Error: GUID must be set before saving FeedItem$/);
|
||||
});
|
||||
it("should require a unique GUID", function* () {
|
||||
let guid = Zotero.randomString();
|
||||
let feedItem1 = yield createDataObject('feedItem', { libraryID, guid });
|
||||
|
||||
let feedItem2 = createUnsavedDataObject('feedItem', { libraryID, guid });
|
||||
yield assert.isRejected(feedItem2.forceSaveTx());
|
||||
yield assert.isRejected(feedItem2.saveTx());
|
||||
|
||||
// But we should be able to save it after deleting the original feed
|
||||
yield feedItem1.forceEraseTx();
|
||||
yield assert.isFulfilled(feedItem2.forceSaveTx());
|
||||
yield feedItem1.eraseTx();
|
||||
yield assert.isFulfilled(feedItem2.saveTx());
|
||||
});
|
||||
it("should require item type being set", function* () {
|
||||
let feedItem = new Zotero.FeedItem(null, { guid: Zotero.randomString() });
|
||||
feedItem.libraryID = feed.libraryID;
|
||||
yield assert.isRejected(feedItem.forceSaveTx(), /^Error: Item type must be set before saving$/);
|
||||
yield assert.isRejected(feedItem.saveTx(), /^Error: Item type must be set before saving$/);
|
||||
});
|
||||
it("should save feed item", function* () {
|
||||
let guid = Zotero.randomString();
|
||||
let feedItem = createUnsavedDataObject('feedItem', { libraryID, guid });
|
||||
yield assert.isFulfilled(feedItem.forceSaveTx());
|
||||
yield assert.isFulfilled(feedItem.saveTx());
|
||||
|
||||
feedItem = yield Zotero.FeedItems.getAsync(feedItem.id);
|
||||
assert.ok(feedItem);
|
||||
|
@ -139,7 +150,7 @@ describe("Zotero.FeedItem", function () {
|
|||
let feedItem = new Zotero.FeedItem(null, type, feed.libraryID);
|
||||
feedItem.fromJSON(allTypesAndFields[type]);
|
||||
|
||||
yield feedItem.forceSaveTx();
|
||||
yield feedItem.saveTx();
|
||||
|
||||
feedItems.push(feedItem);
|
||||
}
|
||||
|
@ -156,7 +167,7 @@ describe("Zotero.FeedItem", function () {
|
|||
let feedItem = yield createDataObject('feedItem', { libraryID });
|
||||
|
||||
feedItem.setField('title', 'bar');
|
||||
yield assert.isFulfilled(feedItem.forceSaveTx());
|
||||
yield assert.isFulfilled(feedItem.saveTx());
|
||||
assert.equal(feedItem.getField('title'), 'bar');
|
||||
});
|
||||
});
|
||||
|
@ -164,15 +175,90 @@ describe("Zotero.FeedItem", function () {
|
|||
it("should erase an existing feed item", function* () {
|
||||
let feedItem = yield createDataObject('feedItem', { libraryID });
|
||||
|
||||
yield feedItem.forceEraseTx();
|
||||
yield feedItem.eraseTx();
|
||||
assert.isFalse(yield Zotero.FeedItems.getAsync(feedItem.id));
|
||||
|
||||
//yield assert.isRejected(feedItem.forceEraseTx(), "does not allow erasing twice");
|
||||
//yield assert.isRejected(feedItem.EraseTx(), "does not allow erasing twice");
|
||||
});
|
||||
it("should require edit check override to erase", function* () {
|
||||
let feedItem = yield createDataObject('feedItem', { libraryID });
|
||||
it("should remove synced setting if exists", function* () {
|
||||
let item = yield createDataObject('feedItem', { libraryID });
|
||||
|
||||
yield assert.isRejected(feedItem.eraseTx(), /^Error: Cannot edit feedItem in read-only library/);
|
||||
yield item.toggleRead();
|
||||
let syncedSettings = feed.getSyncedSettings();
|
||||
assert.ok(syncedSettings.markedAsRead[item.guid]);
|
||||
|
||||
yield item.eraseTx();
|
||||
|
||||
syncedSettings = feed.getSyncedSettings();
|
||||
assert.notOk(syncedSettings.markedAsRead[item.guid]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#toggleRead()", function() {
|
||||
it('should toggle state', function* () {
|
||||
let item = yield createDataObject('feedItem', { libraryID });
|
||||
item.isRead = false;
|
||||
yield item.saveTx();
|
||||
|
||||
yield item.toggleRead();
|
||||
assert.isTrue(item.isRead, "item is toggled to read state");
|
||||
});
|
||||
it('should save if specified state is different from current', function* (){
|
||||
let item = yield createDataObject('feedItem', { libraryID });
|
||||
item.isRead = false;
|
||||
yield item.saveTx();
|
||||
sinon.spy(item, 'save');
|
||||
|
||||
yield item.toggleRead(true);
|
||||
assert.isTrue(item.save.called, "item was saved on toggle read");
|
||||
|
||||
item.save.reset();
|
||||
|
||||
yield item.toggleRead(true);
|
||||
assert.isFalse(item.save.called, "item was not saved on toggle read to same state");
|
||||
});
|
||||
it('should set relevant synced settings', function* () {
|
||||
let item = yield createDataObject('feedItem', { libraryID });
|
||||
item.isRead = false;
|
||||
yield item.saveTx();
|
||||
|
||||
yield item.toggleRead();
|
||||
|
||||
let feed = Zotero.Feeds.get(item.libraryID);
|
||||
let syncedSettings = feed.getSyncedSettings();
|
||||
assert.ok(syncedSettings.markedAsRead[item.guid], "item marked as read stored in synced settings");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#translate()', function() {
|
||||
before(function* () {
|
||||
// Needs an open window to be able to create a hidden window for translation
|
||||
yield loadBrowserWindow();
|
||||
});
|
||||
it('translates and saves items', function* () {
|
||||
var feedItem = yield createDataObject('feedItem', {libraryID});
|
||||
var url = getTestDataUrl('metadata/journalArticle-single.html');
|
||||
feedItem.setField('url', url);
|
||||
yield feedItem.saveTx();
|
||||
|
||||
yield feedItem.translate();
|
||||
|
||||
assert.equal(feedItem.getField('title'), 'Scarcity or Abundance? Preserving the Past in a Digital Era');
|
||||
});
|
||||
it('translates and saves items to corresponding library and collection', function* () {
|
||||
let group = yield createGroup();
|
||||
let collection = yield createDataObject('collection', {libraryID: group.libraryID});
|
||||
|
||||
var feedItem = yield createDataObject('feedItem', {libraryID});
|
||||
var url = getTestDataUrl('metadata/journalArticle-single.html');
|
||||
feedItem.setField('url', url);
|
||||
yield feedItem.saveTx();
|
||||
|
||||
yield feedItem.translate(group.libraryID, collection.id);
|
||||
|
||||
let item = collection.getChildItems(false, false)[0];
|
||||
|
||||
assert.equal(item.getField('title'), 'Scarcity or Abundance? Preserving the Past in a Digital Era');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,55 @@
|
|||
describe("Zotero.FeedItems", function () {
|
||||
let feed;
|
||||
before(function() {
|
||||
feed = new Zotero.Feed({ name: 'foo', url: 'http://' + Zotero.randomString() + '.com' });
|
||||
return feed.saveTx();
|
||||
before(function* () {
|
||||
feed = yield createFeed({ name: 'foo', url: 'http://' + Zotero.randomString() + '.com' });
|
||||
});
|
||||
after(function() {
|
||||
return feed.eraseTx();
|
||||
return clearFeeds();
|
||||
});
|
||||
|
||||
describe("#getMarkedAsRead", function() {
|
||||
var items = [];
|
||||
var result;
|
||||
before(function* () {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let f = yield createDataObject('feedItem', {libraryID: feed.libraryID, guid: 'http://www.example.com/' + i});
|
||||
items.push(f);
|
||||
}
|
||||
yield items[0].toggleRead();
|
||||
yield items[2].toggleRead();
|
||||
result = yield Zotero.FeedItems.getMarkedAsRead(feed.libraryID);
|
||||
});
|
||||
it('should get all marked as read items', function() {
|
||||
assert.include(result, items[0]);
|
||||
assert.include(result, items[2]);
|
||||
});
|
||||
it('should not include items that were not marked', function() {
|
||||
assert.notInclude(result, items[1]);
|
||||
assert.notInclude(result, items[3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#markAsReadByGUID", function() {
|
||||
var items = [];
|
||||
var result;
|
||||
before(function* () {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let f = yield createDataObject('feedItem', {
|
||||
libraryID: feed.libraryID,
|
||||
guid: 'http://' + Zotero.Utilities.randomString() + '.com/feed.rss'
|
||||
});
|
||||
items.push(f);
|
||||
}
|
||||
yield Zotero.FeedItems.markAsReadByGUID([items[0].guid, items[2].guid]);
|
||||
});
|
||||
it('should mark as read only specified guids', function() {
|
||||
assert.isTrue(items[0].isRead);
|
||||
assert.isTrue(items[2].isRead);
|
||||
});
|
||||
it('should leave other items marked unread', function() {
|
||||
assert.isFalse(items[1].isRead);
|
||||
assert.isFalse(items[3].isRead);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getIDFromGUID()", function() {
|
||||
|
@ -15,7 +59,7 @@ describe("Zotero.FeedItems", function () {
|
|||
});
|
||||
it("should return feed item id from GUID", function* () {
|
||||
let feedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
|
||||
yield feedItem.forceSaveTx();
|
||||
yield feedItem.saveTx();
|
||||
|
||||
let id2 = yield Zotero.FeedItems.getIDFromGUID(feedItem.guid);
|
||||
assert.equal(id2, feedItem.id);
|
||||
|
@ -25,7 +69,7 @@ describe("Zotero.FeedItems", function () {
|
|||
it("should return feed item from GUID", function* () {
|
||||
let guid = Zotero.randomString();
|
||||
let feedItem = yield createDataObject('feedItem', { guid, libraryID: feed.libraryID });
|
||||
yield feedItem.forceSaveTx();
|
||||
yield feedItem.saveTx();
|
||||
|
||||
let feedItem2 = yield Zotero.FeedItems.getAsyncByGUID(guid);
|
||||
assert.equal(feedItem2.id, feedItem.id);
|
||||
|
@ -35,4 +79,74 @@ describe("Zotero.FeedItems", function () {
|
|||
assert.isFalse(feedItem);
|
||||
});
|
||||
});
|
||||
describe("#toggleReadByID()", function() {
|
||||
var save, feed, items, ids;
|
||||
|
||||
before(function() {
|
||||
save = sinon.spy(Zotero.FeedItem.prototype, 'save');
|
||||
});
|
||||
|
||||
beforeEach(function* (){
|
||||
feed = yield createFeed();
|
||||
|
||||
items = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
let item = yield createDataObject('feedItem', { guid: Zotero.randomString(), libraryID: feed.id });
|
||||
item.isRead = true;
|
||||
yield item.saveTx();
|
||||
items.push(item);
|
||||
}
|
||||
ids = Array.map(items, (i) => i.id);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
save.restore();
|
||||
});
|
||||
|
||||
afterEach(function* () {
|
||||
save.reset();
|
||||
|
||||
yield clearFeeds();
|
||||
});
|
||||
|
||||
it('should toggle all items read if at least one unread', function* () {
|
||||
items[0].isRead = false;
|
||||
yield items[0].saveTx();
|
||||
|
||||
yield Zotero.FeedItems.toggleReadByID(ids);
|
||||
|
||||
for(let i = 0; i < 10; i++) {
|
||||
assert.isTrue(save.thisValues[i].isRead, "#toggleRead called with true");
|
||||
}
|
||||
});
|
||||
|
||||
it('should toggle all items unread if all read', function* () {
|
||||
yield Zotero.FeedItems.toggleReadByID(ids);
|
||||
|
||||
for(let i = 0; i < 10; i++) {
|
||||
assert.isFalse(save.thisValues[i].isRead, "#toggleRead called with false");
|
||||
}
|
||||
});
|
||||
|
||||
it('should toggle all items unread if unread state specified', function* () {
|
||||
items[0].isRead = false;
|
||||
yield items[0].saveTx();
|
||||
|
||||
yield Zotero.FeedItems.toggleReadByID(ids, false);
|
||||
|
||||
for(let i = 0; i < 10; i++) {
|
||||
assert.isFalse(save.thisValues[i].isRead, "#toggleRead called with true");
|
||||
}
|
||||
});
|
||||
|
||||
it('should set relevant sync settings', function* () {
|
||||
items[0].isRead = false;
|
||||
yield items[0].saveTx();
|
||||
yield Zotero.FeedItems.toggleReadByID(ids);
|
||||
|
||||
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
let markedAsRead = Object.keys(syncedFeeds[feed.url].markedAsRead);
|
||||
assert.deepEqual(markedAsRead, Object.keys(items).map((k) => items[k].guid));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
166
test/tests/feedReaderTest.js
Normal file
166
test/tests/feedReaderTest.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
"use strict";
|
||||
|
||||
describe("Zotero.FeedReader", function () {
|
||||
|
||||
var htmlUrl = getTestDataUrl("test.html");
|
||||
|
||||
var feedUrl = getTestDataUrl("feed.rss");
|
||||
var feedInfo = {
|
||||
title: 'Liftoff News',
|
||||
subtitle: 'Liftoff to Space Exploration.',
|
||||
updated: new Date("Tue, 10 Jun 2003 09:41:01 GMT"),
|
||||
creators: [{
|
||||
firstName: '',
|
||||
lastName: 'editor@example.com',
|
||||
creatorType: 'author',
|
||||
fieldMode: 1
|
||||
}],
|
||||
language: 'en-us'
|
||||
};
|
||||
|
||||
var detailedFeedUrl = getTestDataUrl("feedDetailed.rss");
|
||||
var detailedFeedInfo = {
|
||||
title: 'Feed',
|
||||
subtitle: 'Feed Description',
|
||||
creators: [{firstName: 'Feed', lastName: 'Author', creatorType: 'author'}],
|
||||
publicationTitle: 'Publication',
|
||||
publisher: 'Publisher',
|
||||
rights: '©2016 Published by Publisher',
|
||||
ISSN: '0000-0000',
|
||||
language: 'en'
|
||||
};
|
||||
|
||||
after(function* () {
|
||||
yield clearFeeds();
|
||||
});
|
||||
|
||||
describe('FeedReader()', function () {
|
||||
it('should throw if url not provided', function() {
|
||||
assert.throw(() => new Zotero.FeedReader())
|
||||
});
|
||||
|
||||
it('should throw if url invalid', function() {
|
||||
assert.throw(() => new Zotero.FeedReader('invalid url'))
|
||||
});
|
||||
});
|
||||
|
||||
describe('#process()', function() {
|
||||
it('should reject if the provided url is not a valid feed', function* () {
|
||||
let fr = new Zotero.FeedReader(htmlUrl);
|
||||
let e = yield getPromiseError(fr.process());
|
||||
assert.ok(e);
|
||||
e = yield getPromiseError(fr._feedItems[fr._feedItems.length-1].promise);
|
||||
assert.ok(e);
|
||||
});
|
||||
|
||||
it('should set #feedProperties on FeedReader object', function* () {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
assert.throw(() => fr.feedProperties);
|
||||
yield fr.process();
|
||||
assert.ok(fr.feedProperties);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#terminate()', function() {
|
||||
it('should reject last feed item and feed processing promise if feed not processed yet', function* () {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
fr.terminate("test");
|
||||
let e = yield getPromiseError(fr.process());
|
||||
assert.ok(e);
|
||||
e = yield getPromiseError(fr._feedItems[fr._feedItems.length-1].promise);
|
||||
assert.ok(e);
|
||||
});
|
||||
|
||||
it('should reject last feed item if feed processed', function* () {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
yield fr.process();
|
||||
fr.terminate("test");
|
||||
let e = yield getPromiseError(fr._feedItems[fr._feedItems.length-1].promise);
|
||||
assert.ok(e);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#feedProperties', function() {
|
||||
it('should throw if accessed before feed is processed', function () {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
assert.throw(() => fr.feedProperties);
|
||||
});
|
||||
|
||||
it('should have correct values for a sparse feed', function* () {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
yield fr.process();
|
||||
assert.deepEqual(fr.feedProperties, feedInfo);
|
||||
});
|
||||
|
||||
it('should have correct values for a detailed feed', function* () {
|
||||
let fr = new Zotero.FeedReader(detailedFeedUrl);
|
||||
yield fr.process();
|
||||
assert.deepEqual(fr.feedProperties, detailedFeedInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ItemIterator()', function() {
|
||||
it('should throw if called before feed is resolved', function() {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
assert.throw(() => new fr.ItemIterator);
|
||||
});
|
||||
|
||||
it('should parse items correctly for a sparse feed', function* () {
|
||||
let expected = { guid: 'http://liftoff.msfc.nasa.gov/2003/06/03.html#item573',
|
||||
title: 'Star City',
|
||||
abstractNote: 'How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia\'s Star City.',
|
||||
url: 'http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp',
|
||||
creators: [{ firstName: '', lastName: 'editor@example.com', creatorType: 'author', fieldMode: 1 }],
|
||||
date: 'Tue, 03 Jun 2003 09:39:21 GMT',
|
||||
language: 'en-us',
|
||||
itemType: 'journalArticle',
|
||||
enclosedItems: [{ url: 'http://www.example.com/example.pdf', contentType: 'application/pdf' }]
|
||||
};
|
||||
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
yield fr.process();
|
||||
let itemIterator = new fr.ItemIterator();
|
||||
let item = yield itemIterator.next().value;
|
||||
assert.deepEqual(item, expected);
|
||||
});
|
||||
|
||||
it('should parse items correctly for a detailed feed', function* () {
|
||||
let expected = {
|
||||
guid: 'http://www.example.com/item1',
|
||||
title: 'Title 1',
|
||||
abstractNote: 'Description 1',
|
||||
url: 'http://www.example.com/item1',
|
||||
creators: [
|
||||
{ firstName: 'Author1 A. T.', lastName: 'Rohtua', creatorType: 'author' },
|
||||
{ firstName: 'Author2 A.', lastName: 'Auth', creatorType: 'author' },
|
||||
{ firstName: 'Author3', lastName: 'Autho', creatorType: 'author' },
|
||||
{ firstName: 'Contributor1 A. T.', lastName: 'Rotubirtnoc', creatorType: 'contributor' },
|
||||
{ firstName: 'Contributor2 C.', lastName: 'Contrib', creatorType: 'contributor' },
|
||||
{ firstName: 'Contributor3', lastName: 'Contr', creatorType: 'contributor' }
|
||||
],
|
||||
date: '2016-01-07',
|
||||
publicationTitle: 'Publication',
|
||||
ISSN: '0000-0000',
|
||||
publisher: 'Publisher',
|
||||
rights: '©2016 Published by Publisher',
|
||||
language: 'en',
|
||||
itemType: 'journalArticle',
|
||||
enclosedItems: []
|
||||
};
|
||||
|
||||
let fr = new Zotero.FeedReader(detailedFeedUrl);
|
||||
yield fr.process();
|
||||
let itemIterator = new fr.ItemIterator();
|
||||
let item = yield itemIterator.next().value;
|
||||
assert.deepEqual(item, expected);
|
||||
});
|
||||
it('should resolve last item with null', function* () {
|
||||
let fr = new Zotero.FeedReader(feedUrl);
|
||||
yield fr.process();
|
||||
let itemIterator = new fr.ItemIterator();
|
||||
let item;
|
||||
while(item = yield itemIterator.next().value);
|
||||
assert.isNull(item);
|
||||
});
|
||||
});
|
||||
})
|
|
@ -1,24 +1,7 @@
|
|||
describe("Zotero.Feed", function() {
|
||||
let createFeed = Zotero.Promise.coroutine(function* (props = {}) {
|
||||
let feed = new Zotero.Feed({
|
||||
name: props.name || 'Test ' + Zotero.randomString(),
|
||||
url: props.url || 'http://www.' + Zotero.randomString() + '.com',
|
||||
refreshInterval: props.refreshInterval,
|
||||
cleanupAfter: props.cleanupAfter
|
||||
});
|
||||
|
||||
yield feed.saveTx();
|
||||
return feed;
|
||||
});
|
||||
|
||||
// Clean up after after tests
|
||||
after(function* () {
|
||||
let feeds = Zotero.Feeds.getAll();
|
||||
yield Zotero.DB.executeTransaction(function* () {
|
||||
for (let i=0; i<feeds.length; i++) {
|
||||
yield feeds[i].erase();
|
||||
}
|
||||
});
|
||||
yield clearFeeds();
|
||||
});
|
||||
|
||||
it("should be an instance of Zotero.Library", function() {
|
||||
|
@ -59,12 +42,11 @@ describe("Zotero.Feed", function() {
|
|||
yield feed.saveTx();
|
||||
assert.isFalse(feed.editable);
|
||||
});
|
||||
it("should not allow adding items without editCheck override", function* () {
|
||||
it("should allow adding items without editCheck override", function* () {
|
||||
let feed = yield createFeed();
|
||||
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
|
||||
feedItem.libraryID = feed.libraryID;
|
||||
yield assert.isRejected(feedItem.saveTx(), /^Error: Cannot edit feedItem in read-only library/);
|
||||
yield assert.isFulfilled(feedItem.saveTx({ skipEditCheck: true }));
|
||||
yield assert.isFulfilled(feedItem.saveTx());
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -93,7 +75,7 @@ describe("Zotero.Feed", function() {
|
|||
name: 'Test ' + Zotero.randomString(),
|
||||
url: 'http://' + Zotero.randomString() + '.com/',
|
||||
refreshInterval: 30,
|
||||
cleanupAfter: 2
|
||||
cleanupAfter: 1
|
||||
};
|
||||
|
||||
let feed = yield createFeed(props);
|
||||
|
@ -152,6 +134,31 @@ describe("Zotero.Feed", function() {
|
|||
assert.equal(feed.name, 'bar');
|
||||
assert.equal(dbVal, feed.name);
|
||||
});
|
||||
it("should add a new synced setting after creation", function* () {
|
||||
let url = 'http://' + Zotero.Utilities.randomString(10, 'abcde') + '.com/feed.rss';
|
||||
|
||||
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.notOk(syncedFeeds[url]);
|
||||
|
||||
yield createFeed({url});
|
||||
|
||||
syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.ok(syncedFeeds[url]);
|
||||
});
|
||||
it("should remove previous feed and add a new one if url changed", function* () {
|
||||
let feed = yield createFeed();
|
||||
|
||||
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.ok(syncedFeeds[feed.url]);
|
||||
|
||||
let oldUrl = feed.url;
|
||||
feed.url = 'http://' + Zotero.Utilities.randomString(10, 'abcde') + '.com/feed.rss';
|
||||
yield feed.saveTx();
|
||||
|
||||
syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.notOk(syncedFeeds[oldUrl]);
|
||||
assert.ok(syncedFeeds[feed.url]);
|
||||
});
|
||||
});
|
||||
describe("#erase()", function() {
|
||||
it("should erase a saved feed", function* () {
|
||||
|
@ -177,6 +184,202 @@ describe("Zotero.Feed", function() {
|
|||
|
||||
assert.notOk(yield Zotero.FeedItems.getAsync(feedItem.id));
|
||||
});
|
||||
it("should remove synced settings", function* () {
|
||||
let url = 'http://' + Zotero.Utilities.randomString(10, 'abcde') + '.com/feed.rss';
|
||||
let feed = yield createFeed({url});
|
||||
|
||||
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.ok(syncedFeeds[feed.url]);
|
||||
|
||||
yield feed.eraseTx();
|
||||
|
||||
syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.notOk(syncedFeeds[url]);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getSyncedSettings", function() {
|
||||
it("should return correct synced settings for the feed", function* () {
|
||||
let url = 'http://' + Zotero.Utilities.randomString(10, 'abcde') + '.com/feed.rss';
|
||||
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.notOk(syncedFeeds[url]);
|
||||
|
||||
let feed = yield createFeed({url});
|
||||
|
||||
syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.ok(syncedFeeds[url]);
|
||||
|
||||
let syncedData = feed.getSyncedSettings();
|
||||
|
||||
assert.deepEqual(syncedData, syncedFeeds[url]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#storeSyncedSettings", function() {
|
||||
it("should store updated settings for the feed", function* () {
|
||||
let guid = Zotero.Utilities.randomString();
|
||||
let feed = yield createFeed();
|
||||
|
||||
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
assert.notOk(syncedFeeds[feed.url].markedAsRead[guid]);
|
||||
|
||||
let syncedData = feed.getSyncedSettings();
|
||||
syncedData.markedAsRead[guid] = true;
|
||||
yield feed.setSyncedSettings(syncedData);
|
||||
yield feed.storeSyncedSettings();
|
||||
|
||||
syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
|
||||
|
||||
assert.isTrue(syncedFeeds[feed.url].markedAsRead[guid]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#clearExpiredItems()", function() {
|
||||
var feed, expiredFeedItem, readFeedItem, feedItem, readStillInFeed, feedItemIDs;
|
||||
|
||||
before(function* (){
|
||||
feed = yield createFeed({cleanupAfter: 1});
|
||||
|
||||
expiredFeedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
|
||||
// Read 2 days ago
|
||||
expiredFeedItem.isRead = true;
|
||||
expiredFeedItem._feedItemReadTime = Zotero.Date.dateToSQL(
|
||||
new Date(Date.now() - 2 * 24*60*60*1000), true);
|
||||
yield expiredFeedItem.saveTx();
|
||||
|
||||
readStillInFeed = yield createDataObject('feedItem', { libraryID: feed.libraryID });
|
||||
// Read 2 days ago
|
||||
readStillInFeed.isRead = true;
|
||||
readStillInFeed._feedItemReadTime = Zotero.Date.dateToSQL(
|
||||
new Date(Date.now() - 2 * 24*60*60*1000), true);
|
||||
yield readStillInFeed.saveTx();
|
||||
|
||||
readFeedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
|
||||
readFeedItem.isRead = true;
|
||||
yield readFeedItem.saveTx();
|
||||
|
||||
feedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
|
||||
|
||||
feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID).map((row) => row.id);
|
||||
|
||||
assert.include(feedItemIDs, feedItem.id, "feed contains unread feed item");
|
||||
assert.include(feedItemIDs, readFeedItem.id, "feed contains read feed item");
|
||||
assert.include(feedItemIDs, expiredFeedItem.id, "feed contains expired feed item");
|
||||
assert.include(feedItemIDs, readStillInFeed.id, "feed contains expired but still in rss feed item");
|
||||
|
||||
yield feed.clearExpiredItems(new Set([readStillInFeed.id]));
|
||||
|
||||
feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID).map((row) => row.id);
|
||||
});
|
||||
|
||||
it('should clear expired items', function() {
|
||||
assert.notInclude(feedItemIDs, expiredFeedItem.id, "feed no longer contain expired feed item");
|
||||
});
|
||||
|
||||
it('should not clear read items that have not expired yet', function() {
|
||||
assert.include(feedItemIDs, readFeedItem.id, "feed still contains new feed item");
|
||||
});
|
||||
|
||||
it('should not clear read items that are still in rss', function() {
|
||||
assert.include(feedItemIDs, readStillInFeed.id, "feed still contains read still in rss feed item");
|
||||
});
|
||||
|
||||
it('should not clear unread items', function() {
|
||||
assert.include(feedItemIDs, feedItem.id, "feed still contains new feed item");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateFeed()', function() {
|
||||
var feed;
|
||||
var feedUrl = getTestDataUrl("feed.rss");
|
||||
var modifiedFeedUrl = getTestDataUrl("feedModified.rss");
|
||||
|
||||
beforeEach(function* (){
|
||||
feed = yield createFeed();
|
||||
feed._feedUrl = feedUrl;
|
||||
yield feed.updateFeed();
|
||||
});
|
||||
|
||||
afterEach(function* () {
|
||||
yield clearFeeds();
|
||||
});
|
||||
|
||||
it('should schedule next feed check', function* () {
|
||||
let scheduleNextFeedCheck = sinon.stub(Zotero.Feeds, 'scheduleNextFeedCheck');
|
||||
|
||||
let feed = yield createFeed();
|
||||
feed._feedUrl = feedUrl;
|
||||
yield feed.updateFeed();
|
||||
assert.equal(scheduleNextFeedCheck.called, true);
|
||||
|
||||
scheduleNextFeedCheck.restore();
|
||||
});
|
||||
|
||||
it('should add new feed items', function* () {
|
||||
let feedItems = yield Zotero.FeedItems.getAll(feed.id, true);
|
||||
assert.equal(feedItems.length, 3);
|
||||
});
|
||||
|
||||
it('should set lastCheck and lastUpdated values', function* () {
|
||||
yield clearFeeds();
|
||||
let feed = yield createFeed();
|
||||
feed._feedUrl = feedUrl;
|
||||
|
||||
assert.notOk(feed.lastCheck);
|
||||
assert.notOk(feed.lastUpdate);
|
||||
|
||||
yield feed.updateFeed();
|
||||
|
||||
assert.isTrue(feed.lastCheck > Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60), true), 'feed.lastCheck updated');
|
||||
assert.isTrue(feed.lastUpdate > Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60), true), 'feed.lastUpdate updated');
|
||||
});
|
||||
it('should update modified items and set unread', function* () {
|
||||
let feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
|
||||
feedItem.isRead = true;
|
||||
yield feedItem.saveTx();
|
||||
feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
|
||||
assert.isTrue(feedItem.isRead);
|
||||
|
||||
let oldDateModified = feedItem.getField('date');
|
||||
|
||||
feed._feedUrl = modifiedFeedUrl;
|
||||
yield feed.updateFeed();
|
||||
|
||||
feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
|
||||
|
||||
assert.notEqual(oldDateModified, feedItem.getField('date'));
|
||||
assert.isFalse(feedItem.isRead)
|
||||
});
|
||||
it('should skip items that are not modified', function* () {
|
||||
let save = sinon.spy(Zotero.FeedItem.prototype, 'save');
|
||||
|
||||
feed._feedUrl = modifiedFeedUrl;
|
||||
yield feed.updateFeed();
|
||||
|
||||
assert.equal(save.thisValues[0].guid, "http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
|
||||
save.restore();
|
||||
});
|
||||
it('should update unread count', function* () {
|
||||
assert.equal(feed.unreadCount, 3);
|
||||
|
||||
let feedItems = yield Zotero.FeedItems.getAll(feed.id);
|
||||
for (let feedItem of feedItems) {
|
||||
feedItem.isRead = true;
|
||||
yield feedItem.saveTx();
|
||||
}
|
||||
|
||||
feed._feedUrl = modifiedFeedUrl;
|
||||
yield feed.updateFeed();
|
||||
|
||||
assert.equal(feed.unreadCount, 2);
|
||||
});
|
||||
it('should add a link to enclosed pdfs from <enclosure/> elements', function* () {
|
||||
let feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
|
||||
let pdf = yield Zotero.Items.getAsync(feedItem.getAttachments()[0]);
|
||||
|
||||
assert.equal(pdf.getField('url'), "http://www.example.com/example.pdf");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Adding items", function() {
|
||||
|
@ -184,11 +387,6 @@ describe("Zotero.Feed", function() {
|
|||
before(function* () {
|
||||
feed = yield createFeed();
|
||||
})
|
||||
it("should not allow adding regular items", function* () {
|
||||
let item = new Zotero.Item('book');
|
||||
item.libraryID = feed.libraryID;
|
||||
yield assert.isRejected(item.saveTx({ skipEditCheck: true }), /^Error: Cannot add /);
|
||||
});
|
||||
it("should not allow adding collections", function* () {
|
||||
let collection = new Zotero.Collection({ name: 'test', libraryID: feed.libraryID });
|
||||
yield assert.isRejected(collection.saveTx({ skipEditCheck: true }), /^Error: Cannot add /);
|
||||
|
@ -200,7 +398,7 @@ describe("Zotero.Feed", function() {
|
|||
it("should allow adding feed item", function* () {
|
||||
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
|
||||
feedItem.libraryID = feed.libraryID;
|
||||
yield assert.isFulfilled(feedItem.forceSaveTx());
|
||||
yield assert.isFulfilled(feedItem.saveTx());
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1,25 +1,80 @@
|
|||
describe("Zotero.Feeds", function () {
|
||||
let createFeed = Zotero.Promise.coroutine(function* (props = {}) {
|
||||
let feed = new Zotero.Feed({
|
||||
name: props.name || 'Test ' + Zotero.randomString(),
|
||||
url: props.url || 'http://www.' + Zotero.randomString() + '.com',
|
||||
refreshInterval: props.refreshInterval,
|
||||
cleanupAfter: props.cleanupAfter
|
||||
|
||||
after(function* () {
|
||||
yield clearFeeds();
|
||||
});
|
||||
|
||||
describe("#restoreFromJSON", function() {
|
||||
var json = {};
|
||||
var expiredFeedURL, existingFeedURL;
|
||||
|
||||
before(function() {
|
||||
sinon.stub(Zotero.Feed.prototype, 'updateFeed').resolves();
|
||||
});
|
||||
|
||||
yield feed.saveTx();
|
||||
return feed;
|
||||
});
|
||||
|
||||
let clearFeeds = Zotero.Promise.coroutine(function* () {
|
||||
let feeds = Zotero.Feeds.getAll();
|
||||
yield Zotero.DB.executeTransaction(function* () {
|
||||
for (let i=0; i<feeds.length; i++) {
|
||||
yield feeds[i].erase();
|
||||
after(function() {
|
||||
Zotero.Feed.prototype.updateFeed.restore();
|
||||
});
|
||||
|
||||
beforeEach(function* () {
|
||||
yield clearFeeds();
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
let url = "http://" + Zotero.Utilities.randomString(10, 'abcdefgh') + ".com/feed.rss";
|
||||
json[url] = {
|
||||
url,
|
||||
name: Zotero.Utilities.randomString(),
|
||||
refreshInterval: 5,
|
||||
cleanupAfter: 3,
|
||||
markedAsRead: []
|
||||
};
|
||||
if (i == 0) {
|
||||
existingFeedURL = url;
|
||||
yield createFeed({url});
|
||||
}
|
||||
}
|
||||
expiredFeedURL = (yield createFeed()).url;
|
||||
});
|
||||
|
||||
it("restores correctly when merge is true", function* () {
|
||||
let feeds = Zotero.Feeds.getAll();
|
||||
assert.equal(feeds.length, 2);
|
||||
|
||||
yield Zotero.Feeds.restoreFromJSON(json, true);
|
||||
feeds = Zotero.Feeds.getAll();
|
||||
|
||||
for (let url in json) {
|
||||
let feed = Zotero.Feeds.getByURL(url);
|
||||
assert.ok(feed, "new feed created");
|
||||
}
|
||||
|
||||
let expiredFeed = Zotero.Feeds.getByURL(expiredFeedURL);
|
||||
assert.ok(expiredFeed, "does not remove feeds not in JSON");
|
||||
|
||||
let existingFeed = Zotero.Feeds.getByURL(existingFeedURL);
|
||||
assert.ok(existingFeed, "does not remove feeds in database and JSON");
|
||||
});
|
||||
|
||||
it("restores correctly when merge is false", function* () {
|
||||
let feeds = Zotero.Feeds.getAll();
|
||||
assert.equal(feeds.length, 2);
|
||||
|
||||
yield Zotero.Feeds.restoreFromJSON(json);
|
||||
feeds = Zotero.Feeds.getAll();
|
||||
|
||||
for (let url in json) {
|
||||
let feed = Zotero.Feeds.getByURL(url);
|
||||
assert.ok(feed, "new feed created");
|
||||
}
|
||||
|
||||
let expiredFeed = Zotero.Feeds.getByURL(expiredFeedURL);
|
||||
assert.notOk(expiredFeed, "removes feeds not in JSON");
|
||||
|
||||
let existingFeed = Zotero.Feeds.getByURL(existingFeedURL);
|
||||
assert.ok(existingFeed, "does not remove feeds in database and JSON");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("#haveFeeds()", function() {
|
||||
it("should return false for a DB without feeds", function* () {
|
||||
yield clearFeeds();
|
||||
|
@ -56,4 +111,101 @@ describe("Zotero.Feeds", function () {
|
|||
assert.sameMembers(feeds, [feed1, feed2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getByURL', function() {
|
||||
it("should return a feed by url", function* () {
|
||||
let url = 'http://' + Zotero.Utilities.randomString(10, 'abcdefg') + '.com/feed.rss';
|
||||
yield createFeed({url});
|
||||
let feed = Zotero.Feeds.getByURL(url);
|
||||
assert.ok(feed);
|
||||
assert.equal(feed.url, url);
|
||||
});
|
||||
it("should return undefined if feed does not exist", function* () {
|
||||
var feed;
|
||||
assert.doesNotThrow(function() {
|
||||
feed = Zotero.Feeds.getByURL('doesnotexist');
|
||||
});
|
||||
assert.isUndefined(feed);
|
||||
});
|
||||
});
|
||||
describe('#updateFeeds', function() {
|
||||
var freshFeed, recentFeed, oldFeed;
|
||||
var _updateFeed;
|
||||
|
||||
before(function* () {
|
||||
yield clearFeeds();
|
||||
|
||||
sinon.stub(Zotero.Feeds, 'scheduleNextFeedCheck');
|
||||
_updateFeed = sinon.stub(Zotero.Feed.prototype, '_updateFeed').resolves();
|
||||
let url = getTestDataUrl("feed.rss");
|
||||
|
||||
freshFeed = yield createFeed({refreshInterval: 2});
|
||||
freshFeed._feedUrl = url;
|
||||
freshFeed.lastCheck = null;
|
||||
yield freshFeed.saveTx();
|
||||
|
||||
recentFeed = yield createFeed({refreshInterval: 2});
|
||||
recentFeed._feedUrl = url;
|
||||
recentFeed.lastCheck = Zotero.Date.dateToSQL(new Date(), true);
|
||||
yield recentFeed.saveTx();
|
||||
|
||||
oldFeed = yield createFeed({refreshInterval: 2});
|
||||
oldFeed._feedUrl = url;
|
||||
oldFeed.lastCheck = Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60*60*6), true);
|
||||
yield oldFeed.saveTx();
|
||||
|
||||
yield Zotero.Feeds.updateFeeds();
|
||||
assert.isTrue(_updateFeed.called);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
Zotero.Feeds.scheduleNextFeedCheck.restore();
|
||||
_updateFeed.restore();
|
||||
});
|
||||
|
||||
it('should update feeds that have never been updated', function() {
|
||||
for (var feed of _updateFeed.thisValues) {
|
||||
if (feed.id == freshFeed.id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.isTrue(feed._updateFeed.called);
|
||||
});
|
||||
it('should update feeds that need updating since last check', function() {
|
||||
for (var feed of _updateFeed.thisValues) {
|
||||
if (feed.id == oldFeed.id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.isTrue(feed._updateFeed.called);
|
||||
});
|
||||
it("should not update feeds that don't need updating", function() {
|
||||
for (var feed of _updateFeed.thisValues) {
|
||||
if (feed.id != recentFeed.id) {
|
||||
break;
|
||||
}
|
||||
// should never reach
|
||||
assert.ok(null, "does not update feed that did not need updating")
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('#scheduleNextFeedCheck()', function() {
|
||||
it('schedules next feed check', function* () {
|
||||
sinon.spy(Zotero.Feeds, 'scheduleNextFeedCheck');
|
||||
sinon.spy(Zotero.Promise, 'delay');
|
||||
|
||||
yield clearFeeds();
|
||||
let feed = yield createFeed({refreshInterval: 1});
|
||||
feed._set('_feedLastCheck', Zotero.Date.dateToSQL(new Date(), true));
|
||||
yield feed.saveTx();
|
||||
|
||||
yield Zotero.Feeds.scheduleNextFeedCheck();
|
||||
|
||||
// Allow a propagation delay of 5000ms
|
||||
assert.isTrue(Zotero.Promise.delay.args[0][0] - 1000*60*60 <= 5000);
|
||||
|
||||
Zotero.Feeds.scheduleNextFeedCheck.restore();
|
||||
Zotero.Promise.delay.restore();
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -131,7 +131,7 @@ describe("Zotero.Items", function () {
|
|||
|
||||
let feedItem = new Zotero.FeedItem('journalArticle', { guid: Zotero.randomString() });
|
||||
feedItem.libraryID = feed.libraryID;
|
||||
let id = yield feedItem.forceSaveTx();
|
||||
let id = yield feedItem.saveTx();
|
||||
|
||||
feedItem = yield Zotero.Items.getAsync(id);
|
||||
|
||||
|
|
Loading…
Reference in a new issue