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();"/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue