mirror of
https://github.com/zotero/zotero.git
synced 2026-10-11 03:38:25 +00:00
Compare commits
77 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbf4692b99 | ||
|
|
56f182f816 | ||
|
|
6053321766 | ||
|
|
9828edc2e2 | ||
|
|
35064caddd | ||
|
|
601358f24b | ||
|
|
f4b531e657 | ||
|
|
c93a0e0e80 | ||
|
|
096cc2e27b | ||
|
|
5ae1986f79 | ||
|
|
66d83ec4c8 | ||
|
|
b250aed79d | ||
|
|
85b97f82e6 | ||
|
|
64b444752a | ||
|
|
0542010659 | ||
|
|
4b78b2c130 | ||
|
|
7f6a07b665 | ||
|
|
fbd3fd5ceb | ||
|
|
6ddc360145 | ||
|
|
63b06f4346 | ||
|
|
71375b15a5 | ||
|
|
c6b7829501 | ||
|
|
1bad050d7f | ||
|
|
239530c515 | ||
|
|
828e0c942a | ||
|
|
cf37dfb330 | ||
|
|
6af5a484fd | ||
|
|
7de623accf | ||
|
|
10b3b5f915 | ||
|
|
051280fc43 | ||
|
|
264f55545a | ||
|
|
184671f919 | ||
|
|
24c7f146b3 | ||
|
|
707c71506f | ||
|
|
569bd1ffae | ||
|
|
62d46d337a | ||
|
|
ce0f7c0378 | ||
|
|
d2c9a310a8 | ||
|
|
b4bf86915b | ||
|
|
8804effba4 | ||
|
|
dfde08852a | ||
|
|
006a787272 | ||
|
|
0424faee42 | ||
|
|
3c9dcc94bc | ||
|
|
4bc61273a0 | ||
|
|
59b3268d01 | ||
|
|
ddb1daabbc | ||
|
|
d0debc0bed | ||
|
|
b5b59062a7 | ||
|
|
65461de4f5 | ||
|
|
b9e46ec660 | ||
|
|
22541ff8d8 | ||
|
|
f7b4cf907b | ||
|
|
36838e6a9a | ||
|
|
5b427ac332 | ||
|
|
65c77ddaae | ||
|
|
489d747b44 | ||
|
|
09813be7e0 | ||
|
|
85b111de4a | ||
|
|
788c48cc26 | ||
|
|
e3ae991bb3 | ||
|
|
e4a21b174f | ||
|
|
06fe4ad346 | ||
|
|
2019ac39a1 | ||
|
|
02b48318d7 | ||
|
|
f836e74996 | ||
|
|
1321447374 | ||
|
|
16221ac348 | ||
|
|
c0c00752e7 | ||
|
|
9fc00782de | ||
|
|
b9d1f50559 | ||
|
|
77bdb0b4cb | ||
|
|
320399dca9 | ||
|
|
8c54b98483 | ||
|
|
a0009f0fa3 | ||
|
|
cb0b2f9d58 | ||
|
|
a74117d824 |
94 changed files with 1876 additions and 1150 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -19,6 +19,9 @@ jobs:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
#cache: npm
|
#cache: npm
|
||||||
|
|
||||||
|
- name: Install dbus-glib
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y libdbus-glib-1-2
|
||||||
|
|
||||||
# On GitHub
|
# On GitHub
|
||||||
- name: Install xvfb
|
- name: Install xvfb
|
||||||
if: env.ACT != 'true'
|
if: env.ACT != 'true'
|
||||||
|
|
@ -57,10 +60,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --openssl-legacy-provider
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
# Somehow forces dpl v1 to be installed, even though v2 is installed without
|
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3' # Not needed with a `.ruby-version` or `.tool-versions`
|
ruby-version: '3.3'
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- name: Upload deployment ZIP
|
- name: Upload deployment ZIP
|
||||||
|
|
@ -68,7 +70,7 @@ jobs:
|
||||||
env.ACT != 'true'
|
env.ACT != 'true'
|
||||||
&& github.repository == 'zotero/zotero'
|
&& github.repository == 'zotero/zotero'
|
||||||
&& github.event_name == 'push'
|
&& github.event_name == 'push'
|
||||||
&& (github.ref == 'refs/heads/main' || endsWith(github.ref, '-hotfix'))
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/7.0' || endsWith(github.ref, '-hotfix'))
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
@ -77,11 +79,8 @@ jobs:
|
||||||
cd build
|
cd build
|
||||||
zip -r ../build-zip/$GITHUB_SHA.zip *
|
zip -r ../build-zip/$GITHUB_SHA.zip *
|
||||||
cd ..
|
cd ..
|
||||||
# Once dpl 2 works:
|
gem install --no-document dpl -v '>= 2.0'
|
||||||
#sudo gem install --no-document dpl
|
dpl s3 --bucket zotero-download --local_dir build-zip --upload_dir ci/client --acl public_read
|
||||||
#dpl s3 --bucket zotero-download --local_dir build-zip --upload_dir ci/client --acl public_read
|
|
||||||
sudo gem install --no-document dpl dpl-s3
|
|
||||||
dpl --provider=s3 --bucket=zotero-download --local-dir=build-zip --upload-dir=ci/client --acl=public-read --skip_cleanup=true
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: xvfb-run test/runtests.sh -f
|
run: xvfb-run test/runtests.sh -f
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ pref("javascript.options.showInConsole", true);
|
||||||
|
|
||||||
// Don't retrieve unrequested links when performing standalone translation
|
// Don't retrieve unrequested links when performing standalone translation
|
||||||
pref("network.prefetch-next", false);
|
pref("network.prefetch-next", false);
|
||||||
|
// Don't make DNS requests for links in snapshots
|
||||||
|
pref("network.dns.disablePrefetch", true);
|
||||||
|
// Don't open TCP connection when clicking on a link in a snapshot
|
||||||
|
pref("network.http.speculative-parallel-limit", 0);
|
||||||
|
|
||||||
// Let operations run as long as necessary
|
// Let operations run as long as necessary
|
||||||
pref("dom.max_chrome_script_run_time", 0);
|
pref("dom.max_chrome_script_run_time", 0);
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 13c92c090c12f6555bf6bab98780025e93d75077
|
Subproject commit 01b4a2abfcdaa35f478168af805df627cfedcb6a
|
||||||
|
|
@ -7,7 +7,7 @@ ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
. "$APP_ROOT_DIR/config.sh"
|
. "$APP_ROOT_DIR/config.sh"
|
||||||
|
|
||||||
CHANNEL="release"
|
CHANNEL="release"
|
||||||
BRANCH="main"
|
BRANCH="7.0"
|
||||||
if [ "`uname`" = "Darwin" ]; then
|
if [ "`uname`" = "Darwin" ]; then
|
||||||
export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex"
|
export SAFARI_APPEX="$ROOT_DIR/../safari-app-extension-builds/release/ZoteroSafariExtension.appex"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
title="Scaffold"
|
title="Scaffold"
|
||||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||||
|
windowtype="zotero:scaffold"
|
||||||
style="display: flex;"
|
style="display: flex;"
|
||||||
drawintitlebar-platforms="win, linux">
|
drawintitlebar-platforms="win, linux">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -736,9 +736,9 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
||||||
return numSelected;
|
return numSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a single item and it's in the trash, switch to that
|
// If there's a single item and it or an ancestor are in the trash, switch to that
|
||||||
if (items.length == 1 && items[0].deleted) {
|
if (items.length == 1 && items[0].isInTrash()) {
|
||||||
Zotero.debug("Item is deleted; switching to trash");
|
Zotero.debug("Item is in trash; switching to trash");
|
||||||
await this.selectTrash(libraryID);
|
await this.selectTrash(libraryID);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@
|
||||||
this._abstractField.setAttribute('aria-label', Zotero.ItemFields.getLocalizedString('abstractNote'));
|
this._abstractField.setAttribute('aria-label', Zotero.ItemFields.getLocalizedString('abstractNote'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _ensureFeedAbstractBrowserExists() {
|
_ensureFeedAbstractBrowserExists = Zotero.Utilities.Internal.serial(async () => {
|
||||||
if (!this._feedAbstractBrowser) {
|
if (!this._feedAbstractBrowser) {
|
||||||
// dynamically create a browser element to avoid spawning a process for every tab (#4530)
|
// dynamically create a browser element to avoid spawning a process for every tab (#4530)
|
||||||
this._feedAbstractBrowser = document.createXULElement("browser");
|
this._feedAbstractBrowser = document.createXULElement("browser");
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
});
|
||||||
|
|
||||||
_discardFeedAbstractBrowser() {
|
_discardFeedAbstractBrowser() {
|
||||||
if (this._feedAbstractBrowser) {
|
if (this._feedAbstractBrowser) {
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@
|
||||||
let imageAnnotationRendered = false;
|
let imageAnnotationRendered = false;
|
||||||
for (let annotation of this._annotationItems) {
|
for (let annotation of this._annotationItems) {
|
||||||
if (!imageAnnotationRendered
|
if (!imageAnnotationRendered
|
||||||
&& annotation.annotationType === 'image'
|
&& ['image', 'ink'].includes(annotation.annotationType)
|
||||||
&& !await Zotero.Annotations.hasCacheImage(annotation)) {
|
&& !await Zotero.Annotations.hasCacheImage(annotation)) {
|
||||||
try {
|
try {
|
||||||
await Zotero.PDFWorker.renderAttachmentAnnotations(annotation.parentID);
|
await Zotero.PDFWorker.renderAttachmentAnnotations(annotation.parentID);
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,6 @@
|
||||||
|
|
||||||
async asyncRender() {
|
async asyncRender() {
|
||||||
if (!this.item) return;
|
if (!this.item) return;
|
||||||
if (this._asyncRendering) return;
|
|
||||||
if (!this._section.open) return;
|
if (!this._section.open) return;
|
||||||
if (this._isAlreadyRendered("async")) {
|
if (this._isAlreadyRendered("async")) {
|
||||||
if (this._previewDiscarded) {
|
if (this._previewDiscarded) {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
this._selectFieldSelection = null;
|
this._selectFieldSelection = null;
|
||||||
this._addCreatorRow = false;
|
this._addCreatorRow = false;
|
||||||
this._switchedModeOfCreator = null;
|
this._switchedModeOfCreator = null;
|
||||||
|
this._popupNode = null;
|
||||||
|
|
||||||
this._lastUpdateCustomRows = "";
|
this._lastUpdateCustomRows = "";
|
||||||
// Keep in sync with itemPaneManager.js
|
// Keep in sync with itemPaneManager.js
|
||||||
|
|
@ -125,7 +126,7 @@
|
||||||
init() {
|
init() {
|
||||||
this.initCollapsibleSection();
|
this.initCollapsibleSection();
|
||||||
this._creatorTypeMenu.addEventListener('command', async (event) => {
|
this._creatorTypeMenu.addEventListener('command', async (event) => {
|
||||||
var typeBox = document.popupNode;
|
var typeBox = this._popupNode;
|
||||||
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
||||||
|
|
||||||
var typeID = event.explicitOriginalTarget.getAttribute('typeid');
|
var typeID = event.explicitOriginalTarget.getAttribute('typeid');
|
||||||
|
|
@ -144,7 +145,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
|
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (_event) => {
|
||||||
var row = document.popupNode.closest('.meta-row');
|
var row = this._popupNode.closest('.meta-row');
|
||||||
var typeBox = row.querySelector('.creator-type-label').parentNode;
|
var typeBox = row.querySelector('.creator-type-label').parentNode;
|
||||||
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
||||||
var item = this.item;
|
var item = this.item;
|
||||||
|
|
@ -182,7 +183,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
this._id('zotero-creator-transform-menu').addEventListener('command', async (event) => {
|
this._id('zotero-creator-transform-menu').addEventListener('command', async (event) => {
|
||||||
var row = document.popupNode.closest('.meta-row');
|
var row = this._popupNode.closest('.meta-row');
|
||||||
var typeBox = row.querySelector('.creator-type-label').parentNode;
|
var typeBox = row.querySelector('.creator-type-label').parentNode;
|
||||||
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
||||||
|
|
||||||
|
|
@ -666,12 +667,13 @@
|
||||||
}
|
}
|
||||||
if (addLinkContextMenu) {
|
if (addLinkContextMenu) {
|
||||||
rowData.oncontextmenu = (event) => {
|
rowData.oncontextmenu = (event) => {
|
||||||
this._linkMenu.dataset.link = link;
|
let menupopup = this._linkMenu;
|
||||||
this._linkMenu.dataset.val = val;
|
|
||||||
document.popupNode = rowLabel.parentElement;
|
|
||||||
|
|
||||||
let menupopup = this._id('zotero-link-menu');
|
menupopup.dataset.link = link;
|
||||||
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, event.target.closest('input'));
|
menupopup.dataset.val = val;
|
||||||
|
this._popupNode = rowLabel.parentElement;
|
||||||
|
|
||||||
|
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, event.target);
|
||||||
this.handlePopupOpening(event, menupopup);
|
this.handlePopupOpening(event, menupopup);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1189,7 +1191,7 @@
|
||||||
span.className = 'creator-type-dropmarker';
|
span.className = 'creator-type-dropmarker';
|
||||||
labelWrapper.appendChild(span);
|
labelWrapper.appendChild(span);
|
||||||
labelWrapper.addEventListener('click', (e) => {
|
labelWrapper.addEventListener('click', (e) => {
|
||||||
document.popupNode = rowLabel;
|
this._popupNode = rowLabel;
|
||||||
this._creatorTypeMenu.openPopup(rowLabel);
|
this._creatorTypeMenu.openPopup(rowLabel);
|
||||||
// If the creator menu is opened via mouse-click, add a special attribute to
|
// If the creator menu is opened via mouse-click, add a special attribute to
|
||||||
// blur the focused field so that icons do not show up after the menu is closed.
|
// blur the focused field so that icons do not show up after the menu is closed.
|
||||||
|
|
@ -1296,7 +1298,7 @@
|
||||||
optionsButton.setAttribute('id', `creator-${rowIndex}-options`);
|
optionsButton.setAttribute('id', `creator-${rowIndex}-options`);
|
||||||
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
|
optionsButton.setAttribute('data-l10n-id', "itembox-button-options");
|
||||||
let triggerPopup = (e) => {
|
let triggerPopup = (e) => {
|
||||||
document.popupNode = firstlast;
|
this._popupNode = firstlast;
|
||||||
|
|
||||||
let menupopup = this._id('zotero-creator-transform-menu');
|
let menupopup = this._id('zotero-creator-transform-menu');
|
||||||
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, e.target.closest('input'));
|
Zotero.Utilities.Internal.updateEditContextMenu(menupopup, e.target.closest('input'));
|
||||||
|
|
@ -2196,6 +2198,8 @@
|
||||||
textbox.setAttribute("min-lines", 1);
|
textbox.setAttribute("min-lines", 1);
|
||||||
}
|
}
|
||||||
var value = textbox.value.trim();
|
var value = textbox.value.trim();
|
||||||
|
// Remove trailing whitespaces from editable-text in case itemBox is not refreshed
|
||||||
|
textbox.value = value;
|
||||||
|
|
||||||
var [field, creatorIndex, creatorField] = fieldName.split('-');
|
var [field, creatorIndex, creatorField] = fieldName.split('-');
|
||||||
|
|
||||||
|
|
@ -2330,6 +2334,7 @@
|
||||||
row.querySelector(".zotero-clicky-plus").hidden = true;
|
row.querySelector(".zotero-clicky-plus").hidden = true;
|
||||||
row.querySelector(".zotero-clicky-minus").hidden = true;
|
row.querySelector(".zotero-clicky-minus").hidden = true;
|
||||||
row.querySelector(".zotero-clicky-options").hidden = true;
|
row.querySelector(".zotero-clicky-options").hidden = true;
|
||||||
|
row.querySelector(".zotero-clicky-switch-type").hidden = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2389,7 +2394,7 @@
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async swapNames(_event) {
|
async swapNames(_event) {
|
||||||
var row = document.popupNode.closest('.meta-row');
|
var row = this._popupNode.closest('.meta-row');
|
||||||
var typeBox = row.querySelector('[fieldname]');
|
var typeBox = row.querySelector('[fieldname]');
|
||||||
var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
||||||
var fields = this.getCreatorFields(row);
|
var fields = this.getCreatorFields(row);
|
||||||
|
|
@ -2414,7 +2419,7 @@
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
async capitalizeCreatorName(_event) {
|
async capitalizeCreatorName(_event) {
|
||||||
var row = document.popupNode.closest('.meta-row');
|
var row = this._popupNode.closest('.meta-row');
|
||||||
let label = row.querySelector('.meta-label');
|
let label = row.querySelector('.meta-label');
|
||||||
var creatorIndex = parseInt(label.getAttribute('fieldname').split('-')[1]);
|
var creatorIndex = parseInt(label.getAttribute('fieldname').split('-')[1]);
|
||||||
let [lastName, firstName] = [...row.querySelectorAll("editable-text")];
|
let [lastName, firstName] = [...row.querySelectorAll("editable-text")];
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const waitDOMUpdate = async (timeout = 50) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
requestIdleCallback(resolve, { timeout });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const waitNoLongerThan = async (promise, ms = 1000) => {
|
const waitNoLongerThan = async (promise, ms = 1000) => {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
promise,
|
promise,
|
||||||
|
|
@ -269,6 +275,11 @@
|
||||||
this._restorePinnedPane();
|
this._restorePinnedPane();
|
||||||
|
|
||||||
let panes = this.getPanes();
|
let panes = this.getPanes();
|
||||||
|
|
||||||
|
// Unobserve intersection to prevent unwanted rendering.
|
||||||
|
// Checking flags in _handleIntersection is not reliable because it's async.
|
||||||
|
this._toggleIntersectionObserver(false);
|
||||||
|
|
||||||
for (let box of [this._header, ...panes]) {
|
for (let box of [this._header, ...panes]) {
|
||||||
box.editable = this.editable;
|
box.editable = this.editable;
|
||||||
box.tabID = this.tabID;
|
box.tabID = this.tabID;
|
||||||
|
|
@ -291,7 +302,7 @@
|
||||||
this._paneParent.style.paddingBottom = '';
|
this._paneParent.style.paddingBottom = '';
|
||||||
if (pinnedPaneElem) {
|
if (pinnedPaneElem) {
|
||||||
let paneID = pinnedPaneElem.dataset.pane;
|
let paneID = pinnedPaneElem.dataset.pane;
|
||||||
this.scrollToPane(paneID, 'instant');
|
await this.scrollToPane(paneID, 'instant');
|
||||||
this.pinnedPane = paneID;
|
this.pinnedPane = paneID;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -310,10 +321,17 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await waitNoLongerThan(box.asyncRender(), 500);
|
await waitNoLongerThan(box.asyncRender(), 500);
|
||||||
|
// Make sure the layout is updated for next isPaneVisible check
|
||||||
|
await waitDOMUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.item.id == item.id) {
|
if (this.item.id == item.id) {
|
||||||
this._isRendering = false;
|
this._isRendering = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-enable intersection observer
|
||||||
|
this._toggleIntersectionObserver(true);
|
||||||
|
|
||||||
if (Zotero.test) {
|
if (Zotero.test) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -439,7 +457,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async scrollToPane(paneID, behavior = 'smooth') {
|
async scrollToPane(paneID, behavior = 'smooth') {
|
||||||
let pane = this.getEnabledPane(paneID);
|
let panes = this.getEnabledPanes();
|
||||||
|
let paneIndex = panes.findIndex(elem => elem.dataset.pane == paneID);
|
||||||
|
let pane = panes[paneIndex];
|
||||||
if (!pane) return null;
|
if (!pane) return null;
|
||||||
|
|
||||||
let scrollPromise;
|
let scrollPromise;
|
||||||
|
|
@ -450,6 +470,9 @@
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temporarily disable intersection observer to prevent unwanted rendering
|
||||||
|
this._toggleIntersectionObserver(false);
|
||||||
|
|
||||||
// The pane should always be at the very top
|
// The pane should always be at the very top
|
||||||
// If there isn't enough stuff below it for it to be at the top, we add padding
|
// If there isn't enough stuff below it for it to be at the top, we add padding
|
||||||
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
|
// We use a ::before pseudo-element for this so that we don't need to add another level to the DOM
|
||||||
|
|
@ -459,9 +482,31 @@
|
||||||
scrollPromise = this._waitForScroll();
|
scrollPromise = this._waitForScroll();
|
||||||
scrollPromise.then(() => this._disableScrollHandler = false);
|
scrollPromise.then(() => this._disableScrollHandler = false);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Wait for the next DOM update to make sure the height is updated before rendering
|
||||||
|
scrollPromise = waitDOMUpdate();
|
||||||
|
}
|
||||||
pane.scrollIntoView({ block: 'start', behavior });
|
pane.scrollIntoView({ block: 'start', behavior });
|
||||||
pane.focus();
|
pane.focus();
|
||||||
return scrollPromise;
|
await scrollPromise;
|
||||||
|
|
||||||
|
// Check current and following panes for async render
|
||||||
|
for (let i = paneIndex; i < panes.length; i++) {
|
||||||
|
let nextPane = panes[i];
|
||||||
|
// Stop if the pane is not visible anymore
|
||||||
|
if (!this.isPaneVisible(nextPane.dataset.pane)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextPane.asyncRender) {
|
||||||
|
await nextPane.asyncRender();
|
||||||
|
// Wait for the next DOM update to make sure the layout is updated
|
||||||
|
await waitDOMUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable intersection observer
|
||||||
|
this._toggleIntersectionObserver(true);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeSpaceForPane(pane) {
|
_makeSpaceForPane(pane) {
|
||||||
|
|
@ -528,7 +573,17 @@
|
||||||
this._intersectionOb.disconnect();
|
this._intersectionOb.disconnect();
|
||||||
}
|
}
|
||||||
this._intersectionOb = new IntersectionObserver(this._handleIntersection);
|
this._intersectionOb = new IntersectionObserver(this._handleIntersection);
|
||||||
this.getPanes().forEach(elem => this._intersectionOb.observe(elem));
|
this._toggleIntersectionObserver(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggleIntersectionObserver(enabled) {
|
||||||
|
let panes = this.getPanes();
|
||||||
|
if (enabled) {
|
||||||
|
panes.forEach(elem => this._intersectionOb.observe(elem));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
panes.forEach(elem => this._intersectionOb.unobserve(elem));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleContainerScroll = () => {
|
_handleContainerScroll = () => {
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,18 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItemPane(item) {
|
async renderItemPane(item) {
|
||||||
|
let previousMode = this.mode;
|
||||||
this.mode = "item";
|
this.mode = "item";
|
||||||
|
|
||||||
|
// Fix https://forums.zotero.org/discussion/115450/zotero-7-beta-wrong-vertical-position-in-the-item-pane-after-switching-from-a-note
|
||||||
|
if (previousMode === "note") {
|
||||||
|
// Wait for DOM to update and then trigger item-details render
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
requestIdleCallback(resolve, { timeout: 50 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this._itemDetails.editable = this.editable;
|
this._itemDetails.editable = this.editable;
|
||||||
this._itemDetails.tabID = "zotero-pane";
|
this._itemDetails.tabID = "zotero-pane";
|
||||||
this._itemDetails.tabType = "library";
|
this._itemDetails.tabType = "library";
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,6 @@
|
||||||
|
|
||||||
_contextMenuTarget = null;
|
_contextMenuTarget = null;
|
||||||
|
|
||||||
_disableScrollHandler = false;
|
|
||||||
|
|
||||||
get container() {
|
get container() {
|
||||||
return this._container;
|
return this._container;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,6 @@ import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
|
||||||
choosePane(pane) {
|
choosePane(pane) {
|
||||||
Zotero.debug(new Error().stack);
|
|
||||||
let otherPane;
|
let otherPane;
|
||||||
if (pane.id == 'left-pane') {
|
if (pane.id == 'left-pane') {
|
||||||
otherPane = this._rightPane;
|
otherPane = this._rightPane;
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@
|
||||||
if (this._item == val) {
|
if (this._item == val) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Blur events do not fire when item is changed by clicking on an itemTree row.
|
||||||
|
// Make sure any changes to the tags get saved before item is updated.
|
||||||
|
this.blurOpenField();
|
||||||
this._item = val;
|
this._item = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +135,7 @@
|
||||||
|
|
||||||
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
|
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
|
||||||
|
|
||||||
let focusedTag = this._id('rows').querySelector('editable-text:focus')?.value;
|
let focusedTag = this._id('rows').querySelector('editable-text:focus-within')?.value;
|
||||||
|
|
||||||
let tagRows = this._id('rows');
|
let tagRows = this._id('rows');
|
||||||
tagRows.replaceChildren();
|
tagRows.replaceChildren();
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ const apiFetchUrl = async (tokens, url, headers = {}, options = {}) => {
|
||||||
|
|
||||||
const apiFetch = async (tokens, endPoint, params = {}, headers = {}, options = {}) => {
|
const apiFetch = async (tokens, endPoint, params = {}, headers = {}, options = {}) => {
|
||||||
const stringParams = Object.entries(params).map(p => p.join('=')).join('&');
|
const stringParams = Object.entries(params).map(p => p.join('=')).join('&');
|
||||||
const url = MENDELEY_API_URL + '/' + endPoint + '?' + stringParams;
|
const url = `${MENDELEY_API_URL}/${endPoint}${stringParams.length ? '?' + stringParams : ''}`;
|
||||||
return apiFetchUrl(tokens, url, headers, options);
|
return apiFetchUrl(tokens, url, headers, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -183,8 +183,8 @@ const obtainReferenceManagerToken = async (login, password) => {
|
||||||
hasEnteredLogin = await browser.browsingContext.currentWindowGlobal
|
hasEnteredLogin = await browser.browsingContext.currentWindowGlobal
|
||||||
.getActor("MendeleyAuth")
|
.getActor("MendeleyAuth")
|
||||||
.sendQuery("login", { login });
|
.sendQuery("login", { login });
|
||||||
Zotero.debug(`hasEnteredLogin: ${hasEnteredLogin}`);
|
|
||||||
if (!hasEnteredLogin) {
|
if (!hasEnteredLogin) {
|
||||||
|
browser.destroy();
|
||||||
reject(new Error("Failed to enter login"));
|
reject(new Error("Failed to enter login"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,8 +195,8 @@ const obtainReferenceManagerToken = async (login, password) => {
|
||||||
hasEnteredPassword = await browser.browsingContext.currentWindowGlobal
|
hasEnteredPassword = await browser.browsingContext.currentWindowGlobal
|
||||||
.getActor("MendeleyAuth")
|
.getActor("MendeleyAuth")
|
||||||
.sendQuery("password", { password });
|
.sendQuery("password", { password });
|
||||||
Zotero.debug(`hasEnteredPassword: ${hasEnteredPassword}`);
|
|
||||||
if (!hasEnteredPassword) {
|
if (!hasEnteredPassword) {
|
||||||
|
browser.destroy();
|
||||||
reject(new Error("Failed to enter password"));
|
reject(new Error("Failed to enter password"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +205,7 @@ const obtainReferenceManagerToken = async (login, password) => {
|
||||||
const cookies = cookieSandbox.getCookiesForURI(
|
const cookies = cookieSandbox.getCookiesForURI(
|
||||||
Services.io.newURI("https://www.mendeley.com/reference-manager/library")
|
Services.io.newURI("https://www.mendeley.com/reference-manager/library")
|
||||||
);
|
);
|
||||||
|
browser.destroy();
|
||||||
if (!cookies.accessToken) {
|
if (!cookies.accessToken) {
|
||||||
reject(new Error("Failed to obtain Mendeley access token"));
|
reject(new Error("Failed to obtain Mendeley access token"));
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +219,7 @@ const obtainReferenceManagerToken = async (login, password) => {
|
||||||
|
|
||||||
browser.load("https://www.mendeley.com/sign-in?routeTo=https://www.mendeley.com/reference-manager/library/");
|
browser.load("https://www.mendeley.com/sign-in?routeTo=https://www.mendeley.com/reference-manager/library/");
|
||||||
Zotero.Promise.delay(ACCESS_TOKEN_TIMEOUT).then(() => {
|
Zotero.Promise.delay(ACCESS_TOKEN_TIMEOUT).then(() => {
|
||||||
|
browser.destroy();
|
||||||
reject(new Error("Timed out while obtaining Mendeley access token"));
|
reject(new Error("Timed out while obtaining Mendeley access token"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ var Zotero_Import_Mendeley = function () {
|
||||||
|
|
||||||
this._tokens = null;
|
this._tokens = null;
|
||||||
this._credentials = null;
|
this._credentials = null;
|
||||||
|
this._refManagerToken = null;
|
||||||
this._db = null;
|
this._db = null;
|
||||||
this._file = null;
|
this._file = null;
|
||||||
this._saveOptions = null;
|
this._saveOptions = null;
|
||||||
|
|
@ -327,15 +328,18 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._credentials && !this.skipNotebooks) {
|
if (this._credentials && !this.skipNotebooks) {
|
||||||
const token = await obtainReferenceManagerTokenWithRetry(this._credentials.username, this._credentials.password);
|
if (!this._refManagerToken) {
|
||||||
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 4 more left
|
const token = await obtainReferenceManagerTokenWithRetry(this._credentials.username, this._credentials.password);
|
||||||
if (token) {
|
|
||||||
this._refManagerToken = {
|
this._refManagerToken = {
|
||||||
kind: 'referenceManager',
|
kind: 'referenceManager',
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
username: this._credentials.username,
|
username: this._credentials.username,
|
||||||
password: this._credentials.password
|
password: this._credentials.password
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 4 more left
|
||||||
|
if (this._refManagerToken) {
|
||||||
const notebooks = await this._getNotebooksAPI();
|
const notebooks = await this._getNotebooksAPI();
|
||||||
const notesContent = await Promise.all(notebooks.map(notebook => this._translateNotebookToNoteContent(libraryID, notebook)));
|
const notesContent = await Promise.all(notebooks.map(notebook => this._translateNotebookToNoteContent(libraryID, notebook)));
|
||||||
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 1 more left
|
this._progress += 1; // progress one arbitrary "tick" assigned to importing notebooks task, we have 1 more left
|
||||||
|
|
@ -353,7 +357,7 @@ Zotero_Import_Mendeley.prototype.translate = async function (options = {}) {
|
||||||
|
|
||||||
const isMappedToExisting = !!existingItem;
|
const isMappedToExisting = !!existingItem;
|
||||||
Zotero.debug(isMappedToExisting ? `Updating existing notebook "${uuid}" -> "${existingItem.key}"` : `Importing new notebook "${uuid}"`, 5);
|
Zotero.debug(isMappedToExisting ? `Updating existing notebook "${uuid}" -> "${existingItem.key}"` : `Importing new notebook "${uuid}"`, 5);
|
||||||
let item = existingItem ?? new Zotero.Item('note');
|
let item = isMappedToExisting ? existingItem : new Zotero.Item('note');
|
||||||
item.libraryID = libraryID;
|
item.libraryID = libraryID;
|
||||||
item.setNote(noteContent);
|
item.setNote(noteContent);
|
||||||
item.addRelation(predicate, uuid);
|
item.addRelation(predicate, uuid);
|
||||||
|
|
@ -1085,14 +1089,15 @@ Zotero_Import_Mendeley.prototype._translateNotebookToNoteContent = async functio
|
||||||
'mendeleyDB:annotationUUID',
|
'mendeleyDB:annotationUUID',
|
||||||
annotationUUID
|
annotationUUID
|
||||||
);
|
);
|
||||||
|
if (annotation) {
|
||||||
|
let attachmentItem = Zotero.Items.get(annotation.parentID);
|
||||||
|
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
|
||||||
|
jsonAnnotation.attachmentItemID = attachmentItem.id;
|
||||||
|
jsonAnnotation.id = annotation.key;
|
||||||
|
|
||||||
let attachmentItem = Zotero.Items.get(annotation.parentID);
|
const { html } = Zotero.EditorInstanceUtilities.serializeAnnotations([jsonAnnotation]);
|
||||||
let jsonAnnotation = await Zotero.Annotations.toJSON(annotation);
|
return html;
|
||||||
jsonAnnotation.attachmentItemID = attachmentItem.id;
|
}
|
||||||
jsonAnnotation.id = annotation.key;
|
|
||||||
|
|
||||||
const { html } = Zotero.EditorInstanceUtilities.serializeAnnotations([jsonAnnotation]);
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1745,6 +1750,11 @@ Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mendeley produces empty, type-less annotations that confuse the PDF worker
|
||||||
|
annotations = annotations.filter(
|
||||||
|
annotation => ['highlight', 'note', 'image', 'ink', 'underline', 'text'].includes(annotation.type)
|
||||||
|
);
|
||||||
|
|
||||||
let annotationMap = new Map();
|
let annotationMap = new Map();
|
||||||
for (let annotation of annotations) {
|
for (let annotation of annotations) {
|
||||||
// 'type', 'uuid', 'hash', 'color', 'dateAdded', 'page'
|
// 'type', 'uuid', 'hash', 'color', 'dateAdded', 'page'
|
||||||
|
|
@ -1766,6 +1776,7 @@ Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations,
|
||||||
for (let annotation of annotations) {
|
for (let annotation of annotations) {
|
||||||
// Ignore empty highlights
|
// Ignore empty highlights
|
||||||
if (annotation.type == 'highlight' && !annotation.text) {
|
if (annotation.type == 'highlight' && !annotation.text) {
|
||||||
|
Zotero.debug(`Skipping empty highlight with uuid ${annotation.uuid}`, 5);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2287,7 +2287,8 @@ var Zotero_QuickFormat = new function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Arrow up closes the popup
|
// Arrow up closes the popup
|
||||||
if (event.key === "ArrowUp") {
|
// (except on locator type dropdown to allow for default handling)
|
||||||
|
if (event.key === "ArrowUp" && event.target.tagName !== "menulist") {
|
||||||
skipInputRefocus = true;
|
skipInputRefocus = true;
|
||||||
itemPopover.hidePopup();
|
itemPopover.hidePopup();
|
||||||
return stopEvent();
|
return stopEvent();
|
||||||
|
|
|
||||||
|
|
@ -345,25 +345,13 @@ var Zotero_LocateMenu = new function() {
|
||||||
* Get the first 50 selected items
|
* Get the first 50 selected items
|
||||||
*/
|
*/
|
||||||
function _getSelectedItems() {
|
function _getSelectedItems() {
|
||||||
if (Zotero_Tabs.selectedID == 'zotero-pane') {
|
var allSelectedItems = ZoteroPane.getSelectedItems();
|
||||||
var allSelectedItems = ZoteroPane_Local.getSelectedItems();
|
var selectedItems = [];
|
||||||
var selectedItems = [];
|
while (selectedItems.length < 50 && allSelectedItems.length) {
|
||||||
while (selectedItems.length < 50 && allSelectedItems.length) {
|
var item = allSelectedItems.shift();
|
||||||
var item = allSelectedItems.shift();
|
if (!item.isNote()) selectedItems.push(item);
|
||||||
if (!item.isNote()) selectedItems.push(item);
|
|
||||||
}
|
|
||||||
return selectedItems;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
|
||||||
if (reader) {
|
|
||||||
let item = Zotero.Items.get(reader.itemID);
|
|
||||||
if (item.parentItem) {
|
|
||||||
return [item.parentItem];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
return selectedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ViewOptions = {};
|
var ViewOptions = {};
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ Zotero_Preferences.Sync = {
|
||||||
noChar: '\uD83D\uDEAB',
|
noChar: '\uD83D\uDEAB',
|
||||||
|
|
||||||
init: async function () {
|
init: async function () {
|
||||||
|
this.storeLastStorageSettings();
|
||||||
this.updateStorageSettingsUI();
|
this.updateStorageSettingsUI();
|
||||||
this.updateStorageSettingsGroupsUI();
|
this.updateStorageSettingsGroupsUI();
|
||||||
|
|
||||||
|
|
@ -427,6 +428,16 @@ Zotero_Preferences.Sync = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
_lastStorageProtocol: null,
|
||||||
|
_lastStorageURL: null,
|
||||||
|
|
||||||
|
storeLastStorageSettings: function () {
|
||||||
|
this._lastStorageProtocol = Zotero.Prefs.get('sync.storage.protocol');
|
||||||
|
this._lastStorageURL = Zotero.Prefs.get('sync.storage.url');
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
updateStorageSettingsUI: Zotero.Promise.coroutine(function* () {
|
updateStorageSettingsUI: Zotero.Promise.coroutine(function* () {
|
||||||
this.unverifyStorageServer();
|
this.unverifyStorageServer();
|
||||||
|
|
||||||
|
|
@ -480,17 +491,19 @@ Zotero_Preferences.Sync = {
|
||||||
|
|
||||||
|
|
||||||
onStorageSettingsChange: Zotero.Promise.coroutine(function* () {
|
onStorageSettingsChange: Zotero.Promise.coroutine(function* () {
|
||||||
// Clean URL
|
var oldProtocol = this._lastStorageProtocol;
|
||||||
Zotero.Prefs.set('sync.storage.url', Zotero.Prefs.get('sync.storage.url').trim()
|
var oldURL = this._lastStorageURL;
|
||||||
.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, ''));
|
|
||||||
|
|
||||||
var oldProtocol = Zotero.Prefs.get('sync.storage.protocol');
|
|
||||||
|
|
||||||
|
// Necessary for pref to update
|
||||||
yield Zotero.Promise.delay(1);
|
yield Zotero.Promise.delay(1);
|
||||||
|
|
||||||
var newProtocol = Zotero.Prefs.get('sync.storage.protocol');
|
var newProtocol = Zotero.Prefs.get('sync.storage.protocol');
|
||||||
|
|
||||||
if (oldProtocol != newProtocol) {
|
var newURL = Zotero.Prefs.get('sync.storage.url').trim()
|
||||||
|
// Strip scheme, leading '://' or '//' (#3483), and trailing '/zotero'
|
||||||
|
.replace(/(^https?:\/\/|^:?\/\/|\/zotero\/?$|\/$)/g, '')
|
||||||
|
Zotero.Prefs.set('sync.storage.url', newURL);
|
||||||
|
|
||||||
|
if (oldProtocol != newProtocol || oldURL != newURL) {
|
||||||
yield Zotero.Sync.Storage.Local.resetAllSyncStates(Zotero.Libraries.userLibraryID);
|
yield Zotero.Sync.Storage.Local.resetAllSyncStates(Zotero.Libraries.userLibraryID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,6 +560,7 @@ Zotero_Preferences.Sync = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateStorageSettingsUI();
|
this.updateStorageSettingsUI();
|
||||||
|
this.storeLastStorageSettings();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<html id="run-js">
|
<html id="run-js" windowtype="zotero:run-js">
|
||||||
<head>
|
<head>
|
||||||
<title data-l10n-id="runJS-title"></title>
|
<title data-l10n-id="runJS-title"></title>
|
||||||
<link href="chrome://zotero-platform/content/zotero.css" rel="stylesheet"/>
|
<link href="chrome://zotero-platform/content/zotero.css" rel="stylesheet"/>
|
||||||
|
|
|
||||||
|
|
@ -309,23 +309,12 @@ const ZoteroStandalone = new function() {
|
||||||
|
|
||||||
let win = Zotero.getMainWindow();
|
let win = Zotero.getMainWindow();
|
||||||
if (win) {
|
if (win) {
|
||||||
if (win.Zotero_Tabs.selectedID == 'zotero-pane') {
|
try {
|
||||||
try {
|
selected = win.ZoteroPane.getSelectedItems();
|
||||||
selected = win.ZoteroPane.getSelectedItems();
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
}
|
|
||||||
win.ZoteroPane.updateQuickCopyCommands(selected);
|
|
||||||
}
|
}
|
||||||
else {
|
catch (e) {
|
||||||
let reader = Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID);
|
|
||||||
if (reader) {
|
|
||||||
let item = Zotero.Items.get(reader.itemID);
|
|
||||||
selected = item.parentItem && [item.parentItem] || [];
|
|
||||||
item = item.parentItem || item;
|
|
||||||
win.ZoteroPane.updateQuickCopyCommands([item]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
win.ZoteroPane.updateQuickCopyCommands(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
|
var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,8 @@ var Zotero_Tabs = new function () {
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
tabIndex: tab.index,
|
tabIndex: tab.index,
|
||||||
openInBackground: true
|
openInBackground: true,
|
||||||
|
allowDuplicate: true
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
if (tab.index > maxIndex) {
|
if (tab.index > maxIndex) {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
id="csl-edit"
|
id="csl-edit"
|
||||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||||
|
windowtype="zotero:style-editor"
|
||||||
onload="Zotero_CSL_Editor.init();"
|
onload="Zotero_CSL_Editor.init();"
|
||||||
title="&styles.editor;">
|
title="&styles.editor;">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -921,20 +921,7 @@ Zotero.Attachments = new function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Zotero.debug("Saving file with saveURI()");
|
yield Zotero.HTTP.download(url, tmpFile);
|
||||||
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
|
|
||||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
|
||||||
.createInstance(nsIWBP);
|
|
||||||
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_FROM_CACHE;
|
|
||||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
|
||||||
.getService(Components.interfaces.nsIIOService);
|
|
||||||
var nsIURL = ioService.newURI(url, null, null);
|
|
||||||
var deferred = Zotero.Promise.defer();
|
|
||||||
wbp.progressListener = new Zotero.WebProgressFinishListener(function () {
|
|
||||||
deferred.resolve();
|
|
||||||
});
|
|
||||||
Zotero.Utilities.Internal.saveURI(wbp, nsIURL, tmpFile);
|
|
||||||
yield deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachmentItem;
|
var attachmentItem;
|
||||||
|
|
@ -1100,20 +1087,18 @@ Zotero.Attachments = new function () {
|
||||||
|
|
||||||
let enforcingFileType = false;
|
let enforcingFileType = false;
|
||||||
try {
|
try {
|
||||||
await new Zotero.Promise(function (resolve) {
|
let headers = {};
|
||||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
if (options.referrer) {
|
||||||
.createInstance(Components.interfaces.nsIWebBrowserPersist);
|
headers.Referer = options.referrer;
|
||||||
if (options.cookieSandbox) {
|
}
|
||||||
options.cookieSandbox.attachToInterfaceRequestor(wbp);
|
await Zotero.HTTP.download(
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
cookieSandbox: options.cookieSandbox
|
||||||
}
|
}
|
||||||
|
);
|
||||||
wbp.progressListener = new Zotero.WebProgressFinishListener(() => resolve());
|
|
||||||
var headers = {};
|
|
||||||
if (options.referrer) {
|
|
||||||
headers.Referer = options.referrer;
|
|
||||||
}
|
|
||||||
Zotero.Utilities.Internal.saveURI(wbp, url, path, headers);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.enforceFileType) {
|
if (options.enforceFileType) {
|
||||||
enforcingFileType = true;
|
enforcingFileType = true;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const { HiddenBrowser } = ChromeUtils.import("chrome://zotero/content/HiddenBrow
|
||||||
Zotero.BrowserDownload = {
|
Zotero.BrowserDownload = {
|
||||||
HANDLED_URLS: {
|
HANDLED_URLS: {
|
||||||
'https://zotero-static.s3.amazonaws.com/test-pdf-redirect.html': "html",
|
'https://zotero-static.s3.amazonaws.com/test-pdf-redirect.html': "html",
|
||||||
'://www.sciencedirect.com': "#challenge-form"
|
'://www.sciencedirect.com': "#captcha-box"
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -343,11 +343,15 @@ Zotero.CookieSandbox.Observer = new function() {
|
||||||
// Zotero.debug(`CookieSandbox: Found the browser via doc of load group for ${channelURI}`, 5);
|
// Zotero.debug(`CookieSandbox: Found the browser via doc of load group for ${channelURI}`, 5);
|
||||||
trackedBy = this.trackedBrowsers.get(browser);
|
trackedBy = this.trackedBrowsers.get(browser);
|
||||||
}
|
}
|
||||||
|
else if (notificationCallbacks instanceof XMLHttpRequest) {
|
||||||
|
// Zotero.debug(`CookieSandbox: Found the browser via XHR for ${channelURI}`, 5);
|
||||||
|
tested = true;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// try getting as an XHR or nsIWBP
|
// try getting as an nsIWBP
|
||||||
try {
|
try {
|
||||||
notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest);
|
notificationCallbacks.QueryInterface(Components.interfaces.nsIWebBrowserPersist);
|
||||||
// Zotero.debug(`CookieSandbox: Found the browser via XHR or nsIWBP for ${channelURI}`, 5);
|
// Zotero.debug(`CookieSandbox: Found the browser via nsIWBP for ${channelURI}`, 5);
|
||||||
tested = true;
|
tested = true;
|
||||||
}
|
}
|
||||||
catch (e) {}
|
catch (e) {}
|
||||||
|
|
|
||||||
|
|
@ -2183,6 +2183,22 @@ Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the item or any of its ancestors is in the trash
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
Zotero.Item.prototype.isInTrash = function () {
|
||||||
|
if (this.deleted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.isTopLevelItem()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.parentItem.isInTrash();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|
@ -2540,12 +2556,15 @@ Zotero.Item.prototype.numNonHTMLFileAttachments = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Zotero.Item.prototype.numFileAttachmentsWithContentType = function (contentType) {
|
Zotero.Item.prototype.getFileAttachmentsWithContentType = function (contentType) {
|
||||||
this._requireData('childItems');
|
this._requireData('childItems');
|
||||||
return this.getAttachments()
|
return Zotero.Items.get(this.getAttachments())
|
||||||
.map(itemID => Zotero.Items.get(itemID))
|
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType);
|
||||||
.filter(item => item.isFileAttachment() && item.attachmentContentType == contentType)
|
};
|
||||||
.length;
|
|
||||||
|
|
||||||
|
Zotero.Item.prototype.numFileAttachmentsWithContentType = function (contentType) {
|
||||||
|
return this.getFileAttachmentsWithContentType(contentType).length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3958,7 +3977,8 @@ Zotero.Item.prototype.setAutoAttachmentTitle = function ({ ignoreAutoRenamePrefs
|
||||||
// If this is the only attachment of its type on the parent item and the
|
// If this is the only attachment of its type on the parent item and the
|
||||||
// file is being renamed, give it a default title ("PDF", "Webpage", etc.)
|
// file is being renamed, give it a default title ("PDF", "Webpage", etc.)
|
||||||
let isFirstOfType = this.parentItemID
|
let isFirstOfType = this.parentItemID
|
||||||
&& this.parentItem.numFileAttachmentsWithContentType(this.attachmentContentType) <= 1;
|
&& this.parentItem.getFileAttachmentsWithContentType(this.attachmentContentType)
|
||||||
|
.every(item => item === this);
|
||||||
let isBeingRenamed = ignoreAutoRenamePrefs || Zotero.Attachments.shouldAutoRenameAttachment(this);
|
let isBeingRenamed = ignoreAutoRenamePrefs || Zotero.Attachments.shouldAutoRenameAttachment(this);
|
||||||
if (isFirstOfType && isBeingRenamed) {
|
if (isFirstOfType && isBeingRenamed) {
|
||||||
let defaultTitle = this._getDefaultTitleForAttachmentContentType();
|
let defaultTitle = this._getDefaultTitleForAttachmentContentType();
|
||||||
|
|
@ -5124,6 +5144,7 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
||||||
*/
|
*/
|
||||||
Zotero.Item.prototype.fromJSON = function (json, options = {}) {
|
Zotero.Item.prototype.fromJSON = function (json, options = {}) {
|
||||||
var strict = !!options.strict;
|
var strict = !!options.strict;
|
||||||
|
var migrateExtra = !strict;
|
||||||
|
|
||||||
if (!json.itemType && !this._itemTypeID) {
|
if (!json.itemType && !this._itemTypeID) {
|
||||||
throw new Error("itemType property not provided");
|
throw new Error("itemType property not provided");
|
||||||
|
|
@ -5139,29 +5160,34 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
|
||||||
|
|
||||||
var isValidForType = {};
|
var isValidForType = {};
|
||||||
var setFields = new Set();
|
var setFields = new Set();
|
||||||
var { itemType, fields: extraFields, creators: extraCreators, extra } =
|
|
||||||
Zotero.Utilities.Internal.extractExtraFields(
|
var extraFields = new Map();
|
||||||
json.extra || '',
|
var extraCreators = [];
|
||||||
this,
|
var extra = json.extra || '';
|
||||||
Object.keys(json)
|
// Transfer valid fields from Extra to regular fields
|
||||||
// TEMP until we move creator lines to real creators
|
if (migrateExtra) {
|
||||||
.concat('creators')
|
let itemType;
|
||||||
);
|
({ itemType, fields: extraFields, creators: extraCreators, extra } =
|
||||||
// If a different item type was parsed out of Extra, use that instead
|
Zotero.Utilities.Internal.extractExtraFields(
|
||||||
if (itemType && json.itemType != itemType) {
|
extra,
|
||||||
itemTypeID = Zotero.ItemTypes.getID(itemType);
|
this,
|
||||||
this.setType(itemTypeID);
|
Object.keys(json)
|
||||||
|
// TEMP until we move creator lines to real creators
|
||||||
|
.concat('creators')
|
||||||
|
));
|
||||||
|
// If a different item type was parsed out of Extra, use that instead
|
||||||
|
if (itemType && json.itemType != itemType) {
|
||||||
|
itemTypeID = Zotero.ItemTypes.getID(itemType);
|
||||||
|
this.setType(itemTypeID);
|
||||||
|
}
|
||||||
|
for (let [field, value] of extraFields) {
|
||||||
|
this.setField(field, value);
|
||||||
|
setFields.add(field);
|
||||||
|
extraFields.delete(field);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var invalidFieldLogLines = new Map();
|
var invalidFieldLogLines = new Map();
|
||||||
|
|
||||||
// Transfer valid fields from Extra to regular fields
|
|
||||||
// Currently disabled
|
|
||||||
/*for (let [field, value] of extraFields) {
|
|
||||||
this.setField(field, value);
|
|
||||||
setFields.add(field);
|
|
||||||
extraFields.delete(field);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
for (let field in json) {
|
for (let field in json) {
|
||||||
let val = json[field];
|
let val = json[field];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1653,19 +1653,19 @@ Zotero.Items = function() {
|
||||||
/**
|
/**
|
||||||
* Purge unused data values
|
* Purge unused data values
|
||||||
*/
|
*/
|
||||||
this.purge = Zotero.Promise.coroutine(function* () {
|
this.purge = async function () {
|
||||||
Zotero.DB.requireTransaction();
|
|
||||||
|
|
||||||
if (!Zotero.Prefs.get('purge.items')) {
|
if (!Zotero.Prefs.get('purge.items')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN "
|
await Zotero.DB.executeTransaction(async function () {
|
||||||
+ "(SELECT valueID FROM itemData)";
|
let sql = "DELETE FROM itemDataValues WHERE valueID NOT IN "
|
||||||
yield Zotero.DB.queryAsync(sql);
|
+ "(SELECT valueID FROM itemData)";
|
||||||
|
await Zotero.DB.queryAsync(sql, [], { ignoreDBLock: true });
|
||||||
|
}, { disableForeignKeys: true });
|
||||||
|
|
||||||
Zotero.Prefs.set('purge.items', false)
|
Zotero.Prefs.set('purge.items', false)
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,10 +291,12 @@ Zotero.Library.prototype._set = function(prop, val) {
|
||||||
if (newVal != val) {
|
if (newVal != val) {
|
||||||
throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`);
|
throw new Error(`${prop} must be an integer (${typeof val} '${val}' given)`);
|
||||||
}
|
}
|
||||||
|
// Ensure that it is never decreasing, unless it is being set to -1
|
||||||
|
// by Reset File Sync History
|
||||||
|
if (val != -1 && val < this._libraryStorageVersion) {
|
||||||
|
throw new Error(prop + ' cannot decrease');
|
||||||
|
}
|
||||||
val = newVal;
|
val = newVal;
|
||||||
|
|
||||||
// Ensure that it is never decreasing
|
|
||||||
if (val < this._libraryStorageVersion) throw new Error(prop + ' cannot decrease');
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '_libraryLastSync':
|
case '_libraryLastSync':
|
||||||
|
|
|
||||||
|
|
@ -418,12 +418,14 @@ Zotero.DBConnection.prototype.getNextName = async function (libraryID, table, fi
|
||||||
/**
|
/**
|
||||||
* @param {Function} func - Async function containing `await Zotero.DB.queryAsync()` and similar
|
* @param {Function} func - Async function containing `await Zotero.DB.queryAsync()` and similar
|
||||||
* @param {Object} [options]
|
* @param {Object} [options]
|
||||||
* @param {Boolean} [options.disableForeignKeys] - Disable foreign key constraints before
|
* @param {Boolean} [options.disableForeignKeys] - Disable foreign key checks before the
|
||||||
* transaction and re-enable after. (`PRAGMA foreign_keys=0|1` is a no-op during a transaction.)
|
* transaction and re-enable after, while preventing any other queries from running.
|
||||||
|
* `queryAsync()` and similar within `func` must pass `ignoreDBLock: true` or they'll hang.
|
||||||
|
* (`PRAGMA foreign_keys=OFF|ON` is a no-op during a transaction, so it can't just be set within
|
||||||
|
* the function.)
|
||||||
* @return {Promise} - Promise for result of generator function
|
* @return {Promise} - Promise for result of generator function
|
||||||
*/
|
*/
|
||||||
Zotero.DBConnection.prototype.executeTransaction = async function (func, options) {
|
Zotero.DBConnection.prototype.executeTransaction = async function (func, options = {}) {
|
||||||
options = options || {};
|
|
||||||
var resolve;
|
var resolve;
|
||||||
|
|
||||||
// Set temporary options for this transaction that will be reset at the end
|
// Set temporary options for this transaction that will be reset at the end
|
||||||
|
|
@ -461,34 +463,49 @@ Zotero.DBConnection.prototype.executeTransaction = async function (func, options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.disableForeignKeys) {
|
let result;
|
||||||
await this.queryAsync("PRAGMA foreign_keys = 0");
|
let resolveDBLockPromise;
|
||||||
|
try {
|
||||||
|
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
||||||
|
|
||||||
|
if (func.constructor.name == 'GeneratorFunction') {
|
||||||
|
throw new Error("Zotero.DB.executeTransaction() no longer takes a generator function "
|
||||||
|
+ "-- pass an async function instead");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.disableForeignKeys) {
|
||||||
|
this._dbLockPromise = new Promise(function () {
|
||||||
|
resolveDBLockPromise = arguments[0];
|
||||||
|
});
|
||||||
|
await this.queryAsync("PRAGMA foreign_keys=OFF", [], { ignoreDBLock: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await conn.executeTransaction(func);
|
||||||
|
Zotero.debug(`Committed DB transaction ${id}`, 4);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
var conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
if (options.disableForeignKeys) {
|
||||||
|
await this.queryAsync("PRAGMA foreign_keys=ON", [], { ignoreDBLock: true });
|
||||||
if (func.constructor.name == 'GeneratorFunction') {
|
if (resolveDBLockPromise) {
|
||||||
throw new Error("Zotero.DB.executeTransaction() no longer takes a generator function "
|
resolveDBLockPromise();
|
||||||
+ "-- pass an async function instead");
|
this._dbLockPromise = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await conn.executeTransaction(func);
|
|
||||||
Zotero.debug(`Committed DB transaction ${id}`, 4);
|
|
||||||
|
|
||||||
// Clear transaction time
|
// Clear transaction time
|
||||||
if (this._transactionDate) {
|
if (this._transactionDate) {
|
||||||
this._transactionDate = null;
|
this._transactionDate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._transactionID = null;
|
||||||
|
|
||||||
if (options.vacuumOnCommit) {
|
if (options.vacuumOnCommit) {
|
||||||
Zotero.debug('Vacuuming database');
|
Zotero.debug('Vacuuming database');
|
||||||
await this.queryAsync('VACUUM');
|
await this.queryAsync('VACUUM');
|
||||||
Zotero.debug('Done vacuuming');
|
Zotero.debug('Done vacuuming');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._transactionID = null;
|
|
||||||
|
|
||||||
// Function to run once transaction has been committed but before any
|
// Function to run once transaction has been committed but before any
|
||||||
// permanent callbacks
|
// permanent callbacks
|
||||||
if (options.onCommit) {
|
if (options.onCommit) {
|
||||||
|
|
@ -545,10 +562,6 @@ Zotero.DBConnection.prototype.executeTransaction = async function (func, options
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
if (options.disableForeignKeys) {
|
|
||||||
await this.queryAsync("PRAGMA foreign_keys = 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset options back to their previous values
|
// Reset options back to their previous values
|
||||||
if (options) {
|
if (options) {
|
||||||
for (let option in options) {
|
for (let option in options) {
|
||||||
|
|
@ -596,13 +609,19 @@ Zotero.DBConnection.prototype.requireTransaction = function () {
|
||||||
* rows are Proxy objects that return values from the
|
* rows are Proxy objects that return values from the
|
||||||
* underlying mozIStorageRows based on column names.
|
* underlying mozIStorageRows based on column names.
|
||||||
*/
|
*/
|
||||||
Zotero.DBConnection.prototype.queryAsync = async function (sql, params, options) {
|
Zotero.DBConnection.prototype.queryAsync = async function (sql, params, options = {}) {
|
||||||
try {
|
try {
|
||||||
let onRow = null;
|
let onRow = null;
|
||||||
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
||||||
if (!options || !options.noParseParams) {
|
if (!options || !options.noParseParams) {
|
||||||
[sql, params] = this.parseQueryAndParams(sql, params);
|
[sql, params] = this.parseQueryAndParams(sql, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._dbLockPromise && !options.ignoreDBLock) {
|
||||||
|
Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2);
|
||||||
|
await this._dbLockPromise;
|
||||||
|
}
|
||||||
|
|
||||||
if (Zotero.Debug.enabled) {
|
if (Zotero.Debug.enabled) {
|
||||||
this.logQuery(sql, params, options);
|
this.logQuery(sql, params, options);
|
||||||
}
|
}
|
||||||
|
|
@ -718,6 +737,12 @@ Zotero.DBConnection.prototype.valueQueryAsync = async function (sql, params, opt
|
||||||
try {
|
try {
|
||||||
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
||||||
[sql, params] = this.parseQueryAndParams(sql, params);
|
[sql, params] = this.parseQueryAndParams(sql, params);
|
||||||
|
|
||||||
|
if (this._dbLockPromise && !options.ignoreDBLock) {
|
||||||
|
Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2);
|
||||||
|
await this._dbLockPromise;
|
||||||
|
}
|
||||||
|
|
||||||
if (Zotero.Debug.enabled) {
|
if (Zotero.Debug.enabled) {
|
||||||
this.logQuery(sql, params, options);
|
this.logQuery(sql, params, options);
|
||||||
}
|
}
|
||||||
|
|
@ -765,6 +790,12 @@ Zotero.DBConnection.prototype.columnQueryAsync = async function (sql, params, op
|
||||||
try {
|
try {
|
||||||
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
let conn = this._getConnection(options) || (await this._getConnectionAsync(options));
|
||||||
[sql, params] = this.parseQueryAndParams(sql, params);
|
[sql, params] = this.parseQueryAndParams(sql, params);
|
||||||
|
|
||||||
|
if (this._dbLockPromise && !options.ignoreDBLock) {
|
||||||
|
Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2);
|
||||||
|
await this._dbLockPromise;
|
||||||
|
}
|
||||||
|
|
||||||
if (Zotero.Debug.enabled) {
|
if (Zotero.Debug.enabled) {
|
||||||
this.logQuery(sql, params, options);
|
this.logQuery(sql, params, options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -448,16 +448,15 @@ Zotero.File = new function(){
|
||||||
|
|
||||||
this.download = async function (uri, path) {
|
this.download = async function (uri, path) {
|
||||||
var uriStr = uri.spec || uri;
|
var uriStr = uri.spec || uri;
|
||||||
|
|
||||||
const isHTTP = uriStr.startsWith('http');
|
const isHTTP = uriStr.startsWith('http');
|
||||||
|
if (uriStr.startsWith('http')) {
|
||||||
|
Zotero.warn("Zotero.File.download() is deprecated for HTTP(S) URLs -- use Zotero.HTTP.download()");
|
||||||
|
return Zotero.HTTP.download(uri, path);
|
||||||
|
}
|
||||||
|
|
||||||
Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`);
|
Zotero.debug(`Saving ${uriStr} to ${path.pathQueryRef || path}`);
|
||||||
|
|
||||||
if (isHTTP && Zotero.HTTP.browserIsOffline()) {
|
|
||||||
let msg = `Download failed: ${Zotero.appName} is currently offline`;
|
|
||||||
Zotero.debug(msg, 2);
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var deferred = Zotero.Promise.defer();
|
var deferred = Zotero.Promise.defer();
|
||||||
const inputChannel = NetUtil.newChannel({
|
const inputChannel = NetUtil.newChannel({
|
||||||
uri,
|
uri,
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ Zotero.FileHandlers = {
|
||||||
handler = handler || systemHandler;
|
handler = handler || systemHandler;
|
||||||
if (handler) {
|
if (handler) {
|
||||||
if (Zotero.isMac) {
|
if (Zotero.isMac) {
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
try {
|
try {
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
|
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -156,6 +157,9 @@ Zotero.FileHandlers = {
|
||||||
catch (e) {
|
catch (e) {
|
||||||
Zotero.logError(e);
|
Zotero.logError(e);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -498,8 +502,13 @@ Zotero.FileHandlers = {
|
||||||
if (!Zotero.File.pathToFile(command).isExecutable()) {
|
if (!Zotero.File.pathToFile(command).isExecutable()) {
|
||||||
throw new Error(`${command} is not an executable`);
|
throw new Error(`${command} is not an executable`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
|
|
||||||
// Do not await
|
// Do not await
|
||||||
Zotero.Utilities.Internal.exec(command, args);
|
var promise = Zotero.Utilities.Internal.exec(command, args);
|
||||||
|
|
||||||
|
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* @namespace
|
* @namespace
|
||||||
*/
|
*/
|
||||||
Zotero.HTTP = new function() {
|
Zotero.HTTP = new function() {
|
||||||
|
this.disableErrorRetry = false;
|
||||||
var _errorDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000];
|
var _errorDelayIntervals = [2500, 5000, 10000, 20000, 40000, 60000, 120000, 240000, 300000];
|
||||||
var _errorDelayMax = 60 * 60 * 1000; // 1 hour
|
var _errorDelayMax = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
|
@ -169,7 +170,7 @@ Zotero.HTTP = new function() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Don't retry if errorDelayMax is 0
|
// Don't retry if errorDelayMax is 0
|
||||||
if (options.errorDelayMax === 0) {
|
if (options.errorDelayMax === 0 || Zotero.HTTP.disableErrorRetry) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
// Automatically retry other 5xx errors by default
|
// Automatically retry other 5xx errors by default
|
||||||
|
|
@ -269,7 +270,10 @@ Zotero.HTTP = new function() {
|
||||||
|
|
||||||
var deferred = Zotero.Promise.defer();
|
var deferred = Zotero.Promise.defer();
|
||||||
|
|
||||||
if (!this.mock || url.startsWith('resource://') || url.startsWith('chrome://')) {
|
if (!this.mock
|
||||||
|
|| options.noMock
|
||||||
|
|| url.startsWith('resource://')
|
||||||
|
|| url.startsWith('chrome://')) {
|
||||||
var xmlhttp = new XMLHttpRequest();
|
var xmlhttp = new XMLHttpRequest();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -324,11 +328,13 @@ Zotero.HTTP = new function() {
|
||||||
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
|
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't follow redirects
|
let notificationCallbacks = options.notificationCallbacks || {};
|
||||||
if (options.followRedirects === false) {
|
if (options.followRedirects === false) {
|
||||||
channel.notificationCallbacks = {
|
if (notificationCallbacks.asyncOnChannelRedirect) {
|
||||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSync]),
|
throw new Error("Can't set asyncOnChannelRedirect and followRedirects = false");
|
||||||
getInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
|
}
|
||||||
|
notificationCallbacks = {
|
||||||
|
...notificationCallbacks,
|
||||||
asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
|
asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
|
||||||
redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302;
|
redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302;
|
||||||
redirectLocation = newChannel.URI.spec;
|
redirectLocation = newChannel.URI.spec;
|
||||||
|
|
@ -337,6 +343,9 @@ Zotero.HTTP = new function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (notificationCallbacks) {
|
||||||
|
channel.notificationCallbacks = wrapNotificationCallbacks(notificationCallbacks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set responseType
|
// Set responseType
|
||||||
|
|
@ -381,14 +390,53 @@ Zotero.HTTP = new function() {
|
||||||
xmlhttp.setRequestHeader(header, value);
|
xmlhttp.setRequestHeader(header, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set timeout
|
const defaultTimeout = 30000;
|
||||||
|
let requestTimeout;
|
||||||
|
let connectTimeout;
|
||||||
|
let inactivityTimeout;
|
||||||
if (options.timeout !== 0) {
|
if (options.timeout !== 0) {
|
||||||
xmlhttp.timeout = options.timeout || 30000;
|
// For downloads, manually implement connect and inactivity timeouts, since the XHR
|
||||||
|
// `timeout` property applies to the whole request, even if data is being downloaded
|
||||||
|
if (options.isDownload) {
|
||||||
|
// TODO: Try a lower default connect timeout and take a separate option?
|
||||||
|
connectTimeout = options.timeout || defaultTimeout;
|
||||||
|
inactivityTimeout = options.timeout || defaultTimeout;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
requestTimeout = options.timeout || defaultTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let connectTimerID = null;
|
||||||
|
let inactivityTimerID = null;
|
||||||
|
let timedOutAfter;
|
||||||
|
|
||||||
|
function clearConnectTimer() {
|
||||||
|
if (connectTimerID) {
|
||||||
|
clearTimeout(connectTimerID);
|
||||||
|
connectTimerID = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xmlhttp.ontimeout = function() {
|
function resetInactivityTimer() {
|
||||||
deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout));
|
clearTimeout(inactivityTimerID);
|
||||||
};
|
inactivityTimerID = setTimeout(() => {
|
||||||
|
Zotero.warn(`Inactivity timeout for ${method} ${dispURL} -- aborting request`);
|
||||||
|
timedOutAfter = inactivityTimeout;
|
||||||
|
xmlhttp.abort();
|
||||||
|
}, inactivityTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestTimeout) {
|
||||||
|
xmlhttp.timeout = requestTimeout;
|
||||||
|
xmlhttp.ontimeout = function() {
|
||||||
|
deferred.reject(new Zotero.HTTP.TimeoutException(requestTimeout));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (inactivityTimeout) {
|
||||||
|
xmlhttp.onprogress = function () {
|
||||||
|
resetInactivityTimer();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Provide caller with a callback to cancel a request in progress
|
// Provide caller with a callback to cancel a request in progress
|
||||||
if (options.cancellerReceiver) {
|
if (options.cancellerReceiver) {
|
||||||
|
|
@ -402,7 +450,17 @@ Zotero.HTTP = new function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connectTimeout || inactivityTimeout) {
|
||||||
|
xmlhttp.onloadstart = () => {
|
||||||
|
clearConnectTimer();
|
||||||
|
resetInactivityTimer();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
xmlhttp.onloadend = async function() {
|
xmlhttp.onloadend = async function() {
|
||||||
|
clearConnectTimer();
|
||||||
|
clearTimeout(inactivityTimerID);
|
||||||
|
|
||||||
var status = redirectStatus || xmlhttp.status;
|
var status = redirectStatus || xmlhttp.status;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -504,6 +562,11 @@ Zotero.HTTP = new function() {
|
||||||
}
|
}
|
||||||
Zotero.debug(msg, 1);
|
Zotero.debug(msg, 1);
|
||||||
|
|
||||||
|
if (timedOutAfter) {
|
||||||
|
deferred.reject(new Zotero.HTTP.TimeoutException(timedOutAfter));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (xmlhttp.status == 0) {
|
if (xmlhttp.status == 0) {
|
||||||
try {
|
try {
|
||||||
this.checkSecurity(channel, { isProxyAuthRequest: options.isProxyAuthRequest });
|
this.checkSecurity(channel, { isProxyAuthRequest: options.isProxyAuthRequest });
|
||||||
|
|
@ -529,22 +592,92 @@ Zotero.HTTP = new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send binary data
|
// Send binary data
|
||||||
|
let body;
|
||||||
if (compressedBody) {
|
if (compressedBody) {
|
||||||
let numBytes = compressedBody.length;
|
let numBytes = compressedBody.length;
|
||||||
let ui8Data = new Uint8Array(numBytes);
|
let ui8Data = new Uint8Array(numBytes);
|
||||||
for (let i = 0; i < numBytes; i++) {
|
for (let i = 0; i < numBytes; i++) {
|
||||||
ui8Data[i] = compressedBody.charCodeAt(i) & 0xff;
|
ui8Data[i] = compressedBody.charCodeAt(i) & 0xff;
|
||||||
}
|
}
|
||||||
xmlhttp.send(ui8Data);
|
body = ui8Data;
|
||||||
}
|
}
|
||||||
// Send regular request
|
// Send regular request
|
||||||
else {
|
else {
|
||||||
xmlhttp.send(options.body || null);
|
body = options.body || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connectTimeout) {
|
||||||
|
connectTimerID = setTimeout(() => {
|
||||||
|
Zotero.warn(`Connect timeout for ${method} ${dispURL} -- aborting request`);
|
||||||
|
timedOutAfter = connectTimeout;
|
||||||
|
xmlhttp.abort();
|
||||||
|
}, connectTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlhttp.send(body);
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an nsIInterfaceRequestor object based on the callbacks provided
|
||||||
|
*/
|
||||||
|
function wrapNotificationCallbacks(callbacks) {
|
||||||
|
return {
|
||||||
|
QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
|
||||||
|
|
||||||
|
getInterface(aIID) {
|
||||||
|
// Handle nsIProgressEventSink (for onProgress, onStatus)
|
||||||
|
if (aIID.equals(Ci.nsIProgressEventSink) && (callbacks.onProgress || callbacks.onStatus)) {
|
||||||
|
return {
|
||||||
|
onProgress: callbacks.onProgress || function () {},
|
||||||
|
onStatus: callbacks.onStatus || function () {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nsIChannelEventSink (for asyncOnChannelRedirect)
|
||||||
|
if (aIID.equals(Ci.nsIChannelEventSink) && callbacks.asyncOnChannelRedirect) {
|
||||||
|
return {
|
||||||
|
asyncOnChannelRedirect: callbacks.asyncOnChannelRedirect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Components.Exception("No interface available", Cr.NS_ERROR_NO_INTERFACE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file
|
||||||
|
*
|
||||||
|
* @param {nsIURI|String} url - URL to request
|
||||||
|
* @param {String} path - Path to save file to
|
||||||
|
* @param {Object} [options] - See `Zotero.HTTP.request()`
|
||||||
|
*/
|
||||||
|
this.download = async function(uri, path, options = {}) {
|
||||||
|
// TODO: Convert request() to fetch() and use ReadableStream
|
||||||
|
var req = await this.request(
|
||||||
|
'GET',
|
||||||
|
uri,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
isDownload: true,
|
||||||
|
responseType: 'blob',
|
||||||
|
// Downloads can have channel notification callbacks, etc., so always do them for real
|
||||||
|
noMock: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var bytes = new Uint8Array(await req.response.arrayBuffer());
|
||||||
|
await IOUtils.write(path, bytes);
|
||||||
|
var byteLength = bytes.byteLength;
|
||||||
|
|
||||||
|
Zotero.debug(`Saved file to ${path} (${byteLength} byte${byteLength != 1 ? 's' : ''})`);
|
||||||
|
return req;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an HTTP GET request via XMLHTTPRequest
|
* Send an HTTP GET request via XMLHTTPRequest
|
||||||
*
|
*
|
||||||
|
|
@ -1086,9 +1219,19 @@ Zotero.HTTP = new function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
this.getDisplayURI = function (uri) {
|
this.getDisplayURI = function (uri, noCredentials) {
|
||||||
|
if (typeof uri == 'string') {
|
||||||
|
uri = NetUtil.newURI(uri);
|
||||||
|
}
|
||||||
if (!uri.password) return uri;
|
if (!uri.password) return uri;
|
||||||
return uri.mutate().setPassword('********').finalize();
|
uri = uri.mutate();
|
||||||
|
if (noCredentials) {
|
||||||
|
uri.setUserPass('');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uri.setPassword('********');
|
||||||
|
}
|
||||||
|
return uri.finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -329,7 +329,7 @@ Zotero.Integration = new function() {
|
||||||
if (document) {
|
if (document) {
|
||||||
try {
|
try {
|
||||||
await document.cleanup();
|
await document.cleanup();
|
||||||
if (!Zotero.Integration.currentSession._dontActivateDocument) {
|
if (!Zotero.Integration.currentSession?._dontActivateDocument) {
|
||||||
await document.activate();
|
await document.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -899,6 +899,7 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
|
||||||
// Perform noteType or fieldType conversion
|
// Perform noteType or fieldType conversion
|
||||||
let fields = yield this._session.getFields();
|
let fields = yield this._session.getFields();
|
||||||
|
|
||||||
|
const styleHasChanged = oldData.style.styleID != this._session.data.style.styleID;
|
||||||
var convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType;
|
var convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType;
|
||||||
var convertItems = convertBibliographies
|
var convertItems = convertBibliographies
|
||||||
|| oldData.prefs.noteType != this._session.data.prefs.noteType;
|
|| oldData.prefs.noteType != this._session.data.prefs.noteType;
|
||||||
|
|
@ -931,7 +932,7 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
|
||||||
this._session._fields = null;
|
this._session._fields = null;
|
||||||
this._session.ignoreEmptyBibliography = true;
|
this._session.ignoreEmptyBibliography = true;
|
||||||
|
|
||||||
if (this._session.data.prefs.delayCitationUpdates && !fieldsToConvert.length) return;
|
if (this._session.data.prefs.delayCitationUpdates && !fieldsToConvert.length && !styleHasChanged) return;
|
||||||
|
|
||||||
yield this._session.updateFromDocument(FORCE_CITATIONS_RESET_TEXT);
|
yield this._session.updateFromDocument(FORCE_CITATIONS_RESET_TEXT);
|
||||||
return this._session.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true);
|
return this._session.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true);
|
||||||
|
|
|
||||||
|
|
@ -244,12 +244,14 @@ Zotero.Intl = new function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocaleCollation() {
|
function getLocaleCollation() {
|
||||||
|
var naturalSorting = Zotero.Prefs.get('naturalSorting');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're
|
// DEBUG: Is this necessary, or will Intl.Collator just default to the same locales we're
|
||||||
// passing manually?
|
// passing manually?
|
||||||
let locales = Services.locale.appLocalesAsBCP47;
|
let locales = Services.locale.appLocalesAsBCP47;
|
||||||
var collator = new Intl.Collator(locales, {
|
var collator = new Intl.Collator(locales, {
|
||||||
numeric: true,
|
numeric: naturalSorting,
|
||||||
sensitivity: 'base'
|
sensitivity: 'base'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -260,7 +262,7 @@ Zotero.Intl = new function () {
|
||||||
try {
|
try {
|
||||||
Zotero.logError("Falling back to en-US sorting");
|
Zotero.logError("Falling back to en-US sorting");
|
||||||
collator = new Intl.Collator(['en-US'], {
|
collator = new Intl.Collator(['en-US'], {
|
||||||
numeric: true,
|
numeric: naturalSorting,
|
||||||
sensitivity: 'base'
|
sensitivity: 'base'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ Zotero.RecognizeDocument = new function () {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await _processItem(item);
|
let parentItem = await _processItem(item);
|
||||||
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, item.getField('title'));
|
_progressQueue.updateRow(itemID, Zotero.ProgressQueue.ROW_SUCCEEDED, parentItem.getField('title'));
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
Zotero.logError(e);
|
Zotero.logError(e);
|
||||||
|
|
@ -250,7 +250,7 @@ Zotero.RecognizeDocument = new function () {
|
||||||
/**
|
/**
|
||||||
* Processes the item and places it as a children of the new item
|
* Processes the item and places it as a children of the new item
|
||||||
* @param itemID
|
* @param itemID
|
||||||
* @return {Promise}
|
* @return {Promise} A promise that resolves to a newly created, recognized parent item
|
||||||
*/
|
*/
|
||||||
async function _processItem(attachment) {
|
async function _processItem(attachment) {
|
||||||
// Make sure the attachment still doesn't have a parent
|
// Make sure the attachment still doesn't have a parent
|
||||||
|
|
|
||||||
|
|
@ -600,9 +600,15 @@ Zotero.Schema = new function(){
|
||||||
|
|
||||||
|
|
||||||
this._updateGlobalSchemaForTest = async function (schema) {
|
this._updateGlobalSchemaForTest = async function (schema) {
|
||||||
await Zotero.DB.executeTransaction(async function () {
|
await Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
|
||||||
await _updateGlobalSchema(schema);
|
try {
|
||||||
}.bind(this), { disableForeignKeys: true });
|
await Zotero.DB.executeTransaction(async function () {
|
||||||
|
await _updateGlobalSchema(schema);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
await Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -733,58 +739,64 @@ Zotero.Schema = new function(){
|
||||||
|
|
||||||
var itemTypeID = Zotero.ID.get('customItemTypes');
|
var itemTypeID = Zotero.ID.get('customItemTypes');
|
||||||
|
|
||||||
yield Zotero.DB.executeTransaction(async function () {
|
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
|
||||||
await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
|
try {
|
||||||
|
yield Zotero.DB.executeTransaction(async function () {
|
||||||
|
await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID);
|
||||||
|
|
||||||
var fields = [
|
var fields = [
|
||||||
['name', 'Name'],
|
['name', 'Name'],
|
||||||
['institution', 'Institution'],
|
['institution', 'Institution'],
|
||||||
['address', 'Address'],
|
['address', 'Address'],
|
||||||
['telephone', 'Telephone'],
|
['telephone', 'Telephone'],
|
||||||
['email', 'Email'],
|
['email', 'Email'],
|
||||||
['homepage', 'Webpage'],
|
['homepage', 'Webpage'],
|
||||||
['discipline', 'Discipline'],
|
['discipline', 'Discipline'],
|
||||||
['nsfID', 'NSF ID'],
|
['nsfID', 'NSF ID'],
|
||||||
['dateSent', 'Date Sent'],
|
['dateSent', 'Date Sent'],
|
||||||
['dateDue', 'Date Due'],
|
['dateDue', 'Date Due'],
|
||||||
['accepted', 'Accepted'],
|
['accepted', 'Accepted'],
|
||||||
['programDirector', 'Program Director']
|
['programDirector', 'Program Director']
|
||||||
];
|
];
|
||||||
for (var i=0; i<fields.length; i++) {
|
for (var i=0; i<fields.length; i++) {
|
||||||
var fieldID = Zotero.ItemFields.getID(fields[i][0]);
|
var fieldID = Zotero.ItemFields.getID(fields[i][0]);
|
||||||
if (!fieldID) {
|
if (!fieldID) {
|
||||||
var fieldID = Zotero.ID.get('customFields');
|
var fieldID = Zotero.ID.get('customFields');
|
||||||
await Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
|
await Zotero.DB.queryAsync("INSERT INTO customFields VALUES (?, ?, ?)", [fieldID, fields[i][0], fields[i][1]]);
|
||||||
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
|
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, NULL, ?, 1, ?)", [itemTypeID, fieldID, i+1]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
|
await Zotero.DB.queryAsync("INSERT INTO customItemTypeFields VALUES (?, ?, NULL, 1, ?)", [itemTypeID, fieldID, i+1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (fields[i][0]) {
|
||||||
|
case 'name':
|
||||||
|
var baseFieldID = Zotero.ItemFields.getID('title');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dateSent':
|
||||||
|
var baseFieldID = Zotero.ItemFields.getID('date');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'homepage':
|
||||||
|
var baseFieldID = Zotero.ItemFields.getID('url');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
var baseFieldID = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseFieldID) {
|
||||||
|
await Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (fields[i][0]) {
|
await _reloadSchema();
|
||||||
case 'name':
|
});
|
||||||
var baseFieldID = Zotero.ItemFields.getID('title');
|
}
|
||||||
break;
|
finally {
|
||||||
|
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
|
||||||
case 'dateSent':
|
}
|
||||||
var baseFieldID = Zotero.ItemFields.getID('date');
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'homepage':
|
|
||||||
var baseFieldID = Zotero.ItemFields.getID('url');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
var baseFieldID = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseFieldID) {
|
|
||||||
await Zotero.DB.queryAsync("INSERT INTO customBaseFieldMappings VALUES (?, ?, ?)", [itemTypeID, baseFieldID, fieldID]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _reloadSchema();
|
|
||||||
}, { disableForeignKeys: true });
|
|
||||||
|
|
||||||
var s = new Zotero.Search;
|
var s = new Zotero.Search;
|
||||||
s.name = "Overdue NSF Reviewers";
|
s.name = "Overdue NSF Reviewers";
|
||||||
|
|
@ -814,26 +826,32 @@ Zotero.Schema = new function(){
|
||||||
}
|
}
|
||||||
|
|
||||||
Zotero.debug("Uninstalling nsfReviewer item type");
|
Zotero.debug("Uninstalling nsfReviewer item type");
|
||||||
yield Zotero.DB.executeTransaction(async function () {
|
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF");
|
||||||
await Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
|
try {
|
||||||
await Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
|
yield Zotero.DB.executeTransaction(async function () {
|
||||||
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
|
await Zotero.DB.queryAsync("DELETE FROM customItemTypeFields WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
|
||||||
for (let fieldID of fields) {
|
await Zotero.DB.queryAsync("DELETE FROM customBaseFieldMappings WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
|
||||||
if (Zotero.ItemFields.isCustom(fieldID)) {
|
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
|
||||||
await Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
|
for (let fieldID of fields) {
|
||||||
|
if (Zotero.ItemFields.isCustom(fieldID)) {
|
||||||
|
await Zotero.DB.queryAsync("DELETE FROM customFields WHERE customFieldID=?", fieldID - Zotero.ItemTypes.customIDOffset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
await Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
|
||||||
await Zotero.DB.queryAsync("DELETE FROM customItemTypes WHERE customItemTypeID=?", itemTypeID - Zotero.ItemTypes.customIDOffset);
|
|
||||||
|
|
||||||
var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
|
var searches = Zotero.Searches.getByLibrary(Zotero.Libraries.userLibraryID);
|
||||||
for (let search of searches) {
|
for (let search of searches) {
|
||||||
if (search.name == 'Overdue NSF Reviewers') {
|
if (search.name == 'Overdue NSF Reviewers') {
|
||||||
await search.erase();
|
await search.erase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await _reloadSchema();
|
await _reloadSchema();
|
||||||
}.bind(this), { disableForeignKeys: true });
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
yield Zotero.DB.queryAsync("PRAGMA foreign_keys=ON");
|
||||||
|
}
|
||||||
|
|
||||||
ps.alert(null, "Zotero Item Type Removed", "The 'NSF Reviewer' item type has been uninstalled.");
|
ps.alert(null, "Zotero Item Type Removed", "The 'NSF Reviewer' item type has been uninstalled.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ Zotero.Sync.Storage = new function () {
|
||||||
* @param {String} libraryKey
|
* @param {String} libraryKey
|
||||||
* @param {Number|NULL}
|
* @param {Number|NULL}
|
||||||
*/
|
*/
|
||||||
this.setItemDownloadPercentage = function (libraryKey, percentage) {
|
this.setItemDownloadPercentage = Zotero.Utilities.throttle(function (libraryKey, percentage) {
|
||||||
Zotero.debug("Setting image download percentage to " + percentage
|
Zotero.debug("Setting image download percentage to " + percentage
|
||||||
+ " for item " + libraryKey);
|
+ " for item " + libraryKey);
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ Zotero.Sync.Storage = new function () {
|
||||||
}
|
}
|
||||||
Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" });
|
Zotero.Notifier.trigger('redraw', 'item', parentItem.id, { column: "hasAttachment" });
|
||||||
}
|
}
|
||||||
}
|
}, 100);
|
||||||
|
|
||||||
|
|
||||||
function error(e) {
|
function error(e) {
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,9 @@ Zotero.Sync.Storage.Engine.prototype.start = Zotero.Promise.coroutine(function*
|
||||||
// full check of this library, check only files that were previously modified or opened
|
// full check of this library, check only files that were previously modified or opened
|
||||||
// recently
|
// recently
|
||||||
else if (this.background
|
else if (this.background
|
||||||
&& !this.firstInSession
|
// TEMP: Don't check all files at startup
|
||||||
|
// https://github.com/zotero/zotero/issues/5025
|
||||||
|
//&& !this.firstInSession
|
||||||
&& this.local.lastFullFileCheck[libraryID]
|
&& this.local.lastFullFileCheck[libraryID]
|
||||||
&& (this.local.lastFullFileCheck[libraryID]
|
&& (this.local.lastFullFileCheck[libraryID]
|
||||||
+ (this.maxCheckAge * 1000)) > new Date().getTime()) {
|
+ (this.maxCheckAge * 1000)) > new Date().getTime()) {
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,10 @@ Zotero.Sync.Storage.Local = {
|
||||||
}
|
}
|
||||||
sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN (" + sql + ")";
|
sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN (" + sql + ")";
|
||||||
await Zotero.DB.queryAsync(sql, [this.SYNC_STATE_TO_UPLOAD].concat(params));
|
await Zotero.DB.queryAsync(sql, [this.SYNC_STATE_TO_UPLOAD].concat(params));
|
||||||
|
|
||||||
|
var library = Zotero.Libraries.get(libraryID);
|
||||||
|
library.storageVersion = -1;
|
||||||
|
await library.save();
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,276 +0,0 @@
|
||||||
/*
|
|
||||||
***** BEGIN LICENSE BLOCK *****
|
|
||||||
|
|
||||||
Copyright © 2009 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 *****
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream listener that can handle both download and upload requests
|
|
||||||
*
|
|
||||||
* Possible properties of data object:
|
|
||||||
* - onStart: f(request)
|
|
||||||
* - onProgress: f(request, progress, progressMax)
|
|
||||||
* - onStop: f(request, status, response)
|
|
||||||
* - onCancel: f(request, status)
|
|
||||||
* - streams: array of streams to close on completion
|
|
||||||
*/
|
|
||||||
Zotero.Sync.Storage.StreamListener = function (data) {
|
|
||||||
this._data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
Zotero.Sync.Storage.StreamListener.prototype = {
|
|
||||||
_channel: null,
|
|
||||||
|
|
||||||
// nsIProgressEventSink
|
|
||||||
onProgress: function (request, progress, progressMax) {
|
|
||||||
Zotero.debug("onProgress with " + progress + "/" + progressMax);
|
|
||||||
this._onProgress(request, progress, progressMax);
|
|
||||||
},
|
|
||||||
|
|
||||||
onStatus: function (request, status, statusArg) {
|
|
||||||
Zotero.debug('onStatus with ' + status);
|
|
||||||
},
|
|
||||||
|
|
||||||
// nsIRequestObserver
|
|
||||||
// Note: For uploads, this isn't called until data is done uploading
|
|
||||||
onStartRequest: function (request) {
|
|
||||||
Zotero.debug('onStartRequest');
|
|
||||||
this._response = "";
|
|
||||||
|
|
||||||
this._onStart(request);
|
|
||||||
},
|
|
||||||
|
|
||||||
onStopRequest: function (request, status) {
|
|
||||||
Zotero.debug('onStopRequest with ' + status);
|
|
||||||
|
|
||||||
// Some errors from https://developer.mozilla.org/en-US/docs/Table_Of_Errors
|
|
||||||
var msg = "";
|
|
||||||
switch (status) {
|
|
||||||
// Normal
|
|
||||||
case 0:
|
|
||||||
break;
|
|
||||||
|
|
||||||
// NS_BINDING_ABORTED
|
|
||||||
case 0x804b0002:
|
|
||||||
msg = "Request cancelled";
|
|
||||||
break;
|
|
||||||
|
|
||||||
// NS_ERROR_NET_INTERRUPT
|
|
||||||
case 0x804B0047:
|
|
||||||
msg = "Request interrupted";
|
|
||||||
break;
|
|
||||||
|
|
||||||
// NS_ERROR_NET_TIMEOUT
|
|
||||||
case 0x804B000E:
|
|
||||||
msg = "Request timed out";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
msg = "Request failed";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg) {
|
|
||||||
msg += " in Zotero.Sync.Storage.StreamListener.onStopRequest() (" + status + ")";
|
|
||||||
Components.utils.reportError(msg);
|
|
||||||
Zotero.debug(msg, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._onStop(request, status);
|
|
||||||
},
|
|
||||||
|
|
||||||
// nsIWebProgressListener
|
|
||||||
onProgressChange: function (wp, request, curSelfProgress,
|
|
||||||
maxSelfProgress, curTotalProgress, maxTotalProgress) {
|
|
||||||
//Zotero.debug("onProgressChange with " + curTotalProgress + "/" + maxTotalProgress);
|
|
||||||
|
|
||||||
// onProgress gets called too, so this isn't necessary
|
|
||||||
//this._onProgress(request, curTotalProgress, maxTotalProgress);
|
|
||||||
},
|
|
||||||
|
|
||||||
onStateChange: function (wp, request, stateFlags, status) {
|
|
||||||
Zotero.debug("onStateChange with " + stateFlags);
|
|
||||||
|
|
||||||
if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_REQUEST) {
|
|
||||||
if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_START) {
|
|
||||||
this._onStart(request);
|
|
||||||
}
|
|
||||||
else if (stateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) {
|
|
||||||
this._onStop(request, status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onStatusChange: function (progress, request, status, message) {
|
|
||||||
Zotero.debug("onStatusChange with '" + message + "'");
|
|
||||||
},
|
|
||||||
onLocationChange: function () {
|
|
||||||
Zotero.debug('onLocationChange');
|
|
||||||
},
|
|
||||||
onSecurityChange: function () {
|
|
||||||
Zotero.debug('onSecurityChange');
|
|
||||||
},
|
|
||||||
|
|
||||||
// nsIStreamListener
|
|
||||||
onDataAvailable: function (request, stream, sourceOffset, length) {
|
|
||||||
Zotero.debug('onDataAvailable');
|
|
||||||
var scriptableInputStream =
|
|
||||||
Components.classes["@mozilla.org/scriptableinputstream;1"]
|
|
||||||
.createInstance(Components.interfaces.nsIScriptableInputStream);
|
|
||||||
scriptableInputStream.init(stream);
|
|
||||||
|
|
||||||
var data = scriptableInputStream.read(length);
|
|
||||||
Zotero.debug(data);
|
|
||||||
this._response += data;
|
|
||||||
},
|
|
||||||
|
|
||||||
// nsIChannelEventSink
|
|
||||||
//
|
|
||||||
// If this._data.onChannelRedirect exists, it should return a promise resolving to true to
|
|
||||||
// follow the redirect or false to cancel it
|
|
||||||
onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
|
|
||||||
Zotero.debug('onChannelRedirect');
|
|
||||||
|
|
||||||
if (this._data && this._data.onChannelRedirect) {
|
|
||||||
let result = yield this._data.onChannelRedirect(oldChannel, newChannel, flags);
|
|
||||||
if (!result) {
|
|
||||||
oldChannel.cancel(Components.results.NS_BINDING_ABORTED);
|
|
||||||
newChannel.cancel(Components.results.NS_BINDING_ABORTED);
|
|
||||||
Zotero.debug("Cancelling redirect");
|
|
||||||
// TODO: Prevent onStateChange error
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if redirecting, store the new channel
|
|
||||||
this._channel = newChannel;
|
|
||||||
}),
|
|
||||||
|
|
||||||
asyncOnChannelRedirect: function (oldChan, newChan, flags, redirectCallback) {
|
|
||||||
Zotero.debug('asyncOnRedirect');
|
|
||||||
|
|
||||||
this.onChannelRedirect(oldChan, newChan, flags)
|
|
||||||
.then(function (result) {
|
|
||||||
redirectCallback.onRedirectVerifyCallback(
|
|
||||||
result ? Components.results.NS_SUCCEEDED : Components.results.NS_FAILED
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(function (e) {
|
|
||||||
Zotero.logError(e);
|
|
||||||
redirectCallback.onRedirectVerifyCallback(Components.results.NS_FAILED);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// nsIHttpEventSink
|
|
||||||
onRedirect: function (oldChannel, newChannel) {
|
|
||||||
Zotero.debug('onRedirect');
|
|
||||||
|
|
||||||
var newURL = Zotero.HTTP.getDisplayURI(newChannel.URI).spec;
|
|
||||||
Zotero.debug("Redirecting to " + newURL);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// Private methods
|
|
||||||
//
|
|
||||||
_onStart: function (request) {
|
|
||||||
Zotero.debug('Starting request');
|
|
||||||
if (this._data && this._data.onStart) {
|
|
||||||
this._data.onStart(request);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onProgress: function (request, progress, progressMax) {
|
|
||||||
if (this._data && this._data.onProgress) {
|
|
||||||
this._data.onProgress(request, progress, progressMax);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onStop: function (request, status) {
|
|
||||||
var cancelled = status == 0x804b0002; // NS_BINDING_ABORTED
|
|
||||||
|
|
||||||
if (!cancelled && status == 0 && request instanceof Components.interfaces.nsIHttpChannel) {
|
|
||||||
request.QueryInterface(Components.interfaces.nsIHttpChannel);
|
|
||||||
try {
|
|
||||||
status = request.responseStatus;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.debug("Request responseStatus not available", 1);
|
|
||||||
status = 0;
|
|
||||||
}
|
|
||||||
Zotero.debug('Request ended with status code ' + status);
|
|
||||||
request.QueryInterface(Components.interfaces.nsIRequest);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Zotero.debug('Request ended with status ' + status);
|
|
||||||
status = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._data.streams) {
|
|
||||||
for (let stream of this._data.streams) {
|
|
||||||
stream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled) {
|
|
||||||
if (this._data.onCancel) {
|
|
||||||
this._data.onCancel(request, status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (this._data.onStop) {
|
|
||||||
this._data.onStop(request, status, this._response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._channel = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
// nsIInterfaceRequestor
|
|
||||||
getInterface: function (iid) {
|
|
||||||
try {
|
|
||||||
return this.QueryInterface(iid);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
throw Components.results.NS_NOINTERFACE;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
QueryInterface: function(iid) {
|
|
||||||
if (iid.equals(Components.interfaces.nsISupports) ||
|
|
||||||
iid.equals(Components.interfaces.nsIInterfaceRequestor) ||
|
|
||||||
iid.equals(Components.interfaces.nsIChannelEventSink) ||
|
|
||||||
iid.equals(Components.interfaces.nsIProgressEventSink) ||
|
|
||||||
iid.equals(Components.interfaces.nsIHttpEventSink) ||
|
|
||||||
iid.equals(Components.interfaces.nsIStreamListener) ||
|
|
||||||
iid.equals(Components.interfaces.nsIWebProgressListener)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
throw Components.results.NS_NOINTERFACE;
|
|
||||||
},
|
|
||||||
|
|
||||||
_safeSpec: function (uri) {
|
|
||||||
return uri.scheme + '://' + uri.username + ':********@'
|
|
||||||
+ uri.hostPort + uri.pathQueryRef
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -31,10 +31,10 @@ if (!Zotero.Sync.Storage.Mode) {
|
||||||
Zotero.Sync.Storage.Mode.WebDAV = function (options) {
|
Zotero.Sync.Storage.Mode.WebDAV = function (options) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
this.VerificationError = function (error, uri) {
|
this.VerificationError = function (error, url) {
|
||||||
this.message = `WebDAV verification error (${error})`;
|
this.message = `WebDAV verification error (${error})`;
|
||||||
this.error = error;
|
this.error = error;
|
||||||
this.uri = uri;
|
this.url = url;
|
||||||
}
|
}
|
||||||
this.VerificationError.prototype = Object.create(Error.prototype);
|
this.VerificationError.prototype = Object.create(Error.prototype);
|
||||||
}
|
}
|
||||||
|
|
@ -196,14 +196,23 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
throw new this.VerificationError("NO_PASSWORD");
|
throw new this.VerificationError("NO_PASSWORD");
|
||||||
}
|
}
|
||||||
|
|
||||||
url = scheme + '://'
|
var spec = scheme + '://'
|
||||||
+ encodeURIComponent(username) + ':' + encodeURIComponent(password) + '@'
|
+ encodeURIComponent(username) + ':' + encodeURIComponent(password) + '@'
|
||||||
+ url
|
+ url
|
||||||
+ (url.endsWith('/') ? '' : '/');
|
+ (url.endsWith('/') ? '' : '/');
|
||||||
|
|
||||||
var io = Services.io;
|
var io = Services.io;
|
||||||
this._parentURI = io.newURI(url, null, null);
|
try {
|
||||||
this._rootURI = io.newURI(url + "zotero/", null, null);
|
this._parentURI = io.newURI(spec, null, null);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e.message.includes('NS_ERROR_MALFORMED_URI')) {
|
||||||
|
let displayURL = scheme + '://' + url + (url.endsWith('/') ? '' : '/');
|
||||||
|
throw new this.VerificationError("INVALID_URL", displayURL);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
this._rootURI = io.newURI(spec + "zotero/", null, null);
|
||||||
Zotero.HTTP.CookieBlocker.addURL(this._rootURI.spec);
|
Zotero.HTTP.CookieBlocker.addURL(this._rootURI.spec);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -258,7 +267,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
* @param {Zotero.Sync.Storage.Request} request
|
* @param {Zotero.Sync.Storage.Request} request
|
||||||
* @return {Promise<Zotero.Sync.Storage.Result>}
|
* @return {Promise<Zotero.Sync.Storage.Result>}
|
||||||
*/
|
*/
|
||||||
downloadFile: Zotero.Promise.coroutine(function* (request) {
|
downloadFile: async function (request) {
|
||||||
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
|
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error("Item '" + request.name + "' not found");
|
throw new Error("Item '" + request.name + "' not found");
|
||||||
|
|
@ -272,7 +281,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve modification time from server
|
// Retrieve modification time from server
|
||||||
var metadata = yield this._getStorageFileMetadata(item, request);
|
var metadata = await this._getStorageFileMetadata(item, request);
|
||||||
|
|
||||||
if (!request.isRunning()) {
|
if (!request.isRunning()) {
|
||||||
Zotero.debug("Download request '" + request.name
|
Zotero.debug("Download request '" + request.name
|
||||||
|
|
@ -285,7 +294,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
return new Zotero.Sync.Storage.Result;
|
return new Zotero.Sync.Storage.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileModTime = yield item.attachmentModificationTime;
|
var fileModTime = await item.attachmentModificationTime;
|
||||||
if (metadata.mtime == fileModTime) {
|
if (metadata.mtime == fileModTime) {
|
||||||
Zotero.debug("File mod time matches remote file -- skipping download of "
|
Zotero.debug("File mod time matches remote file -- skipping download of "
|
||||||
+ item.libraryKey);
|
+ item.libraryKey);
|
||||||
|
|
@ -293,10 +302,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
var updateItem = item.attachmentSyncState != 1
|
var updateItem = item.attachmentSyncState != 1
|
||||||
item.attachmentSyncedModificationTime = metadata.mtime;
|
item.attachmentSyncedModificationTime = metadata.mtime;
|
||||||
item.attachmentSyncState = "in_sync";
|
item.attachmentSyncState = "in_sync";
|
||||||
yield item.saveTx({ skipAll: true });
|
await item.saveTx({ skipAll: true });
|
||||||
// DEBUG: Necessary?
|
// DEBUG: Necessary?
|
||||||
if (updateItem) {
|
if (updateItem) {
|
||||||
yield item.updateSynced(false);
|
await item.updateSynced(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Zotero.Sync.Storage.Result({
|
return new Zotero.Sync.Storage.Result({
|
||||||
|
|
@ -307,9 +316,8 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
var uri = this._getItemURI(item);
|
var uri = this._getItemURI(item);
|
||||||
|
|
||||||
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
|
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
|
||||||
yield Zotero.File.removeIfExists(destPath);
|
await Zotero.File.removeIfExists(destPath);
|
||||||
|
|
||||||
var deferred = Zotero.Promise.defer();
|
|
||||||
var requestData = {
|
var requestData = {
|
||||||
item,
|
item,
|
||||||
mtime: metadata.mtime,
|
mtime: metadata.mtime,
|
||||||
|
|
@ -317,88 +325,71 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
compressed: true
|
compressed: true
|
||||||
};
|
};
|
||||||
|
|
||||||
var listener = new Zotero.Sync.Storage.StreamListener(
|
return new Promise(async (resolve, reject) => {
|
||||||
{
|
try {
|
||||||
onStart: function (req) {
|
let req = await Zotero.HTTP.download(
|
||||||
if (request.isFinished()) {
|
uri,
|
||||||
Zotero.debug("Download request " + request.name
|
destPath,
|
||||||
+ " stopped before download started -- closing channel");
|
{
|
||||||
req.cancel(0x804b0002); // NS_BINDING_ABORTED
|
successCodes: [200, 404],
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
noCache: true,
|
||||||
|
notificationCallbacks: {
|
||||||
|
onProgress: function (a, b, c) {
|
||||||
|
request.onProgress(a, b, c)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errorDelayIntervals: this.ERROR_DELAY_INTERVALS,
|
||||||
|
errorDelayMax: this.ERROR_DELAY_MAX,
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
onProgress: function (a, b, c) {
|
|
||||||
request.onProgress(a, b, c)
|
|
||||||
},
|
|
||||||
onStop: Zotero.Promise.coroutine(function* (req, status, res) {
|
|
||||||
request.setChannel(false);
|
|
||||||
|
|
||||||
if (status == 404) {
|
if (req.status == 404) {
|
||||||
let msg = "Remote ZIP file not found for item " + item.libraryKey;
|
let msg = "Remote ZIP file not found for item " + item.libraryKey;
|
||||||
Zotero.debug(msg, 2);
|
Zotero.debug(msg, 2);
|
||||||
Components.utils.reportError(msg);
|
Cu.reportError(msg);
|
||||||
|
|
||||||
// Delete the orphaned prop file
|
|
||||||
try {
|
|
||||||
yield this._deleteStorageFiles([item.key + ".prop"]);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.logError(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (status != 200) {
|
|
||||||
try {
|
|
||||||
this._throwFriendlyError("GET", dispURL, status);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
deferred.reject(e);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't try to process if the request has been cancelled
|
|
||||||
if (request.isFinished()) {
|
|
||||||
Zotero.debug("Download request " + request.name
|
|
||||||
+ " is no longer running after file download");
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Zotero.debug("Finished download of " + destPath);
|
|
||||||
|
|
||||||
|
// Delete the orphaned prop file
|
||||||
try {
|
try {
|
||||||
deferred.resolve(
|
await this._deleteStorageFiles([item.key + ".prop"]);
|
||||||
Zotero.Sync.Storage.Local.processDownload(requestData)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
deferred.reject(e);
|
Zotero.logError(e);
|
||||||
}
|
|
||||||
}.bind(this)),
|
|
||||||
onCancel: function (req, status) {
|
|
||||||
Zotero.debug("Request cancelled");
|
|
||||||
if (deferred.promise.isPending()) {
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve(new Zotero.Sync.Storage.Result);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't try to process if the request has been cancelled
|
||||||
|
if (request.isFinished()) {
|
||||||
|
Zotero.debug("Download request " + request.name
|
||||||
|
+ " is no longer running after file download");
|
||||||
|
resolve(new Zotero.Sync.Storage.Result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug("Finished download of " + destPath);
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
Zotero.Sync.Storage.Local.processDownload(requestData)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
catch (e) {
|
||||||
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||||
// Don't display password in console
|
try {
|
||||||
var dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
|
let dispURL = Zotero.HTTP.getDisplayURI(uri).spec;
|
||||||
Zotero.debug('Saving ' + dispURL);
|
this._throwFriendlyError("GET", dispURL, e.xmlhttp.status);
|
||||||
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
|
}
|
||||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
catch (e) {
|
||||||
.createInstance(nsIWBP);
|
reject(e);
|
||||||
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
|
}
|
||||||
wbp.progressListener = listener;
|
return;
|
||||||
Zotero.Utilities.Internal.saveURI(wbp, uri, destPath);
|
}
|
||||||
|
Zotero.logError(e);
|
||||||
return deferred.promise;
|
reject(new Error(Zotero.Sync.Storage.defaultError));
|
||||||
}),
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
uploadFile: Zotero.Promise.coroutine(function* (request) {
|
uploadFile: Zotero.Promise.coroutine(function* (request) {
|
||||||
|
|
@ -641,7 +632,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
|
|
||||||
var dav = req.getResponseHeader("DAV");
|
var dav = req.getResponseHeader("DAV");
|
||||||
if (dav == null) {
|
if (dav == null) {
|
||||||
throw new this.VerificationError("NOT_DAV", uri);
|
throw new this.VerificationError("NOT_DAV", Zotero.HTTP.getDisplayURI(uri, true).spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers = { Depth: 0 };
|
var headers = { Depth: 0 };
|
||||||
|
|
@ -683,7 +674,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||||
if (e.status >= 200 && e.status < 300) {
|
if (e.status >= 200 && e.status < 300) {
|
||||||
throw new this.VerificationError("NONEXISTENT_FILE_NOT_MISSING", uri);
|
throw new this.VerificationError(
|
||||||
|
"NONEXISTENT_FILE_NOT_MISSING",
|
||||||
|
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
|
@ -730,7 +724,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
// This can also be from IIS 6+, which is configured not to serve .prop files.
|
// This can also be from IIS 6+, which is configured not to serve .prop files.
|
||||||
// http://support.microsoft.com/kb/326965
|
// http://support.microsoft.com/kb/326965
|
||||||
else if (req.status == 404) {
|
else if (req.status == 404) {
|
||||||
throw new this.VerificationError("FILE_MISSING_AFTER_UPLOAD", uri);
|
throw new this.VerificationError(
|
||||||
|
"FILE_MISSING_AFTER_UPLOAD",
|
||||||
|
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (req.status == 404) {
|
else if (req.status == 404) {
|
||||||
|
|
@ -751,10 +748,16 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.status == 207) {
|
if (req.status == 207) {
|
||||||
throw new this.VerificationError("ZOTERO_DIR_NOT_FOUND", uri);
|
throw new this.VerificationError(
|
||||||
|
"ZOTERO_DIR_NOT_FOUND",
|
||||||
|
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (req.status == 404) {
|
else if (req.status == 404) {
|
||||||
throw new this.VerificationError("PARENT_DIR_NOT_FOUND", uri);
|
throw new this.VerificationError(
|
||||||
|
"PARENT_DIR_NOT_FOUND",
|
||||||
|
Zotero.HTTP.getDisplayURI(uri, true).spec
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -805,10 +808,6 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (err instanceof this.VerificationError) {
|
else if (err instanceof this.VerificationError) {
|
||||||
let spec;
|
|
||||||
if (err.uri) {
|
|
||||||
spec = err.uri.scheme + '://' + err.uri.hostPort + err.uri.pathQueryRef;
|
|
||||||
}
|
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "NO_URL":
|
case "NO_URL":
|
||||||
errorMsg = Zotero.getString('sync.storage.error.webdav.enterURL');
|
errorMsg = Zotero.getString('sync.storage.error.webdav.enterURL');
|
||||||
|
|
@ -822,21 +821,22 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = {
|
||||||
errorMsg = Zotero.getString('sync.error.enterPassword');
|
errorMsg = Zotero.getString('sync.error.enterPassword');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "INVALID_URL":
|
||||||
case "NOT_DAV":
|
case "NOT_DAV":
|
||||||
errorMsg = Zotero.getString('sync.storage.error.webdav.invalidURL', spec);
|
errorMsg = Zotero.getString('sync.storage.error.webdav.invalidURL', err.url);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "PARENT_DIR_NOT_FOUND":
|
case "PARENT_DIR_NOT_FOUND":
|
||||||
errorTitle = Zotero.getString('sync.storage.error.directoryNotFound');
|
errorTitle = Zotero.getString('sync.storage.error.directoryNotFound');
|
||||||
var parentSpec = spec.replace(/zotero\/$/, "");
|
var parentURL = err.url.replace(/zotero\/$/, "");
|
||||||
errorMsg = Zotero.getString('sync.storage.error.doesNotExist', parentSpec);
|
errorMsg = Zotero.getString('sync.storage.error.doesNotExist', parentURL);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ZOTERO_DIR_NOT_FOUND":
|
case "ZOTERO_DIR_NOT_FOUND":
|
||||||
var create = promptService.confirmEx(
|
var create = promptService.confirmEx(
|
||||||
window,
|
window,
|
||||||
Zotero.getString('sync.storage.error.directoryNotFound'),
|
Zotero.getString('sync.storage.error.directoryNotFound'),
|
||||||
Zotero.getString('sync.storage.error.doesNotExist', spec) + "\n\n"
|
Zotero.getString('sync.storage.error.doesNotExist', err.url) + "\n\n"
|
||||||
+ Zotero.getString('sync.storage.error.createNow'),
|
+ Zotero.getString('sync.storage.error.createNow'),
|
||||||
promptService.BUTTON_POS_0
|
promptService.BUTTON_POS_0
|
||||||
* promptService.BUTTON_TITLE_IS_STRING
|
* promptService.BUTTON_TITLE_IS_STRING
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
|
||||||
* @param {Zotero.Sync.Storage.Request} request
|
* @param {Zotero.Sync.Storage.Request} request
|
||||||
* @return {Promise<Zotero.Sync.Storage.Result>}
|
* @return {Promise<Zotero.Sync.Storage.Result>}
|
||||||
*/
|
*/
|
||||||
downloadFile: Zotero.Promise.coroutine(function* (request) {
|
downloadFile: async function (request) {
|
||||||
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
|
var item = Zotero.Sync.Storage.Utilities.getItemFromRequest(request);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error("Item '" + request.name + "' not found");
|
throw new Error("Item '" + request.name + "' not found");
|
||||||
|
|
@ -63,195 +63,147 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
|
||||||
|
|
||||||
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
|
var destPath = OS.Path.join(Zotero.getTempDirectory().path, item.key + '.tmp');
|
||||||
|
|
||||||
// saveURI() below appears not to create empty files for Content-Length: 0,
|
// Create an empty file to check file access
|
||||||
// so we create one here just in case, which also lets us check file access
|
|
||||||
try {
|
try {
|
||||||
yield IOUtils.write(destPath, new Uint8Array());
|
await IOUtils.write(destPath, new Uint8Array());
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
Zotero.File.checkFileAccessError(e, destPath, 'create');
|
Zotero.File.checkFileAccessError(e, destPath, 'create');
|
||||||
}
|
}
|
||||||
|
|
||||||
var deferred = Zotero.Promise.defer();
|
|
||||||
var requestData = {item};
|
var requestData = {item};
|
||||||
|
|
||||||
var listener = new Zotero.Sync.Storage.StreamListener(
|
|
||||||
{
|
|
||||||
onStart: function (req) {
|
|
||||||
if (request.isFinished()) {
|
|
||||||
Zotero.debug("Download request " + request.name
|
|
||||||
+ " stopped before download started -- closing channel");
|
|
||||||
req.cancel(Components.results.NS_BINDING_ABORTED);
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChannelRedirect: Zotero.Promise.coroutine(function* (oldChannel, newChannel, flags) {
|
|
||||||
// These will be used in processDownload() if the download succeeds
|
|
||||||
oldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
|
|
||||||
|
|
||||||
Zotero.debug("CHANNEL HERE FOR " + item.libraryKey + " WITH " + oldChannel.status);
|
|
||||||
Zotero.debug(oldChannel.URI.spec);
|
|
||||||
Zotero.debug(newChannel.URI.spec);
|
|
||||||
|
|
||||||
var header;
|
|
||||||
try {
|
|
||||||
header = "Zotero-File-Modification-Time";
|
|
||||||
requestData.mtime = parseInt(oldChannel.getResponseHeader(header));
|
|
||||||
header = "Zotero-File-MD5";
|
|
||||||
requestData.md5 = oldChannel.getResponseHeader(header);
|
|
||||||
header = "Zotero-File-Compressed";
|
|
||||||
requestData.compressed = oldChannel.getResponseHeader(header) == 'Yes';
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
deferred.reject(new Error(`${header} header not set in file request for ${item.libraryKey}`));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(yield OS.File.exists(path))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateHash = false;
|
|
||||||
var fileModTime = yield item.attachmentModificationTime;
|
|
||||||
if (requestData.mtime == fileModTime) {
|
|
||||||
Zotero.debug("File mod time matches remote file -- skipping download of "
|
|
||||||
+ item.libraryKey);
|
|
||||||
}
|
|
||||||
// If not compressed, check hash, in case only timestamp changed
|
|
||||||
else if (!requestData.compressed && (yield item.attachmentHash) == requestData.md5) {
|
|
||||||
Zotero.debug("File hash matches remote file -- skipping download of "
|
|
||||||
+ item.libraryKey);
|
|
||||||
updateHash = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local metadata and stop request, skipping file download
|
|
||||||
yield OS.File.setDates(path, null, new Date(requestData.mtime));
|
|
||||||
item.attachmentSyncedModificationTime = requestData.mtime;
|
|
||||||
if (updateHash) {
|
|
||||||
item.attachmentSyncedHash = requestData.md5;
|
|
||||||
}
|
|
||||||
item.attachmentSyncState = "in_sync";
|
|
||||||
yield item.saveTx({ skipAll: true });
|
|
||||||
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result({
|
|
||||||
localChanges: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}),
|
|
||||||
onProgress: function (req, progress, progressMax) {
|
|
||||||
request.onProgress(progress, progressMax);
|
|
||||||
},
|
|
||||||
onStop: function (req, status, res) {
|
|
||||||
request.setChannel(false);
|
|
||||||
|
|
||||||
if (status != 200) {
|
|
||||||
if (status == 404) {
|
|
||||||
Zotero.debug("Remote file not found for item " + item.libraryKey);
|
|
||||||
// Don't refresh item pane rows when nothing happened
|
|
||||||
request.skipProgressBarUpdate = true;
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for SSL certificate error
|
|
||||||
if (status == 0) {
|
|
||||||
try {
|
|
||||||
Zotero.HTTP.checkSecurity(req);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
deferred.reject(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If S3 connection is interrupted, delay and retry, or bail if too many
|
|
||||||
// consecutive failures
|
|
||||||
if (status == 0 || status == 500 || status == 503) {
|
|
||||||
if (++this._s3ConsecutiveFailures < this._maxS3ConsecutiveFailures) {
|
|
||||||
let libraryKey = item.libraryKey;
|
|
||||||
let msg = "S3 returned 0 for " + libraryKey + " -- retrying download"
|
|
||||||
Components.utils.reportError(msg);
|
|
||||||
Zotero.debug(msg, 1);
|
|
||||||
if (this._s3Backoff < this._maxS3Backoff) {
|
|
||||||
this._s3Backoff *= 2;
|
|
||||||
}
|
|
||||||
Zotero.debug("Delaying " + libraryKey + " download for "
|
|
||||||
+ this._s3Backoff + " seconds", 2);
|
|
||||||
Zotero.Promise.delay(this._s3Backoff * 1000)
|
|
||||||
.then(function () {
|
|
||||||
deferred.resolve(this.downloadFile(request));
|
|
||||||
}.bind(this));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Zotero.debug(this._s3ConsecutiveFailures
|
|
||||||
+ " consecutive S3 failures -- aborting", 1);
|
|
||||||
this._s3ConsecutiveFailures = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg = "Unexpected status code " + status + " for GET " + uri;
|
|
||||||
Zotero.debug(msg, 1);
|
|
||||||
Components.utils.reportError(msg);
|
|
||||||
// Output saved content, in case an error was captured
|
|
||||||
try {
|
|
||||||
let sample = Zotero.File.getContents(destPath, null, 4096);
|
|
||||||
if (sample) {
|
|
||||||
Zotero.debug(sample, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.debug(e, 1);
|
|
||||||
}
|
|
||||||
deferred.reject(new Error(Zotero.Sync.Storage.defaultError));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't try to process if the request has been cancelled
|
|
||||||
if (request.isFinished()) {
|
|
||||||
Zotero.debug("Download request " + request.name
|
|
||||||
+ " is no longer running after file download", 2);
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Zotero.debug("Finished download of " + destPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
deferred.resolve(
|
|
||||||
Zotero.Sync.Storage.Local.processDownload(requestData)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
deferred.reject(e);
|
|
||||||
}
|
|
||||||
}.bind(this),
|
|
||||||
onCancel: function (req, status) {
|
|
||||||
Zotero.debug("Request cancelled");
|
|
||||||
if (deferred.promise.isPending()) {
|
|
||||||
deferred.resolve(new Zotero.Sync.Storage.Result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
var params = this._getRequestParams(item.libraryID, `items/${item.key}/file`);
|
var params = this._getRequestParams(item.libraryID, `items/${item.key}/file`);
|
||||||
var uri = this.apiClient.buildRequestURI(params);
|
var uri = this.apiClient.buildRequestURI(params);
|
||||||
var headers = this.apiClient.getHeaders();
|
|
||||||
|
|
||||||
Zotero.debug('Saving ' + uri);
|
return new Promise(async (resolve, reject) => {
|
||||||
const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
|
try {
|
||||||
var wbp = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
|
let req = await Zotero.HTTP.download(
|
||||||
.createInstance(nsIWBP);
|
uri,
|
||||||
wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
|
destPath,
|
||||||
wbp.progressListener = listener;
|
{
|
||||||
Zotero.Utilities.Internal.saveURI(wbp, uri, destPath, headers);
|
successCodes: [200, 404],
|
||||||
|
headers: this.apiClient.getHeaders(),
|
||||||
|
noCache: true,
|
||||||
|
notificationCallbacks: {
|
||||||
|
asyncOnChannelRedirect: async function (oldChannel, newChannel, flags, callback) {
|
||||||
|
// These will be used in processDownload() if the download succeeds
|
||||||
|
oldChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
|
||||||
|
|
||||||
return deferred.promise;
|
Zotero.debug(`Handling ${oldChannel.responseStatus} redirect for ${item.libraryKey}`);
|
||||||
}),
|
Zotero.debug(oldChannel.URI.spec);
|
||||||
|
Zotero.debug(newChannel.URI.spec);
|
||||||
|
|
||||||
|
var header;
|
||||||
|
try {
|
||||||
|
header = "Zotero-File-Modification-Time";
|
||||||
|
requestData.mtime = parseInt(oldChannel.getResponseHeader(header));
|
||||||
|
header = "Zotero-File-MD5";
|
||||||
|
requestData.md5 = oldChannel.getResponseHeader(header);
|
||||||
|
header = "Zotero-File-Compressed";
|
||||||
|
requestData.compressed = oldChannel.getResponseHeader(header) == 'Yes';
|
||||||
|
}
|
||||||
|
catch (_e) {
|
||||||
|
reject(new Error(`${header} header not set in file request for ${item.libraryKey}`));
|
||||||
|
callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await IOUtils.exists(path))) {
|
||||||
|
callback.onRedirectVerifyCallback(Cr.NS_OK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateHash = false;
|
||||||
|
var fileModTime = await item.attachmentModificationTime;
|
||||||
|
if (requestData.mtime == fileModTime) {
|
||||||
|
Zotero.debug("File mod time matches remote file -- skipping download of "
|
||||||
|
+ item.libraryKey);
|
||||||
|
}
|
||||||
|
// If not compressed, check hash, in case only timestamp changed
|
||||||
|
else if (!requestData.compressed && (await item.attachmentHash) == requestData.md5) {
|
||||||
|
Zotero.debug("File hash matches remote file -- skipping download of "
|
||||||
|
+ item.libraryKey);
|
||||||
|
updateHash = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callback.onRedirectVerifyCallback(Cr.NS_OK);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local metadata and stop request, skipping file download
|
||||||
|
await OS.File.setDates(path, null, new Date(requestData.mtime));
|
||||||
|
item.attachmentSyncedModificationTime = requestData.mtime;
|
||||||
|
if (updateHash) {
|
||||||
|
item.attachmentSyncedHash = requestData.md5;
|
||||||
|
}
|
||||||
|
item.attachmentSyncState = "in_sync";
|
||||||
|
await item.saveTx({ skipAll: true });
|
||||||
|
|
||||||
|
resolve(new Zotero.Sync.Storage.Result({
|
||||||
|
localChanges: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
|
||||||
|
},
|
||||||
|
|
||||||
|
onProgress: function (req, progress, progressMax) {
|
||||||
|
request.onProgress(progress, progressMax);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (req.status == 404) {
|
||||||
|
Zotero.debug("Remote file not found for item " + item.libraryKey);
|
||||||
|
// Don't refresh item pane rows when nothing happened
|
||||||
|
request.skipProgressBarUpdate = true;
|
||||||
|
resolve(new Zotero.Sync.Storage.Result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't try to process if the request has been cancelled
|
||||||
|
if (request.isFinished()) {
|
||||||
|
Zotero.debug(`Download request ${request.name} is no longer running after file download`, 2);
|
||||||
|
resolve(new Zotero.Sync.Storage.Result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug("Finished download of " + destPath);
|
||||||
|
|
||||||
|
resolve(await Zotero.Sync.Storage.Local.processDownload(requestData));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||||
|
// If S3 connection is interrupted, delay and retry, or bail if too many
|
||||||
|
// consecutive failures
|
||||||
|
if (e.xmlhttp.status == 0) {
|
||||||
|
if (++this._s3ConsecutiveFailures < this._maxS3ConsecutiveFailures) {
|
||||||
|
let libraryKey = item.libraryKey;
|
||||||
|
let msg = "S3 returned 0 for " + libraryKey + " -- retrying download";
|
||||||
|
Zotero.logError(msg);
|
||||||
|
if (this._s3Backoff < this._maxS3Backoff) {
|
||||||
|
this._s3Backoff *= 2;
|
||||||
|
}
|
||||||
|
Zotero.debug("Delaying " + libraryKey + " download for "
|
||||||
|
+ this._s3Backoff + " seconds", 2);
|
||||||
|
Zotero.Promise.delay(this._s3Backoff * 1000)
|
||||||
|
.then(function () {
|
||||||
|
resolve(this.downloadFile(request));
|
||||||
|
}.bind(this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug(this._s3ConsecutiveFailures
|
||||||
|
+ " consecutive S3 failures -- aborting", 1);
|
||||||
|
this._s3ConsecutiveFailures = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Zotero.logError(e);
|
||||||
|
reject(new Error(Zotero.Sync.Storage.defaultError));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
uploadFile: Zotero.Promise.coroutine(function* (request) {
|
uploadFile: Zotero.Promise.coroutine(function* (request) {
|
||||||
|
|
@ -1076,7 +1028,9 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = {
|
||||||
var filename = item.attachmentFilename;
|
var filename = item.attachmentFilename;
|
||||||
var fileSize = (await OS.File.stat(item.getFilePath())).size;
|
var fileSize = (await OS.File.stat(item.getFilePath())).size;
|
||||||
|
|
||||||
text += "\n\n" + filename + " (" + Math.round(fileSize / 1024) + " KB)";
|
text += "\n\n" + filename
|
||||||
|
// TODO: Format more intelligently (e.g., use MB or GB for large files)
|
||||||
|
+ " (" + Zotero.Utilities.numberFormat(Math.round(fileSize / 1024), 0) + " KB)";
|
||||||
|
|
||||||
var e = new Zotero.Error(
|
var e = new Zotero.Error(
|
||||||
text,
|
text,
|
||||||
|
|
|
||||||
|
|
@ -933,6 +933,22 @@ Zotero.Sync.Data.Local = {
|
||||||
['mtime', 'md5', 'dateAdded', 'dateModified']
|
['mtime', 'md5', 'dateAdded', 'dateModified']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If local object became a child item and remote was added to any
|
||||||
|
// collections, we need to remove the 'collections' changes and add
|
||||||
|
// the parent item to those collections instead
|
||||||
|
if (objectType == 'item'
|
||||||
|
&& !obj.isTopLevelItem()
|
||||||
|
&& (obj.isNote() || obj.isAttachment())) {
|
||||||
|
let collections = result.changes
|
||||||
|
.filter(x => x.field == 'collections' && x.op == 'member-add')
|
||||||
|
.map(x => x.value);
|
||||||
|
if (collections.length) {
|
||||||
|
result.changes = result.changes
|
||||||
|
.filter(x => !(x.field == 'collections' && x.op == 'member-add'));
|
||||||
|
saveOptions.newParentItemCollections = collections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If no changes, just update local version number and mark as synced
|
// If no changes, just update local version number and mark as synced
|
||||||
if (!result.changes.length && !result.conflicts.length) {
|
if (!result.changes.length && !result.conflicts.length) {
|
||||||
Zotero.debug("No remote changes to apply to local "
|
Zotero.debug("No remote changes to apply to local "
|
||||||
|
|
@ -1542,6 +1558,24 @@ Zotero.Sync.Data.Local = {
|
||||||
Zotero.Libraries.get(obj.libraryID).storageDownloadNeeded = true;
|
Zotero.Libraries.get(obj.libraryID).storageDownloadNeeded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See explanation in processObjectsFromJSON()
|
||||||
|
if (options.newParentItemCollections) {
|
||||||
|
let parentItem = obj.parentItem;
|
||||||
|
for (let c of options.newParentItemCollections) {
|
||||||
|
parentItem.addToCollection(c);
|
||||||
|
}
|
||||||
|
yield parentItem.save({
|
||||||
|
skipEditCheck: true,
|
||||||
|
skipDateModifiedUpdate: true,
|
||||||
|
skipSelect: true,
|
||||||
|
notifierQueue: options.notifierQueue,
|
||||||
|
// Errors are logged elsewhere, so skip in DataObject.save()
|
||||||
|
errorHandler: function (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// For now, allow sync to proceed after all errors
|
// For now, allow sync to proceed after all errors
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
}
|
}
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
background: !!options.background,
|
background: !!options.background,
|
||||||
firstInSession: _firstInSession,
|
firstInSession: options.firstInSession,
|
||||||
resetMode: options.resetMode
|
resetMode: options.resetMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -913,14 +913,25 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.end = Zotero.Promise.coroutine(function* (options) {
|
this.end = async function (options) {
|
||||||
_syncInProgress = false;
|
_syncInProgress = false;
|
||||||
yield this.checkErrors(_errors, options);
|
await this.checkErrors(_errors, options);
|
||||||
if (!options.restartSync) {
|
if (!options.restartSync) {
|
||||||
this.updateIcons(_errors);
|
this.updateIcons(_errors);
|
||||||
|
|
||||||
|
// If foreground sync, trigger dialog button immediately for some errors
|
||||||
|
// (e.g., long tag fixer)
|
||||||
|
if (!options.background && _errors.length) {
|
||||||
|
if (_errors[0].dialogButtonImmediate) {
|
||||||
|
let maybePromise = _errors[0].dialogButtonCallback();
|
||||||
|
if (maybePromise && maybePromise.then) {
|
||||||
|
await maybePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_errors = [];
|
_errors = [];
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1253,6 +1264,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
|
|
||||||
options.restartSync = true;
|
options.restartSync = true;
|
||||||
});
|
});
|
||||||
|
e.dialogButtonImmediate = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Note too long
|
// Note too long
|
||||||
|
|
@ -1296,14 +1308,6 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not a background sync, show dialog immediately
|
|
||||||
if (!options.background && e.dialogButtonCallback) {
|
|
||||||
let maybePromise = e.dialogButtonCallback();
|
|
||||||
if (maybePromise && maybePromise.then) {
|
|
||||||
yield maybePromise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Show warning for unknown data that couldn't be saved
|
// Show warning for unknown data that couldn't be saved
|
||||||
|
|
|
||||||
|
|
@ -468,6 +468,8 @@ Zotero.Utilities.Internal = {
|
||||||
* @param {Zotero.CookieSandbox} [cookieSandbox]
|
* @param {Zotero.CookieSandbox} [cookieSandbox]
|
||||||
*/
|
*/
|
||||||
saveURI: function (wbp, uri, target, headers, cookieSandbox) {
|
saveURI: function (wbp, uri, target, headers, cookieSandbox) {
|
||||||
|
Zotero.warn("Zotero.Utilities.Internal.saveURI() is deprecated -- use Zotero.HTTP.download()");
|
||||||
|
|
||||||
// Handle gzip encoding
|
// Handle gzip encoding
|
||||||
wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
|
wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
|
||||||
// If not explicitly using cache, skip it
|
// If not explicitly using cache, skip it
|
||||||
|
|
@ -938,11 +940,13 @@ Zotero.Utilities.Internal = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
for (let i = 0; i < item.tags.length; i++) {
|
if (item.tags) {
|
||||||
if (!item.tags[i].type) {
|
for (let i = 0; i < item.tags.length; i++) {
|
||||||
item.tags[i].type = 0;
|
if (!item.tags[i].type) {
|
||||||
|
item.tags[i].type = 0;
|
||||||
|
}
|
||||||
|
// No translator ever used "primary", "fields", or "linkedItems" objects
|
||||||
}
|
}
|
||||||
// No translator ever used "primary", "fields", or "linkedItems" objects
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "related" was never used (array of itemIDs)
|
// "related" was never used (array of itemIDs)
|
||||||
|
|
@ -2442,13 +2446,17 @@ Zotero.Utilities.Internal = {
|
||||||
let markup = this._titleMarkup[token];
|
let markup = this._titleMarkup[token];
|
||||||
if (markup.beginsTag) {
|
if (markup.beginsTag) {
|
||||||
if (targetNode) {
|
if (targetNode) {
|
||||||
let node = targetNode.ownerDocument.createElement(markup.beginsTag);
|
let node;
|
||||||
if (markup.style) {
|
|
||||||
Object.assign(node.style, markup.style);
|
|
||||||
}
|
|
||||||
if (markup.inverseStyle && markupStack.some(otherMarkup => otherMarkup.beginsTag === markup.beginsTag)) {
|
if (markup.inverseStyle && markupStack.some(otherMarkup => otherMarkup.beginsTag === markup.beginsTag)) {
|
||||||
|
node = targetNode.ownerDocument.createElement('span');
|
||||||
Object.assign(node.style, markup.inverseStyle);
|
Object.assign(node.style, markup.inverseStyle);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
node = targetNode.ownerDocument.createElement(markup.beginsTag);
|
||||||
|
if (markup.style) {
|
||||||
|
Object.assign(node.style, markup.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
nodeStack.push(node);
|
nodeStack.push(node);
|
||||||
}
|
}
|
||||||
markupStack.push({ ...markup, token });
|
markupStack.push({ ...markup, token });
|
||||||
|
|
@ -2461,12 +2469,14 @@ Zotero.Utilities.Internal = {
|
||||||
let discardedNode = nodeStack.pop();
|
let discardedNode = nodeStack.pop();
|
||||||
if (discardedMarkup.beginsTag === markup.endsTag) {
|
if (discardedMarkup.beginsTag === markup.endsTag) {
|
||||||
nodeStack[nodeStack.length - 1].append(discardedNode);
|
nodeStack[nodeStack.length - 1].append(discardedNode);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
nodeStack[nodeStack.length - 1].append(discardedMarkup.token, ...discardedNode.childNodes);
|
nodeStack[nodeStack.length - 1].append(discardedMarkup.token, ...discardedNode.childNodes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (discardedMarkup.beginsTag === markup.endsTag) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -2906,6 +2916,85 @@ Zotero.Utilities.Internal.getProcessID = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.Utilities.Internal.Environment = {
|
||||||
|
/**
|
||||||
|
* Unset an environment variable
|
||||||
|
*
|
||||||
|
* Since nsIEnvironment doesn't have an unset method
|
||||||
|
*/
|
||||||
|
unset: function (varName) {
|
||||||
|
Components.utils.import("resource://gre/modules/ctypes.jsm");
|
||||||
|
|
||||||
|
let lib;
|
||||||
|
let result;
|
||||||
|
let success;
|
||||||
|
// Windows
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew
|
||||||
|
if (Zotero.isWin) {
|
||||||
|
lib = ctypes.open("kernel32.dll");
|
||||||
|
let SetEnvironmentVariable = lib.declare(
|
||||||
|
"SetEnvironmentVariableW",
|
||||||
|
ctypes.winapi_abi,
|
||||||
|
ctypes.int, // Return type
|
||||||
|
ctypes.char16_t.ptr, // Argument type: LPCTSTR (variable name)
|
||||||
|
ctypes.char16_t.ptr // Argument type: LPCTSTR (value, NULL to unset)
|
||||||
|
);
|
||||||
|
result = SetEnvironmentVariable(varName, null);
|
||||||
|
success = result != 0; // "If the function succeeds, the return value is nonzero."
|
||||||
|
}
|
||||||
|
// macOS or Linux
|
||||||
|
else {
|
||||||
|
lib = ctypes.open(Zotero.isMac ? "/usr/lib/libSystem.dylib" : "libc.so.6");
|
||||||
|
let unsetenv = lib.declare(
|
||||||
|
"unsetenv",
|
||||||
|
ctypes.default_abi,
|
||||||
|
ctypes.int, // Return type
|
||||||
|
ctypes.char.ptr // Argument type: const char *
|
||||||
|
);
|
||||||
|
result = unsetenv(varName);
|
||||||
|
success = result == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the result
|
||||||
|
if (success) {
|
||||||
|
//Zotero.debug(`Unset environment variable ${varName}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.logError(`Failed to unset environment variable ${varName} (${result})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lib.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the Mozilla environment variables that we changed in the launcher
|
||||||
|
*
|
||||||
|
* Call this before launching another process that might be Firefox so that we don't end up
|
||||||
|
* using the wrong Firefox profile when launching URLs or PDFs. On Windows, it's not necessary
|
||||||
|
* to call this when launching URLs, only processes.
|
||||||
|
*
|
||||||
|
* https://github.com/zotero/zotero/issues/4981
|
||||||
|
*/
|
||||||
|
clearMozillaVariables: function () {
|
||||||
|
this.unset("MOZ_ALLOW_DOWNGRADE");
|
||||||
|
this.unset("MOZ_LEGACY_PROFILES");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set the Mozilla environment variables that we changed in the launcher
|
||||||
|
*
|
||||||
|
* Call this in a finally() after using unsetMozillaVariables(). This mostly isn't necessary,
|
||||||
|
* since most new launches of Zotero would use the modified launcher, but a restart on Linux
|
||||||
|
* skips our shell script where we set these variables.
|
||||||
|
*/
|
||||||
|
restoreMozillaVariables: function () {
|
||||||
|
var env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||||
|
env.set("MOZ_ALLOW_DOWNGRADE", "1");
|
||||||
|
env.set("MOZ_LEGACY_PROFILES", "1");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base64 encode / decode
|
* Base64 encode / decode
|
||||||
* From http://www.webtoolkit.info/
|
* From http://www.webtoolkit.info/
|
||||||
|
|
@ -3048,9 +3137,18 @@ Zotero.Utilities.Internal.OpenURL = {
|
||||||
);
|
);
|
||||||
if (co) {
|
if (co) {
|
||||||
let base = Zotero.Prefs.get("openURL.resolver");
|
let base = Zotero.Prefs.get("openURL.resolver");
|
||||||
// Add & if there's already a ?
|
if (!base.endsWith('?')) {
|
||||||
let splice = base.indexOf("?") == -1 ? "?" : "&";
|
if (base.includes('login?url=')) {
|
||||||
return base + splice + co;
|
base += '?';
|
||||||
|
}
|
||||||
|
else if (base.includes('?')) {
|
||||||
|
base += '&';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
base += '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base + co;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -996,6 +996,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
*/
|
*/
|
||||||
this.launchFile = function (file) {
|
this.launchFile = function (file) {
|
||||||
file = Zotero.File.pathToFile(file);
|
file = Zotero.File.pathToFile(file);
|
||||||
|
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Zotero.debug("Launching " + file.path);
|
Zotero.debug("Launching " + file.path);
|
||||||
file.launch();
|
file.launch();
|
||||||
|
|
@ -1038,6 +1041,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1062,8 +1068,12 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
args = [filePath];
|
args = [filePath];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
|
|
||||||
// Async, but we don't want to block
|
// Async, but we don't want to block
|
||||||
Zotero.Utilities.Internal.exec(applicationPath, args);
|
var promise = Zotero.Utilities.Internal.exec(applicationPath, args);
|
||||||
|
|
||||||
|
promise.finally(() => Zotero.Utilities.Internal.Environment.restoreMozillaVariables());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1093,12 +1103,29 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
if (!found.value) {
|
if (!found.value) {
|
||||||
throw new Error(`Handler not found for '${scheme}' URLs`);
|
throw new Error(`Handler not found for '${scheme}' URLs`);
|
||||||
}
|
}
|
||||||
svc.loadURI(Services.io.newURI(url, null, null));
|
try {
|
||||||
|
if (!Zotero.isWin) {
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.loadURI(Services.io.newURI(url, null, null));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (!Zotero.isWin) {
|
||||||
|
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mozCleared = false;
|
||||||
try {
|
try {
|
||||||
|
if (!Zotero.isWin) {
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
|
mozCleared = true;
|
||||||
|
}
|
||||||
|
|
||||||
var uri = Services.io.newURI(url, null, null);
|
var uri = Services.io.newURI(url, null, null);
|
||||||
var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
|
var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1']
|
||||||
.getService(Components.interfaces.nsIExternalProtocolService)
|
.getService(Components.interfaces.nsIExternalProtocolService)
|
||||||
|
|
@ -1123,6 +1150,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
+ "check extensions.zotero." + pref + " in about:config");
|
+ "check extensions.zotero." + pref + " in about:config");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mozCleared) {
|
||||||
|
Zotero.Utilities.Internal.Environment.clearMozillaVariables();
|
||||||
|
mozCleared = true;
|
||||||
|
}
|
||||||
|
|
||||||
var proc = Components.classes["@mozilla.org/process/util;1"]
|
var proc = Components.classes["@mozilla.org/process/util;1"]
|
||||||
.createInstance(Components.interfaces.nsIProcess);
|
.createInstance(Components.interfaces.nsIProcess);
|
||||||
proc.init(exec);
|
proc.init(exec);
|
||||||
|
|
@ -1130,6 +1162,11 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
var args = [url];
|
var args = [url];
|
||||||
proc.runw(false, args, args.length);
|
proc.runw(false, args, args.length);
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
if (mozCleared) {
|
||||||
|
Zotero.Utilities.Internal.Environment.restoreMozillaVariables();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1721,9 +1758,7 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
return Zotero.Tags.purge();
|
return Zotero.Tags.purge();
|
||||||
});
|
});
|
||||||
yield Zotero.Fulltext.purgeUnusedWords();
|
yield Zotero.Fulltext.purgeUnusedWords();
|
||||||
yield Zotero.DB.executeTransaction(async function () {
|
yield Zotero.Items.purge();
|
||||||
return Zotero.Items.purge();
|
|
||||||
});
|
|
||||||
// DEBUG: this might not need to be permanent
|
// DEBUG: this might not need to be permanent
|
||||||
//yield Zotero.DB.executeTransaction(async function () {
|
//yield Zotero.DB.executeTransaction(async function () {
|
||||||
// return Zotero.Relations.purge();
|
// return Zotero.Relations.purge();
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,6 @@ const xpcomFilesLocal = [
|
||||||
'storage/storageRequest',
|
'storage/storageRequest',
|
||||||
'storage/storageResult',
|
'storage/storageResult',
|
||||||
'storage/storageUtilities',
|
'storage/storageUtilities',
|
||||||
'storage/streamListener',
|
|
||||||
'storage/zfs',
|
'storage/zfs',
|
||||||
'storage/webdav',
|
'storage/webdav',
|
||||||
'syncedSettings',
|
'syncedSettings',
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,6 @@ var ZoteroPane = new function()
|
||||||
this.handleKeyPress = handleKeyPress;
|
this.handleKeyPress = handleKeyPress;
|
||||||
this.getSelectedCollection = getSelectedCollection;
|
this.getSelectedCollection = getSelectedCollection;
|
||||||
this.getSelectedSavedSearch = getSelectedSavedSearch;
|
this.getSelectedSavedSearch = getSelectedSavedSearch;
|
||||||
this.getSelectedItems = getSelectedItems;
|
|
||||||
this.getSortedItems = getSortedItems;
|
|
||||||
this.getSortField = getSortField;
|
this.getSortField = getSortField;
|
||||||
this.getSortDirection = getSortDirection;
|
this.getSortDirection = getSortDirection;
|
||||||
this.setItemsPaneMessage = setItemsPaneMessage;
|
this.setItemsPaneMessage = setItemsPaneMessage;
|
||||||
|
|
@ -1936,7 +1934,7 @@ var ZoteroPane = new function()
|
||||||
|
|
||||||
// Tab selection observer in standalone.js makes sure that
|
// Tab selection observer in standalone.js makes sure that
|
||||||
// updateQuickCopyCommands is called
|
// updateQuickCopyCommands is called
|
||||||
if (Zotero_Tabs.selectedID == 'zotero-pane') {
|
if (Zotero_Tabs.selectedType == 'library') {
|
||||||
this.updateQuickCopyCommands(selectedItems);
|
this.updateQuickCopyCommands(selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2654,21 +2652,10 @@ var ZoteroPane = new function()
|
||||||
|
|
||||||
this.copySelectedItemsToClipboard = function (asCitations) {
|
this.copySelectedItemsToClipboard = function (asCitations) {
|
||||||
var items = [];
|
var items = [];
|
||||||
if (Zotero_Tabs.selectedID == 'zotero-pane') {
|
let itemIDs = this.getSelectedItems(true);
|
||||||
let itemIDs = this.getSelectedItems(true);
|
// Get selected item IDs in the item tree order
|
||||||
// Get selected item IDs in the item tree order
|
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
|
||||||
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
|
items = Zotero.Items.get(itemIDs);
|
||||||
items = Zotero.Items.get(itemIDs);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
|
||||||
if (reader) {
|
|
||||||
let item = Zotero.Items.get(reader.itemID);
|
|
||||||
if (item.parentItem) {
|
|
||||||
items = [item.parentItem];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -3070,14 +3057,28 @@ var ZoteroPane = new function()
|
||||||
*
|
*
|
||||||
* If asIDs is true, return an array of itemIDs instead
|
* If asIDs is true, return an array of itemIDs instead
|
||||||
*/
|
*/
|
||||||
function getSelectedItems(asIDs)
|
this.getSelectedItems = function (asIDs) {
|
||||||
{
|
switch (Zotero_Tabs.selectedType) {
|
||||||
if (!this.itemsView) {
|
case 'library':
|
||||||
return [];
|
if (!this.itemsView) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.itemsView.getSelectedItems(asIDs);
|
||||||
|
case 'reader': {
|
||||||
|
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||||
|
if (reader) {
|
||||||
|
let item = Zotero.Items.get(reader.itemID);
|
||||||
|
if (item.parentItem) {
|
||||||
|
item = item.parentItem;
|
||||||
|
}
|
||||||
|
return asIDs ? [item.id] : [item];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return this.itemsView.getSelectedItems(asIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -3085,13 +3086,18 @@ var ZoteroPane = new function()
|
||||||
*
|
*
|
||||||
* If asIDs is true, return an array of itemIDs instead
|
* If asIDs is true, return an array of itemIDs instead
|
||||||
*/
|
*/
|
||||||
function getSortedItems(asIDs) {
|
this.getSortedItems = function (asIDs) {
|
||||||
if (!this.itemsView) {
|
switch (Zotero_Tabs.selectedType) {
|
||||||
return [];
|
case 'library':
|
||||||
|
if (!this.itemsView) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.itemsView.getSortedItems(asIDs);
|
||||||
|
default:
|
||||||
|
// ALl non-library tabs: Visible items == "selected" items
|
||||||
|
return this.getSelectedItems(asIDs);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return this.itemsView.getSortedItems(asIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getSortField() {
|
function getSortField() {
|
||||||
|
|
@ -4510,22 +4516,14 @@ var ZoteroPane = new function()
|
||||||
/**
|
/**
|
||||||
* @param {Boolean} [link]
|
* @param {Boolean} [link]
|
||||||
* @param {Number} [parentItemID]
|
* @param {Number} [parentItemID]
|
||||||
* @param {nsIFile[]} [files] Used instead of showing a file picker - for tests
|
* @param {String[]} [files] Used instead of showing a file picker - for tests
|
||||||
* @returns {Promise<Zotero.Item[] | null>}
|
* @returns {Promise<Zotero.Item[] | null>}
|
||||||
*/
|
*/
|
||||||
this.addAttachmentFromDialog = async function (link, parentItemID, files = null) {
|
this.addAttachmentFromDialog = async function (link, parentItemID, files = null) {
|
||||||
if (!this.canEdit()) {
|
var libraryID;
|
||||||
this.displayCannotEditLibraryMessage();
|
if (Zotero_Tabs.selectedType === 'library') {
|
||||||
return null;
|
let collectionTreeRow = this.getCollectionTreeRow();
|
||||||
}
|
if (link && collectionTreeRow.isPublications()) {
|
||||||
|
|
||||||
var collectionTreeRow = this.getCollectionTreeRow();
|
|
||||||
if (link) {
|
|
||||||
if (collectionTreeRow.isWithinGroup()) {
|
|
||||||
Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
else if (collectionTreeRow.isPublications()) {
|
|
||||||
Zotero.alert(
|
Zotero.alert(
|
||||||
null,
|
null,
|
||||||
Zotero.getString('general.error'),
|
Zotero.getString('general.error'),
|
||||||
|
|
@ -4533,15 +4531,25 @@ var ZoteroPane = new function()
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
libraryID = collectionTreeRow.ref.libraryID;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
libraryID = Zotero.Items.get(parentItemID).libraryID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.canEdit()) {
|
||||||
|
this.displayCannotEditLibraryMessage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// TODO: disable in menu
|
// TODO: disable in menu
|
||||||
if (!this.canEditFiles()) {
|
if (!this.canEditFiles()) {
|
||||||
this.displayCannotEditLibraryFilesMessage();
|
this.displayCannotEditLibraryFilesMessage();
|
||||||
return;
|
return null;
|
||||||
|
}
|
||||||
|
if (link && Zotero.Libraries.get(libraryID).isGroup) {
|
||||||
|
Zotero.alert(null, "", "Linked files cannot be added to group libraries.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryID = collectionTreeRow.ref.libraryID;
|
|
||||||
|
|
||||||
if (!files) {
|
if (!files) {
|
||||||
var fp = new FilePicker();
|
var fp = new FilePicker();
|
||||||
|
|
@ -5369,36 +5377,48 @@ var ZoteroPane = new function()
|
||||||
/**
|
/**
|
||||||
* Test if the user can edit the currently selected view
|
* Test if the user can edit the currently selected view
|
||||||
*
|
*
|
||||||
* @param {Integer} [row]
|
* @param {Integer} [row] Row index - ignored if not in library tab
|
||||||
*
|
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
|
||||||
*/
|
*/
|
||||||
this.canEdit = function (row) {
|
this.canEdit = function (row) {
|
||||||
// Currently selected row
|
switch (Zotero_Tabs.selectedType) {
|
||||||
if (row === undefined) {
|
case 'library':
|
||||||
row = this.collectionsView.selection.focused;
|
// Currently selected row
|
||||||
|
if (row === undefined) {
|
||||||
|
row = this.collectionsView.selection.focused;
|
||||||
|
}
|
||||||
|
return this.collectionsView.getRow(row).editable;
|
||||||
|
case 'reader': {
|
||||||
|
let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
|
||||||
|
if (!itemID) {
|
||||||
|
throw new Error('Reader tab has no itemID');
|
||||||
|
}
|
||||||
|
return Zotero.Items.get(itemID).library.editable;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
|
||||||
return collectionTreeRow.editable;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test if the user can edit the parent library of the selected view
|
* Test if the user can edit the parent library of the selected view
|
||||||
*
|
*
|
||||||
* @param {Integer} [row]
|
* @param {Integer} [row] Row index - ignored if not in library tab
|
||||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||||
*/
|
*/
|
||||||
this.canEditLibrary = function (row) {
|
this.canEditLibrary = function (row) {
|
||||||
// Currently selected row
|
switch (Zotero_Tabs.selectedType) {
|
||||||
if (row === undefined) {
|
case 'library': // Currently selected row
|
||||||
row = this.collectionsView.selection.focused;
|
if (row === undefined) {
|
||||||
|
row = this.collectionsView.selection.focused;
|
||||||
|
}
|
||||||
|
return Zotero.Libraries.get(this.collectionsView.getRow(row).ref.libraryID).editable;
|
||||||
|
default:
|
||||||
|
// All non-library tabs: canEditLibrary() == canEdit()
|
||||||
|
return this.canEdit(row);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
|
||||||
return Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -5409,14 +5429,24 @@ var ZoteroPane = new function()
|
||||||
* @return {Boolean} TRUE if user can edit, FALSE if not
|
* @return {Boolean} TRUE if user can edit, FALSE if not
|
||||||
*/
|
*/
|
||||||
this.canEditFiles = function (row) {
|
this.canEditFiles = function (row) {
|
||||||
// Currently selected row
|
switch (Zotero_Tabs.selectedType) {
|
||||||
if (row === undefined) {
|
case 'library':
|
||||||
row = this.collectionsView.selection.focused;
|
// Currently selected row
|
||||||
|
if (row === undefined) {
|
||||||
|
row = this.collectionsView.selection.focused;
|
||||||
|
}
|
||||||
|
return this.collectionsView.getRow(row).filesEditable;
|
||||||
|
case 'reader': {
|
||||||
|
let itemID = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID)?.itemID;
|
||||||
|
if (!itemID) {
|
||||||
|
throw new Error('Reader tab has no itemID');
|
||||||
|
}
|
||||||
|
return Zotero.Items.get(itemID).library.filesEditable;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
var collectionTreeRow = this.collectionsView.getRow(row);
|
|
||||||
return collectionTreeRow.filesEditable;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.displayCannotEditLibraryMessage = function () {
|
this.displayCannotEditLibraryMessage = function () {
|
||||||
|
|
@ -6814,13 +6844,14 @@ var ZoteroPane = new function()
|
||||||
};
|
};
|
||||||
|
|
||||||
this.buildFieldTransformMenu = function ({ target, onTransform }) {
|
this.buildFieldTransformMenu = function ({ target, onTransform }) {
|
||||||
|
let doc = target.ownerDocument;
|
||||||
let value = target.value;
|
let value = target.value;
|
||||||
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
|
let valueTitleCased = Zotero.Utilities.capitalizeTitle(value.toLowerCase(), true);
|
||||||
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
|
let valueSentenceCased = Zotero.Utilities.sentenceCase(value);
|
||||||
|
|
||||||
let menupopup = document.createXULElement('menupopup');
|
let menupopup = doc.createXULElement('menupopup');
|
||||||
|
|
||||||
let titleCase = document.createXULElement('menuitem');
|
let titleCase = doc.createXULElement('menuitem');
|
||||||
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
|
titleCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.titlecase'));
|
||||||
titleCase.addEventListener('command', () => {
|
titleCase.addEventListener('command', () => {
|
||||||
onTransform(valueTitleCased);
|
onTransform(valueTitleCased);
|
||||||
|
|
@ -6828,7 +6859,7 @@ var ZoteroPane = new function()
|
||||||
titleCase.disabled = valueTitleCased == value;
|
titleCase.disabled = valueTitleCased == value;
|
||||||
menupopup.append(titleCase);
|
menupopup.append(titleCase);
|
||||||
|
|
||||||
let sentenceCase = document.createXULElement('menuitem');
|
let sentenceCase = doc.createXULElement('menuitem');
|
||||||
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
|
sentenceCase.setAttribute('label', Zotero.getString('zotero.item.textTransform.sentencecase'));
|
||||||
sentenceCase.addEventListener('command', () => {
|
sentenceCase.addEventListener('command', () => {
|
||||||
onTransform(valueSentenceCased);
|
onTransform(valueSentenceCased);
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,7 @@
|
||||||
command="cmd_find"/>
|
command="cmd_find"/>
|
||||||
<menuitem id="menu_advancedSearch"
|
<menuitem id="menu_advancedSearch"
|
||||||
label="&zotero.toolbar.advancedSearch;"
|
label="&zotero.toolbar.advancedSearch;"
|
||||||
|
key="key_advancedSearch"
|
||||||
command="cmd_zotero_advancedSearch"/>
|
command="cmd_zotero_advancedSearch"/>
|
||||||
<menuseparator hidden="true" id="textfieldDirection-separator"/>
|
<menuseparator hidden="true" id="textfieldDirection-separator"/>
|
||||||
<menuitem id="textfieldDirection-swap"
|
<menuitem id="textfieldDirection-swap"
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,9 @@ preferences-advanced-language-and-region-title = Kieli ja alue
|
||||||
preferences-advanced-enable-bidi-ui =
|
preferences-advanced-enable-bidi-ui =
|
||||||
.label = Käytä kaksisuuntaisen tekstin editointiohjelmia
|
.label = Käytä kaksisuuntaisen tekstin editointiohjelmia
|
||||||
preferences-advanced-reset-data-dir =
|
preferences-advanced-reset-data-dir =
|
||||||
.label = Revert to Default Location…
|
.label = Palauta vakiosijaintiin…
|
||||||
preferences-advanced-custom-data-dir =
|
preferences-advanced-custom-data-dir =
|
||||||
.label = Use Custom Location…
|
.label = Käytä omaa sijaintia…
|
||||||
preferences-advanced-default-data-dir =
|
preferences-advanced-default-data-dir =
|
||||||
.value = (Default: { $directory })
|
.value = (Oletus: { $directory })
|
||||||
.aria-label = Oletussijainti
|
.aria-label = Oletussijainti
|
||||||
|
|
|
||||||
|
|
@ -48,18 +48,18 @@ pdfReader-import-from-epub =
|
||||||
.label = Tuo e-kirjan huomautukset...
|
.label = Tuo e-kirjan huomautukset...
|
||||||
pdfReader-import-from-epub-prompt-title = Tuo e-kirjan huomautukset
|
pdfReader-import-from-epub-prompt-title = Tuo e-kirjan huomautukset
|
||||||
pdfReader-import-from-epub-prompt-text =
|
pdfReader-import-from-epub-prompt-text =
|
||||||
{ -app-name } found { $count ->
|
{ -app-name } löysi { $count ->
|
||||||
[1] { $count } { $tool } annotation
|
[1] { $count } { $tool } huomautuksen
|
||||||
*[other] { $count } { $tool } annotations
|
*[other] { $count } { $tool } huomautusta
|
||||||
}, last edited { $lastModifiedRelative }.
|
}, muokattu viimeksi { $lastModifiedRelative }.
|
||||||
|
|
||||||
Any { -app-name } annotations that were previously imported from this ebook will be updated.
|
Kaikki { -app-name }-huomautukset jotka on tuotu aiemmin tästä e-kirjasta päivitetään.
|
||||||
pdfReader-import-from-epub-no-annotations-current-file =
|
pdfReader-import-from-epub-no-annotations-current-file =
|
||||||
This ebook does not appear to contain any importable annotations.
|
Tämä e-kirja ei vaikuta sisältävän tuotavia huomautuksia.
|
||||||
|
|
||||||
{ -app-name } can import ebook annotations created in Calibre and KOReader.
|
{ -app-name } voi tuoda e-kirjan huomautukset jotka on luotu Calibrella tai KOReaderilla.
|
||||||
pdfReader-import-from-epub-no-annotations-other-file =
|
pdfReader-import-from-epub-no-annotations-other-file =
|
||||||
“{ $filename }” does not appear to contain any Calibre or KOReader annotations.
|
“{ $filename }” ei vaikuta sisältävän Calibrella tai KOReaderilla luotuja huomautuksia.
|
||||||
|
|
||||||
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
|
Jos tämän e-kirjan huomautukset on tehty KOReaderilla, koita valita “metadata.epub.lua”-tiedosto suoraan.
|
||||||
pdfReader-import-from-epub-select-other = Select Other File…
|
pdfReader-import-from-epub-select-other = Valitse toinen tiedosto…
|
||||||
|
|
|
||||||
|
|
@ -285,11 +285,11 @@ integration-editBibliography-remove-button =
|
||||||
.aria-label = { general-remove }
|
.aria-label = { general-remove }
|
||||||
integration-editBibliography-editor =
|
integration-editBibliography-editor =
|
||||||
.aria-label = Vuokkaa viitettä
|
.aria-label = Vuokkaa viitettä
|
||||||
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }.
|
-integration-editBibliography-include-uncited = Sisällyttääksesi viittaamattoman nimikkeen lähdeluetteloosi, valitse se nimikelistalta ja paina { general-add }.
|
||||||
-integration-editBibliography-exclude-cited = You can also exclude a cited item by selecting it from the list of references and pressing { general-remove }.
|
-integration-editBibliography-exclude-cited = Voit myös jättää pois viitatun nimikkeen lähdeluettelosta valitsemalla sen ja painamalla { general-remove }.
|
||||||
-integration-editBibliography-edit-reference = To change how a reference is formatted, use the text editor.
|
-integration-editBibliography-edit-reference = Muuttaaksesi viitteen muotoilua, käytä tekstieditoria.
|
||||||
integration-editBibliography-wrapper =
|
integration-editBibliography-wrapper =
|
||||||
.aria-label = Edit Bibliography dialog
|
.aria-label = Muokkaa lähdeluetteloa -valintaikkuna
|
||||||
.aria-description =
|
.aria-description =
|
||||||
{ -integration-editBibliography-include-uncited }
|
{ -integration-editBibliography-include-uncited }
|
||||||
{ -integration-editBibliography-exclude-cited }
|
{ -integration-editBibliography-exclude-cited }
|
||||||
|
|
@ -391,7 +391,7 @@ menu-ui-density-comfortable =
|
||||||
.label = Väljempi
|
.label = Väljempi
|
||||||
menu-ui-density-compact =
|
menu-ui-density-compact =
|
||||||
.label = Tiivis
|
.label = Tiivis
|
||||||
pane-item-details = Item Details
|
pane-item-details = Nimikkeen tiedot
|
||||||
pane-info = Tiedot
|
pane-info = Tiedot
|
||||||
pane-abstract = Tiivistelmä
|
pane-abstract = Tiivistelmä
|
||||||
pane-attachments = Liitteet
|
pane-attachments = Liitteet
|
||||||
|
|
@ -641,7 +641,7 @@ find-pdf-files-added =
|
||||||
select-items-dialog =
|
select-items-dialog =
|
||||||
.buttonlabelaccept = Valitse
|
.buttonlabelaccept = Valitse
|
||||||
select-items-convertToStandalone =
|
select-items-convertToStandalone =
|
||||||
.label = Convert to Standalone
|
.label = Muuta erilliseksi
|
||||||
select-items-convertToStandaloneAttachment =
|
select-items-convertToStandaloneAttachment =
|
||||||
.label =
|
.label =
|
||||||
{ $count ->
|
{ $count ->
|
||||||
|
|
@ -651,8 +651,8 @@ select-items-convertToStandaloneAttachment =
|
||||||
select-items-convertToStandaloneNote =
|
select-items-convertToStandaloneNote =
|
||||||
.label =
|
.label =
|
||||||
{ $count ->
|
{ $count ->
|
||||||
[one] Convert to Standalone Note
|
[one] Muuta erilliseksi muistiinpanoksi
|
||||||
*[other] Convert to Standalone Notes
|
*[other] Muuta erillisiksi muistiinpanoiksi
|
||||||
}
|
}
|
||||||
file-type-webpage = Verkkosivu
|
file-type-webpage = Verkkosivu
|
||||||
file-type-image = Kuva
|
file-type-image = Kuva
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,96 @@
|
||||||
<!ENTITY preferencesCmdMac.label "Preferences...">
|
<!ENTITY preferencesCmdMac.label "Postavke…">
|
||||||
<!ENTITY preferencesCmdMac.commandkey ",">
|
<!ENTITY preferencesCmdMac.commandkey ",">
|
||||||
<!ENTITY servicesMenuMac.label "Services">
|
<!ENTITY servicesMenuMac.label "Servisi">
|
||||||
<!ENTITY hideThisAppCmdMac.label "Hide &brandShortName;">
|
<!ENTITY hideThisAppCmdMac.label "Sakrij &brandShortName;">
|
||||||
<!ENTITY hideThisAppCmdMac.commandkey "H">
|
<!ENTITY hideThisAppCmdMac.commandkey "H">
|
||||||
<!ENTITY hideOtherAppsCmdMac.label "Hide Others">
|
<!ENTITY hideOtherAppsCmdMac.label "Sakrij ostalo">
|
||||||
<!ENTITY hideOtherAppsCmdMac.commandkey "H">
|
<!ENTITY hideOtherAppsCmdMac.commandkey "H">
|
||||||
<!ENTITY showAllAppsCmdMac.label "Show All">
|
<!ENTITY showAllAppsCmdMac.label "Prikaži sve">
|
||||||
<!ENTITY quitApplicationCmdMac.label "Quit Zotero">
|
<!ENTITY quitApplicationCmdMac.label "Prekini Zotero">
|
||||||
<!ENTITY quitApplicationCmdMac.key "Q">
|
<!ENTITY quitApplicationCmdMac.key "Q">
|
||||||
|
|
||||||
<!ENTITY fileMenu.label "File">
|
<!ENTITY fileMenu.label "Datoteka">
|
||||||
<!ENTITY fileMenu.accesskey "F">
|
<!ENTITY fileMenu.accesskey "F">
|
||||||
<!ENTITY saveCmd.label "Save…">
|
<!ENTITY saveCmd.label "Spremi…">
|
||||||
<!ENTITY saveCmd.key "S">
|
<!ENTITY saveCmd.key "S">
|
||||||
<!ENTITY saveCmd.accesskey "A">
|
<!ENTITY saveCmd.accesskey "A">
|
||||||
<!ENTITY pageSetupCmd.label "Page Setup…">
|
<!ENTITY pageSetupCmd.label "Postava stranice…">
|
||||||
<!ENTITY pageSetupCmd.accesskey "U">
|
<!ENTITY pageSetupCmd.accesskey "U">
|
||||||
<!ENTITY printCmd.label "Print…">
|
<!ENTITY printCmd.label "Printaj…">
|
||||||
<!ENTITY printCmd.key "P">
|
<!ENTITY printCmd.key "P">
|
||||||
<!ENTITY printCmd.accesskey "P">
|
<!ENTITY printCmd.accesskey "P">
|
||||||
<!ENTITY closeCmd.label "Close">
|
<!ENTITY closeCmd.label "Zatvori">
|
||||||
<!ENTITY closeCmd.key "W">
|
<!ENTITY closeCmd.key "W">
|
||||||
<!ENTITY closeCmd.accesskey "C">
|
<!ENTITY closeCmd.accesskey "C">
|
||||||
<!ENTITY manageAttachments.label "Manage Attachments">
|
<!ENTITY manageAttachments.label "Upravljanje prilozima">
|
||||||
<!ENTITY convertToStored.label "Convert Linked Files to Stored Files…">
|
<!ENTITY convertToStored.label "Pretvori povezane u pohranjene datoteke…">
|
||||||
<!ENTITY importCmd.label "Import...">
|
<!ENTITY importCmd.label "Uvoz…">
|
||||||
<!ENTITY importCmd.key "I">
|
<!ENTITY importCmd.key "I">
|
||||||
<!ENTITY importFromClipboardCmd.label "Import from Clipboard">
|
<!ENTITY importFromClipboardCmd.label "Uvoz iz međuspremnika">
|
||||||
<!ENTITY quitApplicationCmdWin.label "Exit">
|
<!ENTITY quitApplicationCmdWin.label "Izlaz">
|
||||||
<!ENTITY quitApplicationCmdWin.accesskey "x">
|
<!ENTITY quitApplicationCmdWin.accesskey "x">
|
||||||
<!ENTITY quitApplicationCmd.label "Quit">
|
<!ENTITY quitApplicationCmd.label "Prekid">
|
||||||
<!ENTITY quitApplicationCmd.accesskey "Q">
|
<!ENTITY quitApplicationCmd.accesskey "Q">
|
||||||
|
|
||||||
<!ENTITY copyCitationCmd.label "Copy Citation">
|
<!ENTITY copyCitationCmd.label "Kopiraj citat">
|
||||||
<!ENTITY copyBibliographyCmd.label "Copy Bibliography">
|
<!ENTITY copyBibliographyCmd.label "Kopiraj biblioteku">
|
||||||
<!ENTITY copyNoteCmd.label "Copy Note">
|
<!ENTITY copyNoteCmd.label "Kopiraj bilješku">
|
||||||
|
|
||||||
<!ENTITY bidiSwitchPageDirectionItem.label "Switch Page Direction">
|
<!ENTITY bidiSwitchPageDirectionItem.label "Promjena smjera stranice">
|
||||||
<!ENTITY bidiSwitchPageDirectionItem.accesskey "g">
|
<!ENTITY bidiSwitchPageDirectionItem.accesskey "g">
|
||||||
<!ENTITY bidiSwitchTextDirectionItem.label "Switch Text Direction">
|
<!ENTITY bidiSwitchTextDirectionItem.label "Promjena smjera teksta">
|
||||||
<!ENTITY bidiSwitchTextDirectionItem.accesskey "w">
|
<!ENTITY bidiSwitchTextDirectionItem.accesskey "w">
|
||||||
<!ENTITY bidiSwitchTextDirectionItem.commandkey "X">
|
<!ENTITY bidiSwitchTextDirectionItem.commandkey "X">
|
||||||
|
|
||||||
<!ENTITY goMenu.label "Go">
|
<!ENTITY goMenu.label "Idi">
|
||||||
<!ENTITY goMenu.accesskey "G">
|
<!ENTITY goMenu.accesskey "G">
|
||||||
|
|
||||||
<!ENTITY viewMenu.label "View">
|
<!ENTITY viewMenu.label "Pogled">
|
||||||
<!ENTITY viewMenu.accesskey "V">
|
<!ENTITY viewMenu.accesskey "V">
|
||||||
<!ENTITY layout.label "Layout">
|
<!ENTITY layout.label "Izgled">
|
||||||
<!ENTITY standardView.label "Standard View">
|
<!ENTITY standardView.label "Standardni pogled">
|
||||||
<!ENTITY stackedView.label "Stacked View">
|
<!ENTITY stackedView.label "Složeni prikaz">
|
||||||
<!ENTITY collectionsPane.label "Collections Pane">
|
<!ENTITY collectionsPane.label "Okno zbirke">
|
||||||
<!ENTITY itemPane.label "Item Pane">
|
<!ENTITY itemPane.label "Okno stavke">
|
||||||
<!ENTITY tagSelector.label "Tag Selector">
|
<!ENTITY tagSelector.label "Odabir oznaka">
|
||||||
<!ENTITY recursiveCollections.label "Show Items from Subcollections">
|
<!ENTITY recursiveCollections.label "Prikaži stavke iz podzbirki">
|
||||||
<!ENTITY fontSize.label "Font Size">
|
<!ENTITY fontSize.label "Veličina slova">
|
||||||
<!ENTITY noteFontSize.label "Note Font Size">
|
<!ENTITY noteFontSize.label "Veličina slova bilješke">
|
||||||
<!ENTITY columns.label "Columns">
|
<!ENTITY columns.label "Stupac">
|
||||||
<!ENTITY sortBy.label "Sort By">
|
<!ENTITY sortBy.label "Sortiraj po">
|
||||||
|
|
||||||
<!ENTITY toolsMenu.label "Tools">
|
<!ENTITY toolsMenu.label "Alati">
|
||||||
<!ENTITY toolsMenu.accesskey "T">
|
<!ENTITY toolsMenu.accesskey "T">
|
||||||
<!ENTITY installConnector.label "Install Browser Connector">
|
<!ENTITY installConnector.label "Instalirajte Browser Connector…">
|
||||||
<!ENTITY installConnector.accesskey "C">
|
<!ENTITY installConnector.accesskey "C">
|
||||||
<!ENTITY addons.label "Add-ons">
|
<!ENTITY addons.label "Dodaci">
|
||||||
<!ENTITY developer.label "Developer">
|
<!ENTITY developer.label "Programer">
|
||||||
|
|
||||||
<!ENTITY bringAllToFront.label "Bring All to Front">
|
<!ENTITY bringAllToFront.label "Dovedite sve naprijed">
|
||||||
|
|
||||||
<!ENTITY helpMenu.label "Help">
|
<!ENTITY helpMenu.label "Pomoć">
|
||||||
<!ENTITY helpMenu.accesskey "H">
|
<!ENTITY helpMenu.accesskey "H">
|
||||||
<!ENTITY helpMenuWin.label "Help">
|
<!ENTITY helpMenuWin.label "Pomoć">
|
||||||
<!ENTITY helpMenuWin.accesskey "H">
|
<!ENTITY helpMenuWin.accesskey "H">
|
||||||
<!ENTITY helpMac.commandkey "?">
|
<!ENTITY helpMac.commandkey "?">
|
||||||
<!ENTITY aboutProduct.label "About &brandShortName;">
|
<!ENTITY aboutProduct.label "O &brandShortName;">
|
||||||
<!ENTITY aboutProduct.accesskey "A">
|
<!ENTITY aboutProduct.accesskey "A">
|
||||||
<!ENTITY productHelp.label "Support and Documentation">
|
<!ENTITY productHelp.label "Potpora i dokumentacija">
|
||||||
<!ENTITY productHelp.accesskey "D">
|
<!ENTITY productHelp.accesskey "D">
|
||||||
<!ENTITY helpTroubleshootingInfo.label "Troubleshooting Information">
|
<!ENTITY helpTroubleshootingInfo.label "Informacije o rješavanju problema">
|
||||||
<!ENTITY helpTroubleshootingInfo.accesskey "T">
|
<!ENTITY helpTroubleshootingInfo.accesskey "T">
|
||||||
<!ENTITY helpFeedbackPage.label "Discussion Forums">
|
<!ENTITY helpFeedbackPage.label "Forumi za raspravu">
|
||||||
<!ENTITY helpFeedbackPage.accesskey "F">
|
<!ENTITY helpFeedbackPage.accesskey "F">
|
||||||
|
|
||||||
<!ENTITY helpReportErrors.label "Report Errors...">
|
<!ENTITY helpReportErrors.label "Prijavi pogreške…">
|
||||||
<!ENTITY helpReportErrors.accesskey "R">
|
<!ENTITY helpReportErrors.accesskey "R">
|
||||||
|
|
||||||
<!ENTITY debugOutputLogging.label "Debug Output Logging">
|
<!ENTITY debugOutputLogging.label "Zapisivanje izlaza otklanjanja pogrešaka">
|
||||||
<!ENTITY debugOutputLogging.accesskey "L">
|
<!ENTITY debugOutputLogging.accesskey "L">
|
||||||
<!ENTITY debugOutputLogging.submit "Submit Output">
|
<!ENTITY debugOutputLogging.submit "Pošalji izlaz">
|
||||||
<!ENTITY debugOutputLogging.view "View Output">
|
<!ENTITY debugOutputLogging.view "Pregled izlaza">
|
||||||
<!ENTITY debugOutputLogging.clear "Clear Output">
|
<!ENTITY debugOutputLogging.clear "Očisti izlaz">
|
||||||
<!ENTITY debugOutputLogging.restartWithLoggingEnabled "Restart with Logging Enabled…">
|
<!ENTITY debugOutputLogging.restartWithLoggingEnabled "Ponovo pokreni s omogućenim zapisivanjem…">
|
||||||
|
|
||||||
<!ENTITY helpCheckForUpdates.label "Check for Updates…">
|
<!ENTITY helpCheckForUpdates.label "Provjera nadopuna…">
|
||||||
<!ENTITY helpCheckForUpdates.accesskey "U">
|
<!ENTITY helpCheckForUpdates.accesskey "U">
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,9 @@ preferences-advanced-language-and-region-title = Kalba ir regonas
|
||||||
preferences-advanced-enable-bidi-ui =
|
preferences-advanced-enable-bidi-ui =
|
||||||
.label = Įgalinti abikrypčius teksto redagavimo įrankius
|
.label = Įgalinti abikrypčius teksto redagavimo įrankius
|
||||||
preferences-advanced-reset-data-dir =
|
preferences-advanced-reset-data-dir =
|
||||||
.label = Revert to Default Location…
|
.label = Atstatyti numatytąją vietą…
|
||||||
preferences-advanced-custom-data-dir =
|
preferences-advanced-custom-data-dir =
|
||||||
.label = Use Custom Location…
|
.label = Naudoti savitą vietą...
|
||||||
preferences-advanced-default-data-dir =
|
preferences-advanced-default-data-dir =
|
||||||
.value = (Default: { $directory })
|
.value = (Numatytoji: { $directory })
|
||||||
.aria-label = Default location
|
.aria-label = Numatytoji vieta
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ pdfReader-selectArea = Pažymėti sritį
|
||||||
pdfReader-draw = Piešti
|
pdfReader-draw = Piešti
|
||||||
pdfReader-highlightAnnotation = Paryškinimas
|
pdfReader-highlightAnnotation = Paryškinimas
|
||||||
pdfReader-underlineAnnotation = Pabraukimas
|
pdfReader-underlineAnnotation = Pabraukimas
|
||||||
pdfReader-noteAnnotation = Note Annotation
|
pdfReader-noteAnnotation = Pastaba
|
||||||
pdfReader-textAnnotation = Text Annotation
|
pdfReader-textAnnotation = Tekstinė anotacija
|
||||||
pdfReader-imageAnnotation = Image Annotation
|
pdfReader-imageAnnotation = Image Annotation
|
||||||
pdfReader-find-in-document = Paieška dokumente
|
pdfReader-find-in-document = Paieška dokumente
|
||||||
pdfReader-move-annotation-start-key =
|
pdfReader-move-annotation-start-key =
|
||||||
|
|
@ -14,13 +14,13 @@ pdfReader-move-annotation-start-key =
|
||||||
[macos] { general-key-command }
|
[macos] { general-key-command }
|
||||||
*[other] { general-key-alt }
|
*[other] { general-key-alt }
|
||||||
}
|
}
|
||||||
pdfReader-a11yMoveAnnotation = Use the arrow keys to move the annotation.
|
pdfReader-a11yMoveAnnotation = Spausdami rodyklių klavišus galite pastumti anotaciją.
|
||||||
pdfReader-a11yEditTextAnnotation = To move the end of the text annotation, hold { general-key-shift } and use the left/right arrow keys. To move the start of the annotation, hold { general-key-shift }-{ pdfReader-move-annotation-start-key } and use the arrow keys.
|
pdfReader-a11yEditTextAnnotation = Norėdami pakeisti tekstinės anotacijos plotį pagal dešinįjį kraštą (pabaigą), laikykite { general-key-shift } ir spauskite rodyklių klavišus kairėn arba dešinėn. Norėdami pakeisti tekstinės anotacijos plotį pagal kairįjį kraštą (pradžią), laikykite { general-key-shift }-{ pdfReader-move-annotation-start-key } ir spauskite rodyklių klavišus.
|
||||||
pdfReader-a11yResizeAnnotation = To resize the annotation, hold { general-key-shift } and use the arrow keys.
|
pdfReader-a11yResizeAnnotation = Norėdami pakeisti tekstinės anotacijos dydį, laikykite { general-key-shift } ir spauskite rodyklių klavišus aukštyn arba žemyn.
|
||||||
pdfReader-a11yAnnotationPopupAppeared = Use Tab to navigate the annotation popup.
|
pdfReader-a11yAnnotationPopupAppeared = Use Tab to navigate the annotation popup.
|
||||||
pdfReader-a11yAnnotationCreated = { $type } created.
|
pdfReader-a11yAnnotationCreated = { $type } sukurtas.
|
||||||
pdfReader-a11yAnnotationSelected = { $type } selected.
|
pdfReader-a11yAnnotationSelected = { $type } pasirinktas.
|
||||||
-pdfReader-a11yTextualAnnotationInstruction = To annotate text via the keyboard, first use “{ pdfReader-find-in-document }” to locate the phrase, and then press { general-key-control }-{ option-or-alt }-{ $number } to turn the search result into an annotation.
|
-pdfReader-a11yTextualAnnotationInstruction = Norėdami sukurti tekstinę anotaciją naudodamiesi klaviatūra, pirmiausia raskite tekstą naudodamiesi „{ pdfReader-find-in-document }“, tuomet spauskite { general-key-control }-{ option-or-alt }-{ $number } pažymėtam tekstui paversti anotacija.
|
||||||
-pdfReader-a11yAnnotationInstruction = To add this annotation into the document, focus the document and press { general-key-control }-{ option-or-alt }-{ $number }.
|
-pdfReader-a11yAnnotationInstruction = To add this annotation into the document, focus the document and press { general-key-control }-{ option-or-alt }-{ $number }.
|
||||||
pdfReader-toolbar-highlight =
|
pdfReader-toolbar-highlight =
|
||||||
.aria-description = { -pdfReader-a11yTextualAnnotationInstruction(number: 1) }
|
.aria-description = { -pdfReader-a11yTextualAnnotationInstruction(number: 1) }
|
||||||
|
|
@ -38,15 +38,15 @@ pdfReader-toolbar-area =
|
||||||
.aria-description = { -pdfReader-a11yAnnotationInstruction(number: 5) }
|
.aria-description = { -pdfReader-a11yAnnotationInstruction(number: 5) }
|
||||||
.title = { pdfReader-selectArea }
|
.title = { pdfReader-selectArea }
|
||||||
pdfReader-toolbar-draw =
|
pdfReader-toolbar-draw =
|
||||||
.aria-description = This annotation type cannot be created via the keyboard.
|
.aria-description = Šio anotacijų tipo negalite sukurti naudodamiesi klaviatūra.
|
||||||
.title = { pdfReader-draw }
|
.title = { pdfReader-draw }
|
||||||
pdfReader-findInDocumentInput =
|
pdfReader-findInDocumentInput =
|
||||||
.title = Ieškoti
|
.title = Ieškoti
|
||||||
.placeholder = { pdfReader-find-in-document }
|
.placeholder = { pdfReader-find-in-document }
|
||||||
.aria-description = To turn a search result into a highlight annotation, press { general-key-control }-{ option-or-alt }-1. To turn a search result into an underline annotation, press { general-key-control }-{ option-or-alt }-2.
|
.aria-description = To turn a search result into a highlight annotation, press { general-key-control }-{ option-or-alt }-1. To turn a search result into an underline annotation, press { general-key-control }-{ option-or-alt }-2.
|
||||||
pdfReader-import-from-epub =
|
pdfReader-import-from-epub =
|
||||||
.label = Import Ebook Annotations…
|
.label = Importuoti el. knygos anotacijas…
|
||||||
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations
|
pdfReader-import-from-epub-prompt-title = Importuoti el. knygos anotacijas
|
||||||
pdfReader-import-from-epub-prompt-text =
|
pdfReader-import-from-epub-prompt-text =
|
||||||
{ -app-name } found { $count ->
|
{ -app-name } found { $count ->
|
||||||
[1] { $count } { $tool } annotation
|
[1] { $count } { $tool } annotation
|
||||||
|
|
@ -62,4 +62,4 @@ pdfReader-import-from-epub-no-annotations-other-file =
|
||||||
“{ $filename }” does not appear to contain any Calibre or KOReader annotations.
|
“{ $filename }” does not appear to contain any Calibre or KOReader annotations.
|
||||||
|
|
||||||
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
|
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
|
||||||
pdfReader-import-from-epub-select-other = Select Other File…
|
pdfReader-import-from-epub-select-other = Pasirinkti kitą rinkmeną…
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<!ENTITY saveCmd.label "Įrašyti...">
|
<!ENTITY saveCmd.label "Įrašyti...">
|
||||||
<!ENTITY saveCmd.key "S">
|
<!ENTITY saveCmd.key "S">
|
||||||
<!ENTITY saveCmd.accesskey "r">
|
<!ENTITY saveCmd.accesskey "r">
|
||||||
<!ENTITY pageSetupCmd.label "Puslapio nuostatos...">
|
<!ENTITY pageSetupCmd.label "Puslapio nuostatos…">
|
||||||
<!ENTITY pageSetupCmd.accesskey "n">
|
<!ENTITY pageSetupCmd.accesskey "n">
|
||||||
<!ENTITY printCmd.label "Spausdinti...">
|
<!ENTITY printCmd.label "Spausdinti...">
|
||||||
<!ENTITY printCmd.key "P">
|
<!ENTITY printCmd.key "P">
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
<!ENTITY closeCmd.key "W">
|
<!ENTITY closeCmd.key "W">
|
||||||
<!ENTITY closeCmd.accesskey "U">
|
<!ENTITY closeCmd.accesskey "U">
|
||||||
<!ENTITY manageAttachments.label "Tvarkyti pridėtas rinkmenas">
|
<!ENTITY manageAttachments.label "Tvarkyti pridėtas rinkmenas">
|
||||||
<!ENTITY convertToStored.label "Nuorodas pakeisti tikromis rinkmenomis...">
|
<!ENTITY convertToStored.label "Nuorodas pakeisti tikromis rinkmenomis…">
|
||||||
<!ENTITY importCmd.label "Importuoti...">
|
<!ENTITY importCmd.label "Importuoti…">
|
||||||
<!ENTITY importCmd.key "I">
|
<!ENTITY importCmd.key "I">
|
||||||
<!ENTITY importFromClipboardCmd.label "Importuoti iškarpinės turinį">
|
<!ENTITY importFromClipboardCmd.label "Importuoti iškarpinės turinį">
|
||||||
<!ENTITY quitApplicationCmdWin.label "Išeiti">
|
<!ENTITY quitApplicationCmdWin.label "Išeiti">
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
<!ENTITY toolsMenu.label "Priemonės">
|
<!ENTITY toolsMenu.label "Priemonės">
|
||||||
<!ENTITY toolsMenu.accesskey "P">
|
<!ENTITY toolsMenu.accesskey "P">
|
||||||
<!ENTITY installConnector.label "Install Browser Connector…">
|
<!ENTITY installConnector.label "Įdiegti papildinį naršyklei…">
|
||||||
<!ENTITY installConnector.accesskey "C">
|
<!ENTITY installConnector.accesskey "C">
|
||||||
<!ENTITY addons.label "Priedai">
|
<!ENTITY addons.label "Priedai">
|
||||||
<!ENTITY developer.label "Programuotojams">
|
<!ENTITY developer.label "Programuotojams">
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
<!ENTITY helpFeedbackPage.label "Diskusijų forumai">
|
<!ENTITY helpFeedbackPage.label "Diskusijų forumai">
|
||||||
<!ENTITY helpFeedbackPage.accesskey "I">
|
<!ENTITY helpFeedbackPage.accesskey "I">
|
||||||
|
|
||||||
<!ENTITY helpReportErrors.label "Pranešti apie klaidas...">
|
<!ENTITY helpReportErrors.label "Pranešti apie klaidas…">
|
||||||
<!ENTITY helpReportErrors.accesskey "r">
|
<!ENTITY helpReportErrors.accesskey "r">
|
||||||
|
|
||||||
<!ENTITY debugOutputLogging.label "Derinimo pranešimų išvedimas">
|
<!ENTITY debugOutputLogging.label "Derinimo pranešimų išvedimas">
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
<!ENTITY debugOutputLogging.submit "Siųsti išvedimą">
|
<!ENTITY debugOutputLogging.submit "Siųsti išvedimą">
|
||||||
<!ENTITY debugOutputLogging.view "Rodyti išvedimą">
|
<!ENTITY debugOutputLogging.view "Rodyti išvedimą">
|
||||||
<!ENTITY debugOutputLogging.clear "Išvalyti išvedimą">
|
<!ENTITY debugOutputLogging.clear "Išvalyti išvedimą">
|
||||||
<!ENTITY debugOutputLogging.restartWithLoggingEnabled "Paleisti iš naujo įgalinus derinimo pranešimus...">
|
<!ENTITY debugOutputLogging.restartWithLoggingEnabled "Paleisti iš naujo įgalinus derinimo pranešimus…">
|
||||||
|
|
||||||
<!ENTITY helpCheckForUpdates.label "Tikrinti, ar yra atnaujinimų...">
|
<!ENTITY helpCheckForUpdates.label "Tikrinti, ar yra atnaujinimų…">
|
||||||
<!ENTITY helpCheckForUpdates.accesskey "T">
|
<!ENTITY helpCheckForUpdates.accesskey "T">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
general-key-control = Control
|
general-key-control = Vald
|
||||||
general-key-shift = Shift
|
general-key-shift = Lyg2
|
||||||
general-key-alt = Alt
|
general-key-alt = Alt
|
||||||
general-key-option = Option
|
general-key-option = Parinktis
|
||||||
general-key-command = Command
|
general-key-command = Command
|
||||||
option-or-alt =
|
option-or-alt =
|
||||||
{ PLATFORM() ->
|
{ PLATFORM() ->
|
||||||
|
|
@ -24,7 +24,7 @@ general-help = Žinynas
|
||||||
general-tag = Gairė
|
general-tag = Gairė
|
||||||
general-done = Atlikta
|
general-done = Atlikta
|
||||||
general-view-troubleshooting-instructions = Peržiūrėti nesklandumų sprendimo instrukcijas
|
general-view-troubleshooting-instructions = Peržiūrėti nesklandumų sprendimo instrukcijas
|
||||||
general-go-back = Go Back
|
general-go-back = Atgal
|
||||||
citation-style-label = Citavimo stilius:
|
citation-style-label = Citavimo stilius:
|
||||||
language-label = Kalba:
|
language-label = Kalba:
|
||||||
menu-file-show-in-finder =
|
menu-file-show-in-finder =
|
||||||
|
|
@ -66,9 +66,9 @@ menu-deletePermanently =
|
||||||
menu-tools-plugins =
|
menu-tools-plugins =
|
||||||
.label = Papildiniai
|
.label = Papildiniai
|
||||||
menu-view-columns-move-left =
|
menu-view-columns-move-left =
|
||||||
.label = Move Column Left
|
.label = Perkelti stulpelį kairėn
|
||||||
menu-view-columns-move-right =
|
menu-view-columns-move-right =
|
||||||
.label = Move Column Right
|
.label = Perkelti stulpelį dešinėn
|
||||||
main-window-command =
|
main-window-command =
|
||||||
.label = Library
|
.label = Library
|
||||||
main-window-key =
|
main-window-key =
|
||||||
|
|
@ -91,9 +91,9 @@ collections-menu-rename-collection =
|
||||||
collections-menu-edit-saved-search =
|
collections-menu-edit-saved-search =
|
||||||
.label = Taisyti įsimintąją paiešką
|
.label = Taisyti įsimintąją paiešką
|
||||||
collections-menu-move-collection =
|
collections-menu-move-collection =
|
||||||
.label = Move To
|
.label = Perkelti į
|
||||||
collections-menu-copy-collection =
|
collections-menu-copy-collection =
|
||||||
.label = Copy To
|
.label = Kopijuoti į
|
||||||
item-creator-moveDown =
|
item-creator-moveDown =
|
||||||
.label = Nuleisti žemyn
|
.label = Nuleisti žemyn
|
||||||
item-creator-moveToTop =
|
item-creator-moveToTop =
|
||||||
|
|
@ -129,7 +129,7 @@ item-menu-add-linked-file =
|
||||||
item-menu-add-url =
|
item-menu-add-url =
|
||||||
.label = Tinklalapio nuoroda
|
.label = Tinklalapio nuoroda
|
||||||
item-menu-change-parent-item =
|
item-menu-change-parent-item =
|
||||||
.label = Change Parent Item…
|
.label = Priskirti kitam aukštesniam įrašui…
|
||||||
view-online = Atverti tinklalapį
|
view-online = Atverti tinklalapį
|
||||||
item-menu-option-view-online =
|
item-menu-option-view-online =
|
||||||
.label = { view-online }
|
.label = { view-online }
|
||||||
|
|
@ -286,7 +286,7 @@ integration-editBibliography-add-button =
|
||||||
integration-editBibliography-remove-button =
|
integration-editBibliography-remove-button =
|
||||||
.aria-label = { general-remove }
|
.aria-label = { general-remove }
|
||||||
integration-editBibliography-editor =
|
integration-editBibliography-editor =
|
||||||
.aria-label = Edit reference
|
.aria-label = Keisti nuorodą
|
||||||
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }.
|
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }.
|
||||||
-integration-editBibliography-exclude-cited = You can also exclude a cited item by selecting it from the list of references and pressing { general-remove }.
|
-integration-editBibliography-exclude-cited = You can also exclude a cited item by selecting it from the list of references and pressing { general-remove }.
|
||||||
-integration-editBibliography-edit-reference = To change how a reference is formatted, use the text editor.
|
-integration-editBibliography-edit-reference = To change how a reference is formatted, use the text editor.
|
||||||
|
|
@ -304,7 +304,7 @@ styleEditor-locatorInput = Locator input
|
||||||
styleEditor-citationStyle = { citation-style-label }
|
styleEditor-citationStyle = { citation-style-label }
|
||||||
styleEditor-locale = { language-label }
|
styleEditor-locale = { language-label }
|
||||||
styleEditor-editor =
|
styleEditor-editor =
|
||||||
.aria-label = Style editor
|
.aria-label = Stilių rengyklė
|
||||||
styleEditor-preview =
|
styleEditor-preview =
|
||||||
.aria-label = Peržiūra
|
.aria-label = Peržiūra
|
||||||
integration-prefs-displayAs-label = Citavimo rodymo pavidalas:
|
integration-prefs-displayAs-label = Citavimo rodymo pavidalas:
|
||||||
|
|
@ -620,9 +620,11 @@ item-pane-message-unselected =
|
||||||
}
|
}
|
||||||
item-pane-message-objects-unselected =
|
item-pane-message-objects-unselected =
|
||||||
{ $count ->
|
{ $count ->
|
||||||
[0] No objects in this view
|
[0] Šiame rodinyje objektų nėra
|
||||||
[one] { $count } object in this view
|
[one] { $count } objektas šiame rodinyje
|
||||||
*[other] { $count } objects in this view
|
[few] { $count } objektai šiame rodinyje
|
||||||
|
[many] { $count } objektų šiame rodinyje
|
||||||
|
*[other] { $count } objektas šiame rodinyje
|
||||||
}
|
}
|
||||||
item-pane-duplicates-merge-items =
|
item-pane-duplicates-merge-items =
|
||||||
.label =
|
.label =
|
||||||
|
|
@ -696,9 +698,9 @@ post-upgrade-done =
|
||||||
.label = { general-done }
|
.label = { general-done }
|
||||||
text-action-paste-and-search =
|
text-action-paste-and-search =
|
||||||
.label = Įdėti ir ieškoti
|
.label = Įdėti ir ieškoti
|
||||||
mac-word-plugin-install-message = Zotero needs access to Word data to install the Word plugin.
|
mac-word-plugin-install-message = Zotero turi galėti prieiti prie „Word“ duomenų tam, kad galėtų įdiegti papildinį „Word“ programai.
|
||||||
mac-word-plugin-install-action-button =
|
mac-word-plugin-install-action-button =
|
||||||
.label = Install Word plugin
|
.label = Įdiegti papildinį „Word“ programai
|
||||||
mac-word-plugin-install-remind-later-button =
|
mac-word-plugin-install-remind-later-button =
|
||||||
.label = { general-remind-me-later }
|
.label = { general-remind-me-later }
|
||||||
mac-word-plugin-install-dont-ask-again-button =
|
mac-word-plugin-install-dont-ask-again-button =
|
||||||
|
|
|
||||||
|
|
@ -1358,7 +1358,7 @@ noteEditor.deleteTable=Ištrinti lentelę
|
||||||
pdfReader.annotations=Anotacijos
|
pdfReader.annotations=Anotacijos
|
||||||
pdfReader.showAnnotations=Rodyti anotacijas
|
pdfReader.showAnnotations=Rodyti anotacijas
|
||||||
pdfReader.searchAnnotations=Ieškoti anotacijų
|
pdfReader.searchAnnotations=Ieškoti anotacijų
|
||||||
pdfReader.searchOutline=Search Outline
|
pdfReader.searchOutline=Ieškoti žymelės
|
||||||
pdfReader.noAnnotations=Sukurkite anotaciją ir ją galėsite matyti šoniniame skydelyje.
|
pdfReader.noAnnotations=Sukurkite anotaciją ir ją galėsite matyti šoniniame skydelyje.
|
||||||
pdfReader.noExtractedText=Tekstas neišgautas
|
pdfReader.noExtractedText=Tekstas neišgautas
|
||||||
pdfReader.addComment=Pridėti komentarą
|
pdfReader.addComment=Pridėti komentarą
|
||||||
|
|
@ -1448,8 +1448,8 @@ pdfReader.epubAppearance.pageWidth.revert=Naudoti numatytąjį puslapio plotį
|
||||||
pdfReader.convertToHighlight=Paversti paryškinimu
|
pdfReader.convertToHighlight=Paversti paryškinimu
|
||||||
pdfReader.convertToUnderline=Paversti pabraukimu
|
pdfReader.convertToUnderline=Paversti pabraukimu
|
||||||
pdfReader.size=Dydis
|
pdfReader.size=Dydis
|
||||||
pdfReader.merge=Merge
|
pdfReader.merge=Apjungti
|
||||||
pdfReader.copyLink=Copy Link
|
pdfReader.copyLink=Kopijuoti nuorodą
|
||||||
|
|
||||||
spellCheck.checkSpelling=Rašybos tikrinimas
|
spellCheck.checkSpelling=Rašybos tikrinimas
|
||||||
spellCheck.addRemoveDictionaries=Pridėti / šalinti žodynus...
|
spellCheck.addRemoveDictionaries=Pridėti / šalinti žodynus...
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
<!ENTITY zotero.preferences.default "Standard:">
|
<!ENTITY zotero.preferences.default "Standard:">
|
||||||
<!ENTITY zotero.preferences.items "items">
|
<!ENTITY zotero.preferences.items "items">
|
||||||
<!ENTITY zotero.preferences.period ".">
|
<!ENTITY zotero.preferences.period ".">
|
||||||
<!ENTITY zotero.preferences.custom "Custom…">
|
<!ENTITY zotero.preferences.custom "Eigendefinert…">
|
||||||
|
|
||||||
<!ENTITY zotero.preferences.prefpane.general "Generelt">
|
<!ENTITY zotero.preferences.prefpane.general "Generelt">
|
||||||
|
|
||||||
<!ENTITY zotero.preferences.interface "Interface">
|
<!ENTITY zotero.preferences.interface "Grensesnitt">
|
||||||
<!ENTITY zotero.preferences.fileHandling "File Handling">
|
<!ENTITY zotero.preferences.fileHandling "File Handling">
|
||||||
<!ENTITY zotero.preferences.automaticSnapshots "Ta snapshot automatisk når du genererer element frå nettsider">
|
<!ENTITY zotero.preferences.automaticSnapshots "Ta snapshot automatisk når du genererer element frå nettsider">
|
||||||
<!ENTITY zotero.preferences.downloadAssociatedFiles "Lagra tilknytte PDF-filer og andre filer ved lagring av element">
|
<!ENTITY zotero.preferences.downloadAssociatedFiles "Lagra tilknytte PDF-filer og andre filer ved lagring av element">
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
<!ENTITY zotero.preferences.reader.tabsTitle.label "Show tabs as">
|
<!ENTITY zotero.preferences.reader.tabsTitle.label "Show tabs as">
|
||||||
<!ENTITY zotero.preferences.reader.tabsTitle.creatorYearTitle "Creator - Year - Title">
|
<!ENTITY zotero.preferences.reader.tabsTitle.creatorYearTitle "Creator - Year - Title">
|
||||||
<!ENTITY zotero.preferences.reader.tabsTitle.titleCreatorYear "Title - Creator - Year">
|
<!ENTITY zotero.preferences.reader.tabsTitle.titleCreatorYear "Title - Creator - Year">
|
||||||
<!ENTITY zotero.preferences.reader.tabsTitle.filename "Filename">
|
<!ENTITY zotero.preferences.reader.tabsTitle.filename "Filnamn">
|
||||||
|
|
||||||
<!ENTITY zotero.preferences.groups "Grupper">
|
<!ENTITY zotero.preferences.groups "Grupper">
|
||||||
<!ENTITY zotero.preferences.groups.whenCopyingInclude "Ta med når element vert kopierte mellom bibliotek:">
|
<!ENTITY zotero.preferences.groups.whenCopyingInclude "Ta med når element vert kopierte mellom bibliotek:">
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
<!ENTITY zotero.preferences.sync.reset.resetFileSyncHistory.desc "Compare all attachment files with the storage service">
|
<!ENTITY zotero.preferences.sync.reset.resetFileSyncHistory.desc "Compare all attachment files with the storage service">
|
||||||
<!ENTITY zotero.preferences.sync.reset "Nullstill">
|
<!ENTITY zotero.preferences.sync.reset "Nullstill">
|
||||||
<!ENTITY zotero.preferences.sync.reset.showResetOptions "Show Reset Options…">
|
<!ENTITY zotero.preferences.sync.reset.showResetOptions "Show Reset Options…">
|
||||||
<!ENTITY zotero.preferences.sync.reset.button "Reset…">
|
<!ENTITY zotero.preferences.sync.reset.button "Nullstill…">
|
||||||
|
|
||||||
|
|
||||||
<!ENTITY zotero.preferences.prefpane.search "Søk">
|
<!ENTITY zotero.preferences.prefpane.search "Søk">
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
<!ENTITY zotero.preferences.quickCopy.copyAsHTML "Kopier som HTML">
|
<!ENTITY zotero.preferences.quickCopy.copyAsHTML "Kopier som HTML">
|
||||||
<!ENTITY zotero.preferences.quickCopy.siteEditor.setings "Innstillingar">
|
<!ENTITY zotero.preferences.quickCopy.siteEditor.setings "Innstillingar">
|
||||||
<!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath "Domene/stig">
|
<!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath "Domene/stig">
|
||||||
<!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath.example "(e.g., wikipedia.org)">
|
<!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath.example "(t.d. wikipedia.org)">
|
||||||
<!ENTITY zotero.preferences.quickCopy.siteEditor.format "Format">
|
<!ENTITY zotero.preferences.quickCopy.siteEditor.format "Format">
|
||||||
<!ENTITY zotero.preferences.quickCopy.siteEditor.locale "Språk">
|
<!ENTITY zotero.preferences.quickCopy.siteEditor.locale "Språk">
|
||||||
<!ENTITY zotero.preferences.quickCopy.dragLimit "Slå av rask kopiering når du drar fleire enn">
|
<!ENTITY zotero.preferences.quickCopy.dragLimit "Slå av rask kopiering når du drar fleire enn">
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
<!ENTITY zotero.preferences.cite.styles.styleManager.title "Tittel">
|
<!ENTITY zotero.preferences.cite.styles.styleManager.title "Tittel">
|
||||||
<!ENTITY zotero.preferences.cite.styles.styleManager.updated "Oppdatert">
|
<!ENTITY zotero.preferences.cite.styles.styleManager.updated "Oppdatert">
|
||||||
<!ENTITY zotero.preferences.cite.styles.styleManager.csl "CSL">
|
<!ENTITY zotero.preferences.cite.styles.styleManager.csl "CSL">
|
||||||
<!ENTITY zotero.preferences.export.getAdditionalStyles "Get additional styles…">
|
<!ENTITY zotero.preferences.export.getAdditionalStyles "Hent fleire stilar…">
|
||||||
|
|
||||||
<!ENTITY zotero.preferences.keys.saveToZotero "Save to Zotero (address bar icon)">
|
<!ENTITY zotero.preferences.keys.saveToZotero "Save to Zotero (address bar icon)">
|
||||||
<!ENTITY zotero.preferences.keys.focusLibrariesPane "Focus Libraries Pane">
|
<!ENTITY zotero.preferences.keys.focusLibrariesPane "Focus Libraries Pane">
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,7 @@ new-collection-dialog =
|
||||||
new-collection-name = Namn:
|
new-collection-name = Namn:
|
||||||
new-collection-create-in = Create in:
|
new-collection-create-in = Create in:
|
||||||
attachment-info-title = Tittel
|
attachment-info-title = Tittel
|
||||||
attachment-info-filename = Filename
|
attachment-info-filename = Filnamn
|
||||||
attachment-info-accessed = Lest
|
attachment-info-accessed = Lest
|
||||||
attachment-info-pages = Sidetal
|
attachment-info-pages = Sidetal
|
||||||
attachment-info-modified = Sist endra
|
attachment-info-modified = Sist endra
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,7 @@ pane.item.attachments.hasLink=Has link
|
||||||
pane.item.attachments.select=Vel ei fil
|
pane.item.attachments.select=Vel ei fil
|
||||||
pane.item.attachments.PDF.installTools.title=PDF Tools Not Installed
|
pane.item.attachments.PDF.installTools.title=PDF Tools Not Installed
|
||||||
pane.item.attachments.PDF.installTools.text=To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences.
|
pane.item.attachments.PDF.installTools.text=To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences.
|
||||||
pane.item.attachments.filename=Filename
|
pane.item.attachments.filename=Filnamn
|
||||||
pane.item.noteEditor.clickHere=Klikk her
|
pane.item.noteEditor.clickHere=Klikk her
|
||||||
pane.item.tags.icon.user=Brukerdefinert tagg:
|
pane.item.tags.icon.user=Brukerdefinert tagg:
|
||||||
pane.item.tags.icon.automatic=Automatisk tagg:
|
pane.item.tags.icon.automatic=Automatisk tagg:
|
||||||
|
|
@ -1344,7 +1344,7 @@ noteEditor.removeCitations=Hide Annotation Citations
|
||||||
noteEditor.image=Bilete
|
noteEditor.image=Bilete
|
||||||
noteEditor.math=Math
|
noteEditor.math=Math
|
||||||
noteEditor.table=Table
|
noteEditor.table=Table
|
||||||
noteEditor.citation=Citation
|
noteEditor.citation=Sitering
|
||||||
noteEditor.copyImage=Copy Image
|
noteEditor.copyImage=Copy Image
|
||||||
noteEditor.saveImageAs=Save Image As…
|
noteEditor.saveImageAs=Save Image As…
|
||||||
noteEditor.insertRowBefore=Insert Row Above
|
noteEditor.insertRowBefore=Insert Row Above
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ preferences-advanced-enable-bidi-ui =
|
||||||
preferences-advanced-reset-data-dir =
|
preferences-advanced-reset-data-dir =
|
||||||
.label = Revert to Default Location…
|
.label = Revert to Default Location…
|
||||||
preferences-advanced-custom-data-dir =
|
preferences-advanced-custom-data-dir =
|
||||||
.label = Use Custom Location…
|
.label = 使用自定义位置
|
||||||
preferences-advanced-default-data-dir =
|
preferences-advanced-default-data-dir =
|
||||||
.value = (Default: { $directory })
|
.value = (Default: { $directory })
|
||||||
.aria-label = Default location
|
.aria-label = 默认位置
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ pdfReader-findInDocumentInput =
|
||||||
.placeholder = { pdfReader-find-in-document }
|
.placeholder = { pdfReader-find-in-document }
|
||||||
.aria-description = 要将搜索结果转换为高亮注释,请按 { general-key-control }-{ option-or-alt }-1。要将搜索结果转换为下划线注释,请按 { general-key-control }-{ option-or-alt }-2。
|
.aria-description = 要将搜索结果转换为高亮注释,请按 { general-key-control }-{ option-or-alt }-1。要将搜索结果转换为下划线注释,请按 { general-key-control }-{ option-or-alt }-2。
|
||||||
pdfReader-import-from-epub =
|
pdfReader-import-from-epub =
|
||||||
.label = Import Ebook Annotations…
|
.label = 导入电子书注释…
|
||||||
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations
|
pdfReader-import-from-epub-prompt-title = 导入电子书注释
|
||||||
pdfReader-import-from-epub-prompt-text =
|
pdfReader-import-from-epub-prompt-text =
|
||||||
{ -app-name } found { $count ->
|
{ -app-name } found { $count ->
|
||||||
[1] { $count } { $tool } annotation
|
[1] { $count } { $tool } annotation
|
||||||
|
|
@ -62,4 +62,4 @@ pdfReader-import-from-epub-no-annotations-other-file =
|
||||||
“{ $filename }” does not appear to contain any Calibre or KOReader annotations.
|
“{ $filename }” does not appear to contain any Calibre or KOReader annotations.
|
||||||
|
|
||||||
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
|
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
|
||||||
pdfReader-import-from-epub-select-other = Select Other File…
|
pdfReader-import-from-epub-select-other = 选择其他文件…
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
<!ENTITY toolsMenu.label "工具">
|
<!ENTITY toolsMenu.label "工具">
|
||||||
<!ENTITY toolsMenu.accesskey "T">
|
<!ENTITY toolsMenu.accesskey "T">
|
||||||
<!ENTITY installConnector.label "Install Browser Connector…">
|
<!ENTITY installConnector.label "安装浏览器扩展…">
|
||||||
<!ENTITY installConnector.accesskey "C">
|
<!ENTITY installConnector.accesskey "C">
|
||||||
<!ENTITY addons.label "插件">
|
<!ENTITY addons.label "插件">
|
||||||
<!ENTITY developer.label "开发者">
|
<!ENTITY developer.label "开发者">
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ general-help = 帮助
|
||||||
general-tag = 标签
|
general-tag = 标签
|
||||||
general-done = 完成
|
general-done = 完成
|
||||||
general-view-troubleshooting-instructions = 查看故障排除说明
|
general-view-troubleshooting-instructions = 查看故障排除说明
|
||||||
general-go-back = Go Back
|
general-go-back = 返回
|
||||||
citation-style-label = 参考文献样式:
|
citation-style-label = 参考文献样式:
|
||||||
language-label = 语言:
|
language-label = 语言:
|
||||||
menu-file-show-in-finder =
|
menu-file-show-in-finder =
|
||||||
|
|
@ -66,9 +66,9 @@ menu-deletePermanently =
|
||||||
menu-tools-plugins =
|
menu-tools-plugins =
|
||||||
.label = 插件
|
.label = 插件
|
||||||
menu-view-columns-move-left =
|
menu-view-columns-move-left =
|
||||||
.label = Move Column Left
|
.label = 将列移至左侧
|
||||||
menu-view-columns-move-right =
|
menu-view-columns-move-right =
|
||||||
.label = Move Column Right
|
.label = 将列移至右侧
|
||||||
main-window-command =
|
main-window-command =
|
||||||
.label = Library
|
.label = Library
|
||||||
main-window-key =
|
main-window-key =
|
||||||
|
|
@ -283,7 +283,7 @@ integration-editBibliography-add-button =
|
||||||
integration-editBibliography-remove-button =
|
integration-editBibliography-remove-button =
|
||||||
.aria-label = { general-remove }
|
.aria-label = { general-remove }
|
||||||
integration-editBibliography-editor =
|
integration-editBibliography-editor =
|
||||||
.aria-label = Edit reference
|
.aria-label = 编辑参考文献表
|
||||||
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }.
|
-integration-editBibliography-include-uncited = To include an uncited item in your bibliography, select it from the items list and press { general-add }.
|
||||||
-integration-editBibliography-exclude-cited = You can also exclude a cited item by selecting it from the list of references and pressing { general-remove }.
|
-integration-editBibliography-exclude-cited = You can also exclude a cited item by selecting it from the list of references and pressing { general-remove }.
|
||||||
-integration-editBibliography-edit-reference = To change how a reference is formatted, use the text editor.
|
-integration-editBibliography-edit-reference = To change how a reference is formatted, use the text editor.
|
||||||
|
|
|
||||||
|
|
@ -1358,7 +1358,7 @@ noteEditor.deleteTable=删除表格
|
||||||
pdfReader.annotations=注释
|
pdfReader.annotations=注释
|
||||||
pdfReader.showAnnotations=显示注释
|
pdfReader.showAnnotations=显示注释
|
||||||
pdfReader.searchAnnotations=搜索注释
|
pdfReader.searchAnnotations=搜索注释
|
||||||
pdfReader.searchOutline=Search Outline
|
pdfReader.searchOutline=搜索大纲
|
||||||
pdfReader.noAnnotations=创建注释使其在侧栏可见
|
pdfReader.noAnnotations=创建注释使其在侧栏可见
|
||||||
pdfReader.noExtractedText=无提取的文本
|
pdfReader.noExtractedText=无提取的文本
|
||||||
pdfReader.addComment=添加评论
|
pdfReader.addComment=添加评论
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ pref("extensions.zotero.autoRenameFiles.fileTypes", "application/pdf,application
|
||||||
pref("extensions.zotero.attachmentRenameTemplate", "{{ firstCreator suffix=\" - \" }}{{ year suffix=\" - \" }}{{ title truncate=\"100\" }}");
|
pref("extensions.zotero.attachmentRenameTemplate", "{{ firstCreator suffix=\" - \" }}{{ year suffix=\" - \" }}{{ title truncate=\"100\" }}");
|
||||||
pref("extensions.zotero.capitalizeTitles", false);
|
pref("extensions.zotero.capitalizeTitles", false);
|
||||||
pref("extensions.zotero.launchNonNativeFiles", false);
|
pref("extensions.zotero.launchNonNativeFiles", false);
|
||||||
|
pref("extensions.zotero.naturalSorting", true);
|
||||||
pref("extensions.zotero.sortNotesChronologically", false);
|
pref("extensions.zotero.sortNotesChronologically", false);
|
||||||
pref("extensions.zotero.sortNotesChronologically.reader", true);
|
pref("extensions.zotero.sortNotesChronologically.reader", true);
|
||||||
pref("extensions.zotero.sortAttachmentsChronologically", false);
|
pref("extensions.zotero.sortAttachmentsChronologically", false);
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0f4f218980ceed99639ae887b084c9b059ebd4d0
|
Subproject commit cd609cafd3049f4d922c891641ffc7a76eb8ccdf
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 64c6acb0c06ed9f1d7f090052132292378c5cfe5
|
Subproject commit f9857be7000bdd90997e7a28abad2383f8d2f88b
|
||||||
2
reader
2
reader
|
|
@ -1 +1 @@
|
||||||
Subproject commit 533ccd38ca0f2f5ab3c5be277516a4dba581629c
|
Subproject commit 51fb33cb8046238d4baee153fd40b2cd035fd7fb
|
||||||
|
|
@ -1 +1 @@
|
||||||
2024-11-26 07:33:38
|
2025-03-13 03:14:07
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,6 @@ zoterosearch {
|
||||||
width: 12em;
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#condition-tooltips tooltip
|
|
||||||
{
|
|
||||||
background: red !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#condition-tooltips hbox > label
|
#condition-tooltips hbox > label
|
||||||
{
|
{
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
||||||
2
styles
2
styles
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1554b78c316ad2634885a014b0b0b2afa7a476a4
|
Subproject commit 40b18d8266fba6c740d48218d31ecead786e5e6b
|
||||||
|
|
@ -446,7 +446,7 @@ async function waitNoLongerThan(promise, ms = 1000) {
|
||||||
|
|
||||||
async function waitForScrollToPane(itemDetails, paneID) {
|
async function waitForScrollToPane(itemDetails, paneID) {
|
||||||
await itemDetails._renderPromise;
|
await itemDetails._renderPromise;
|
||||||
itemDetails.scrollToPane(paneID, "instant");
|
await itemDetails.scrollToPane(paneID, "instant");
|
||||||
// Wait for some frames or up to 150ms to ensure the pane is visible
|
// Wait for some frames or up to 150ms to ensure the pane is visible
|
||||||
await waitForFrames(5);
|
await waitForFrames(5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -925,7 +925,8 @@ describe("Zotero.Attachments", function() {
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 2);
|
// doi.org, publisher, download
|
||||||
|
assert.equal(requestStub.callCount, 3);
|
||||||
assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi));
|
||||||
assert.ok(attachment);
|
assert.ok(attachment);
|
||||||
var json = attachment.toJSON();
|
var json = attachment.toJSON();
|
||||||
|
|
@ -961,7 +962,8 @@ describe("Zotero.Attachments", function() {
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 2);
|
// doi.org, publisher, download
|
||||||
|
assert.equal(requestStub.callCount, 3);
|
||||||
assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(requestStub.getCall(0).calledWith('GET', 'https://doi.org/' + doi));
|
||||||
assert.ok(attachment);
|
assert.ok(attachment);
|
||||||
var json = attachment.toJSON();
|
var json = attachment.toJSON();
|
||||||
|
|
@ -979,7 +981,8 @@ describe("Zotero.Attachments", function() {
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 1);
|
// URL, download
|
||||||
|
assert.equal(requestStub.callCount, 2);
|
||||||
assert.isTrue(requestStub.calledWith('GET', url));
|
assert.isTrue(requestStub.calledWith('GET', url));
|
||||||
assert.ok(attachment);
|
assert.ok(attachment);
|
||||||
var json = attachment.toJSON();
|
var json = attachment.toJSON();
|
||||||
|
|
@ -996,6 +999,7 @@ describe("Zotero.Attachments", function() {
|
||||||
item.setField('url', url);
|
item.setField('url', url);
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
// URL, redirect target URL
|
||||||
assert.equal(requestStub.callCount, 2);
|
assert.equal(requestStub.callCount, 2);
|
||||||
var call = requestStub.getCall(0);
|
var call = requestStub.getCall(0);
|
||||||
assert.isTrue(call.calledWith('GET', url));
|
assert.isTrue(call.calledWith('GET', url));
|
||||||
|
|
@ -1017,7 +1021,7 @@ describe("Zotero.Attachments", function() {
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 3);
|
assert.equal(requestStub.callCount, 4);
|
||||||
var call1 = requestStub.getCall(0);
|
var call1 = requestStub.getCall(0);
|
||||||
assert.isTrue(call1.calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(call1.calledWith('GET', 'https://doi.org/' + doi));
|
||||||
var call2 = requestStub.getCall(1);
|
var call2 = requestStub.getCall(1);
|
||||||
|
|
@ -1041,7 +1045,7 @@ describe("Zotero.Attachments", function() {
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 4);
|
assert.equal(requestStub.callCount, 5);
|
||||||
// Check the DOI (and get nothing)
|
// Check the DOI (and get nothing)
|
||||||
var call = requestStub.getCall(0);
|
var call = requestStub.getCall(0);
|
||||||
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
||||||
|
|
@ -1097,8 +1101,9 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]);
|
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 2);
|
// 2 URLs and 2 downloads
|
||||||
assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 998);
|
assert.equal(requestStub.callCount, 4);
|
||||||
|
assert.isAbove(requestStubCallTimes[2] - requestStubCallTimes[0], 998);
|
||||||
// Make sure both items have attachments
|
// Make sure both items have attachments
|
||||||
assert.equal(item1.numAttachments(), 1);
|
assert.equal(item1.numAttachments(), 1);
|
||||||
assert.equal(item2.numAttachments(), 1);
|
assert.equal(item2.numAttachments(), 1);
|
||||||
|
|
@ -1119,7 +1124,7 @@ describe("Zotero.Attachments", function() {
|
||||||
item2.setField('url', url2);
|
item2.setField('url', url2);
|
||||||
await item2.saveTx();
|
await item2.saveTx();
|
||||||
|
|
||||||
// DOI URL resolves to 'website2' domain without PDF
|
// DOI URL resolves to 'website2' domain with PDF
|
||||||
var url3 = doiPrefix + doi6;
|
var url3 = doiPrefix + doi6;
|
||||||
var item3 = createUnsavedDataObject('item', { itemType: 'journalArticle' });
|
var item3 = createUnsavedDataObject('item', { itemType: 'journalArticle' });
|
||||||
item3.setField('title', 'Test');
|
item3.setField('title', 'Test');
|
||||||
|
|
@ -1128,19 +1133,22 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2, item3]);
|
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2, item3]);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 6);
|
assert.equal(requestStub.callCount, 8);
|
||||||
assert.equal(requestStub.getCall(0).args[1], doiPrefix + doi1);
|
assert.equal(requestStub.getCall(0).args[1], doiPrefix + doi1);
|
||||||
assert.equal(requestStub.getCall(1).args[1], pageURL1);
|
assert.equal(requestStub.getCall(1).args[1], pageURL1);
|
||||||
assert.equal(requestStub.getCall(2).args[1], doiPrefix + doi4);
|
assert.equal(requestStub.getCall(2).args[1], pdfURL);
|
||||||
|
|
||||||
|
assert.equal(requestStub.getCall(3).args[1], doiPrefix + doi4);
|
||||||
// Should skip ahead to the next DOI
|
// Should skip ahead to the next DOI
|
||||||
assert.equal(requestStub.getCall(3).args[1], doiPrefix + doi6);
|
assert.equal(requestStub.getCall(4).args[1], doiPrefix + doi6);
|
||||||
// which is on a new domain
|
// which is on a new domain
|
||||||
assert.equal(requestStub.getCall(4).args[1], pageURL8);
|
assert.equal(requestStub.getCall(5).args[1], pageURL8);
|
||||||
|
assert.equal(requestStub.getCall(6).args[1], pdfURL);
|
||||||
// and then return to make 'website' request for DOI 4
|
// and then return to make 'website' request for DOI 4
|
||||||
assert.equal(requestStub.getCall(5).args[1], pageURL4);
|
assert.equal(requestStub.getCall(7).args[1], pageURL4);
|
||||||
|
|
||||||
// 'website' requests should be a second apart
|
// 'website' requests should be a second apart
|
||||||
assert.isAbove(requestStubCallTimes[5] - requestStubCallTimes[1], 995);
|
assert.isAbove(requestStubCallTimes[7] - requestStubCallTimes[1], 995);
|
||||||
|
|
||||||
assert.equal(item1.numAttachments(), 1);
|
assert.equal(item1.numAttachments(), 1);
|
||||||
assert.equal(item2.numAttachments(), 0);
|
assert.equal(item2.numAttachments(), 0);
|
||||||
|
|
@ -1162,10 +1170,13 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]);
|
var attachments = await Zotero.Attachments.addAvailableFiles([item1, item2]);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 3);
|
// 429, URL9, download, URL3, download
|
||||||
|
assert.equal(requestStub.callCount, 5);
|
||||||
assert.equal(requestStub.getCall(0).args[1], pageURL9);
|
assert.equal(requestStub.getCall(0).args[1], pageURL9);
|
||||||
assert.equal(requestStub.getCall(1).args[1], pageURL9);
|
assert.equal(requestStub.getCall(1).args[1], pageURL9);
|
||||||
assert.equal(requestStub.getCall(2).args[1], pageURL3);
|
assert.equal(requestStub.getCall(2).args[1], pdfURL);
|
||||||
|
assert.equal(requestStub.getCall(3).args[1], pageURL3);
|
||||||
|
assert.equal(requestStub.getCall(4).args[1], pdfURL);
|
||||||
assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 1999);
|
assert.isAbove(requestStubCallTimes[1] - requestStubCallTimes[0], 1999);
|
||||||
// Make sure both items have attachments
|
// Make sure both items have attachments
|
||||||
assert.equal(item1.numAttachments(), 1);
|
assert.equal(item1.numAttachments(), 1);
|
||||||
|
|
@ -1180,9 +1191,10 @@ describe("Zotero.Attachments", function() {
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 2);
|
assert.equal(requestStub.callCount, 3);
|
||||||
assert.equal(requestStub.getCall(0).args[1], pageURL10)
|
assert.equal(requestStub.getCall(0).args[1], pageURL10)
|
||||||
assert.equal(requestStub.getCall(1).args[1], pageURL1)
|
assert.equal(requestStub.getCall(1).args[1], pageURL1)
|
||||||
|
assert.equal(requestStub.getCall(2).args[1], pdfURL)
|
||||||
assert.ok(attachment);
|
assert.ok(attachment);
|
||||||
var json = attachment.toJSON();
|
var json = attachment.toJSON();
|
||||||
assert.equal(json.url, pdfURL);
|
assert.equal(json.url, pdfURL);
|
||||||
|
|
@ -1228,7 +1240,7 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 4);
|
assert.equal(requestStub.callCount, 5);
|
||||||
var call = requestStub.getCall(0);
|
var call = requestStub.getCall(0);
|
||||||
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
||||||
var call = requestStub.getCall(1);
|
var call = requestStub.getCall(1);
|
||||||
|
|
@ -1266,7 +1278,7 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 4);
|
assert.equal(requestStub.callCount, 5);
|
||||||
var call = requestStub.getCall(0);
|
var call = requestStub.getCall(0);
|
||||||
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
||||||
var call = requestStub.getCall(1);
|
var call = requestStub.getCall(1);
|
||||||
|
|
@ -1302,7 +1314,7 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 4);
|
assert.equal(requestStub.callCount, 5);
|
||||||
var call = requestStub.getCall(0);
|
var call = requestStub.getCall(0);
|
||||||
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
||||||
call = requestStub.getCall(1);
|
call = requestStub.getCall(1);
|
||||||
|
|
@ -1342,7 +1354,7 @@ describe("Zotero.Attachments", function() {
|
||||||
|
|
||||||
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
var attachment = await Zotero.Attachments.addAvailableFile(item);
|
||||||
|
|
||||||
assert.equal(requestStub.callCount, 5);
|
assert.equal(requestStub.callCount, 6);
|
||||||
var call = requestStub.getCall(0);
|
var call = requestStub.getCall(0);
|
||||||
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
assert.isTrue(call.calledWith('GET', 'https://doi.org/' + doi));
|
||||||
call = requestStub.getCall(1);
|
call = requestStub.getCall(1);
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,23 @@ describe("Zotero.CollectionTree", function() {
|
||||||
assert.equal(cv.selection.focused, 0);
|
assert.equal(cv.selection.focused, 0);
|
||||||
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item1.id, item2.id]);
|
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item1.id, item2.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should switch to trash if item is in trash", async function () {
|
||||||
|
var item = await createDataObject('item', { deleted: true });
|
||||||
|
await cv.selectItems([item.id]);
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
assert.isTrue(zp.getCollectionTreeRow().isTrash());
|
||||||
|
assert.sameMembers(zp.itemsView.getSelectedItems(true), [item.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should switch to trash if parent of child note is in trash", async function () {
|
||||||
|
var item = await createDataObject('item', { deleted: true });
|
||||||
|
var note = await createDataObject('item', { itemType: 'note', parentItemID: item.id });
|
||||||
|
await cv.selectItems([note.id]);
|
||||||
|
await waitForItemsLoad(win);
|
||||||
|
assert.isTrue(zp.getCollectionTreeRow().isTrash());
|
||||||
|
assert.sameMembers(zp.itemsView.getSelectedItems(true), [note.id]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#onDrop()", function () {
|
describe("#onDrop()", function () {
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,43 @@
|
||||||
{
|
{
|
||||||
"id": "339d0202-d99f-48a2-aa0d-9b0c5631af26",
|
"id": "339d0202-d99f-48a2-aa0d-9b0c5631af26",
|
||||||
"type": "highlight",
|
"type": "highlight",
|
||||||
"color":
|
"color": {
|
||||||
{
|
|
||||||
"r": 255,
|
"r": 255,
|
||||||
"g": 245,
|
"g": 245,
|
||||||
"b": 173
|
"b": 173
|
||||||
},
|
},
|
||||||
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
|
"profile_id": "8dbf0832-8723-4c48-b532-20c0b7f6e01a",
|
||||||
"positions":
|
"positions": [
|
||||||
[
|
|
||||||
{
|
{
|
||||||
"top_left":
|
"top_left": {
|
||||||
{
|
"x": 191.5049,
|
||||||
"x": 429.74257247464135,
|
"y": 588.5418
|
||||||
"y": 770.1238821199208
|
|
||||||
},
|
},
|
||||||
"bottom_right":
|
"bottom_right": {
|
||||||
{
|
"x": 461.6047999999994,
|
||||||
"x": 572.5401236420382,
|
"y": 576.1542
|
||||||
"y": 680.1292839565452
|
},
|
||||||
|
"page": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"top_left": {
|
||||||
|
"x": 146.3,
|
||||||
|
"y": 576.8651
|
||||||
|
},
|
||||||
|
"bottom_right": {
|
||||||
|
"x": 461.3467999999992,
|
||||||
|
"y": 564.6542
|
||||||
|
},
|
||||||
|
"page": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"top_left": {
|
||||||
|
"x": 146.3,
|
||||||
|
"y": 565.3651
|
||||||
|
},
|
||||||
|
"bottom_right": {
|
||||||
|
"x": 220.06760000000003,
|
||||||
|
"y": 553.1542
|
||||||
},
|
},
|
||||||
"page": 1
|
"page": 1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
],
|
],
|
||||||
"created": "2024-11-04T13:49:03.274Z",
|
"created": "2024-11-04T13:49:03.274Z",
|
||||||
"modified": "2024-11-04T13:49:03.274Z",
|
"modified": "2024-11-04T13:49:03.274Z",
|
||||||
"id": "https://api.mendeley.com/annotations/v2/84f12446-3b49-4052-bbdc-832d28e1e072",
|
"id": "https://api.mendeley.com/annotations/v2/339d0202-d99f-48a2-aa0d-9b0c5631af26",
|
||||||
"body": [
|
"body": [
|
||||||
{
|
{
|
||||||
"purpose": "highlighting",
|
"purpose": "highlighting",
|
||||||
|
|
|
||||||
8
test/tests/data/mendeleyMock/notebooks.json
Normal file
8
test/tests/data/mendeleyMock/notebooks.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "8041a43b-f740-41ec-be42-a24cc5106d68",
|
||||||
|
"created": "2024-07-09T09:56:04.349Z",
|
||||||
|
"modified": "2024-12-06T17:41:37.200Z",
|
||||||
|
"title": "TEST"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -269,7 +269,7 @@ describe("Item pane", function () {
|
||||||
|
|
||||||
var menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
|
var menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
|
||||||
// Fake a right-click
|
// Fake a right-click
|
||||||
doc.popupNode = parent;
|
itemBox._popupNode = parent;
|
||||||
menupopup.openPopup(
|
menupopup.openPopup(
|
||||||
parent, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 })
|
parent, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 })
|
||||||
);
|
);
|
||||||
|
|
@ -298,7 +298,7 @@ describe("Item pane", function () {
|
||||||
var firstlast = label.closest('.creator-type-value');
|
var firstlast = label.closest('.creator-type-value');
|
||||||
var menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
|
var menupopup = itemBox.querySelector('#zotero-creator-transform-menu');
|
||||||
// Fake a right-click
|
// Fake a right-click
|
||||||
doc.popupNode = firstlast;
|
itemBox._popupNode = firstlast;
|
||||||
menupopup.openPopup(
|
menupopup.openPopup(
|
||||||
firstlast, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 })
|
firstlast, "after_start", 0, 0, true, false, new MouseEvent('click', { button: 2 })
|
||||||
);
|
);
|
||||||
|
|
@ -1795,4 +1795,47 @@ describe("Item pane", function () {
|
||||||
assert.isTrue(ZoteroContextPane.splitter.hidden);
|
assert.isTrue(ZoteroContextPane.splitter.hidden);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Libraries pane", function () {
|
||||||
|
let paneID = "libraries-collections";
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Zotero.Prefs.set("panes.libraries-collections.open", true);
|
||||||
|
Zotero_Tabs.select("zotero-pane");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Zotero_Tabs.select("zotero-pane");
|
||||||
|
Zotero_Tabs.closeAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should scroll to pinned pane after selection moves from note to item", async function () {
|
||||||
|
let itemDetails = ZoteroPane.itemPane._itemDetails;
|
||||||
|
let pane = itemDetails.getPane(paneID);
|
||||||
|
|
||||||
|
let item = await createDataObject('item');
|
||||||
|
await importPDFAttachment(item);
|
||||||
|
|
||||||
|
let note = await createDataObject('item', { itemType: 'note' });
|
||||||
|
|
||||||
|
await select(win, item);
|
||||||
|
await waitForScrollToPane(itemDetails, paneID);
|
||||||
|
|
||||||
|
itemDetails.pinnedPane = paneID;
|
||||||
|
|
||||||
|
await select(win, note);
|
||||||
|
|
||||||
|
await Zotero.Promise.delay(10);
|
||||||
|
|
||||||
|
await select(win, item);
|
||||||
|
await waitForScrollToPane(itemDetails, paneID);
|
||||||
|
|
||||||
|
// Should scroll to pinned pane
|
||||||
|
assert.isTrue(
|
||||||
|
Math.abs(pane.getBoundingClientRect().top - pane.parentElement.getBoundingClientRect().top) < 3
|
||||||
|
);
|
||||||
|
|
||||||
|
itemDetails.pinnedPane = "";
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2731,7 +2731,7 @@ describe("Zotero.Item", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("not-strict mode", function () {
|
describe("not-strict mode", function () {
|
||||||
it("should handle Extra in non-strict mode", function () {
|
it("should preserve Extra", function () {
|
||||||
var json = {
|
var json = {
|
||||||
itemType: "journalArticle",
|
itemType: "journalArticle",
|
||||||
title: "Test",
|
title: "Test",
|
||||||
|
|
@ -2834,6 +2834,45 @@ describe("Zotero.Item", function () {
|
||||||
assert.equal(item.getField('extra'), '');
|
assert.equal(item.getField('extra'), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use a Zotero item type stored in Extra", async function () {
|
||||||
|
var json = {
|
||||||
|
itemType: "document",
|
||||||
|
title: "Test",
|
||||||
|
extra: "Type: dataset\nCitation Key: abc123\nFoo: Bar"
|
||||||
|
};
|
||||||
|
var item = new Zotero.Item;
|
||||||
|
item.fromJSON(json);
|
||||||
|
assert.equal(Zotero.ItemTypes.getName(item.itemTypeID), 'dataset');
|
||||||
|
// Move a valid field out of Extra
|
||||||
|
assert.equal(item.getField('citationKey'), 'abc123');
|
||||||
|
assert.equal(item.getField('extra'), 'Foo: Bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use a CSL item type stored in Extra", async function () {
|
||||||
|
var json = {
|
||||||
|
itemType: "journalArticle",
|
||||||
|
title: "Test",
|
||||||
|
extra: "Type: song"
|
||||||
|
};
|
||||||
|
var item = new Zotero.Item;
|
||||||
|
item.fromJSON(json);
|
||||||
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('audioRecording'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should move a now-invalid existing field to Extra when using Type from Extra", async function () {
|
||||||
|
var json = {
|
||||||
|
itemType: "journalArticle",
|
||||||
|
title: "Test",
|
||||||
|
pages: "123",
|
||||||
|
extra: "Type: audioRecording"
|
||||||
|
};
|
||||||
|
var item = new Zotero.Item;
|
||||||
|
item.fromJSON(json);
|
||||||
|
assert.equal(item.itemTypeID, Zotero.ItemTypes.getID('audioRecording'));
|
||||||
|
// 'pages' should've been moved to Extra, since it's not valid for the new type
|
||||||
|
assert.equal(item.getField('extra'), 'Pages: 123');
|
||||||
|
});
|
||||||
|
|
||||||
it("should ignore some redundant fields from RDF translator (temporary)", function () {
|
it("should ignore some redundant fields from RDF translator (temporary)", function () {
|
||||||
var json = {
|
var json = {
|
||||||
itemType: "book",
|
itemType: "book",
|
||||||
|
|
@ -2941,6 +2980,28 @@ describe("Zotero.Item", function () {
|
||||||
};
|
};
|
||||||
assert.throws(f, /^Invalid creator type/);
|
assert.throws(f, /^Invalid creator type/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should ignore item type in Extra", function () {
|
||||||
|
var json = {
|
||||||
|
itemType: "document",
|
||||||
|
title: "",
|
||||||
|
extra: "Type: preprint"
|
||||||
|
};
|
||||||
|
var item = new Zotero.Item;
|
||||||
|
item.fromJSON(json, { strict: true });
|
||||||
|
assert.equal(item.getField('extra'), "Type: preprint");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore valid field in Extra", function () {
|
||||||
|
var json = {
|
||||||
|
itemType: "journalArticle",
|
||||||
|
title: "",
|
||||||
|
extra: "DOI: 10.1234/abcd"
|
||||||
|
};
|
||||||
|
var item = new Zotero.Item;
|
||||||
|
item.fromJSON(json, { strict: true });
|
||||||
|
assert.equal(item.getField('extra'), "DOI: 10.1234/abcd");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should accept ISO 8601 dates", function* () {
|
it("should accept ISO 8601 dates", function* () {
|
||||||
|
|
|
||||||
|
|
@ -1640,7 +1640,7 @@ describe("Zotero.ItemTree", function() {
|
||||||
cellText = win.document.querySelector('#zotero-items-tree .row.selected .cell.title .cell-text');
|
cellText = win.document.querySelector('#zotero-items-tree .row.selected .cell.title .cell-text');
|
||||||
}
|
}
|
||||||
while (!cellText);
|
while (!cellText);
|
||||||
assert.equal(cellText.innerHTML, 'Review of <i xmlns="http://www.w3.org/1999/xhtml">Review of <i style="font-style: normal;">B<sub>oo</sub>k</i> <another-tag/></i>');
|
assert.equal(cellText.innerHTML, 'Review of <i xmlns="http://www.w3.org/1999/xhtml">Review of <span style="font-style: normal;">B<sub>oo</sub>k</span> <another-tag/></i>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,18 @@
|
||||||
describe('Zotero_Import_Mendeley', function () {
|
describe('Zotero_Import_Mendeley', function () {
|
||||||
var server, httpd, httpdURL, importers;
|
var server, httpd, httpdURL, importers;
|
||||||
|
|
||||||
const getImporter = () => {
|
const getImporter = ({ importNotes = false } = {}) => {
|
||||||
const importer = new Zotero_Import_Mendeley();
|
const importer = new Zotero_Import_Mendeley();
|
||||||
importer.mendeleyAuth = { kind: 'direct', tokens: { accessToken: 'access_token', refreshToken: 'refresh_token' } };
|
importer.mendeleyAuth = { kind: 'direct', tokens: { accessToken: 'access_token', refreshToken: 'refresh_token' } };
|
||||||
importer.skipNotebooks = true;
|
importer.skipNotebooks = !importNotes;
|
||||||
|
if (importNotes) {
|
||||||
|
importer._refManagerToken = {
|
||||||
|
kind: 'referenceManager',
|
||||||
|
accessToken: 'refman_token',
|
||||||
|
username: 'username',
|
||||||
|
password: 'password'
|
||||||
|
};
|
||||||
|
}
|
||||||
importers.push(importer);
|
importers.push(importer);
|
||||||
return importer;
|
return importer;
|
||||||
};
|
};
|
||||||
|
|
@ -14,7 +22,7 @@ describe('Zotero_Import_Mendeley', function () {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js');
|
Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js');
|
||||||
|
|
||||||
// real http server is used to deliver an empty pdf so that annotations can be processed during import
|
// A real HTTP server is used to deliver a Bitcoin PDF so that annotations can be processed during import.
|
||||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||||
const port = 16213;
|
const port = 16213;
|
||||||
httpd = new HttpServer();
|
httpd = new HttpServer();
|
||||||
|
|
@ -22,7 +30,7 @@ describe('Zotero_Import_Mendeley', function () {
|
||||||
httpd.start(port);
|
httpd.start(port);
|
||||||
httpd.registerFile(
|
httpd.registerFile(
|
||||||
'/file1.pdf',
|
'/file1.pdf',
|
||||||
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'empty.pdf'))
|
Zotero.File.pathToFile(OS.Path.join(getTestDataDirectory().path, 'recognizePDF_test_title.pdf'))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -84,7 +92,7 @@ describe('Zotero_Import_Mendeley', function () {
|
||||||
|
|
||||||
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `profiles/v2/me?`,
|
url: `profiles/v2/me`,
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {},
|
headers: {},
|
||||||
json: JSON.parse(
|
json: JSON.parse(
|
||||||
|
|
@ -104,7 +112,7 @@ describe('Zotero_Import_Mendeley', function () {
|
||||||
|
|
||||||
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `files/19fb5e5b-1a39-4851-b513-d48441a670e1?`,
|
url: `files/19fb5e5b-1a39-4851-b513-d48441a670e1`,
|
||||||
status: 200, // ideally would be 303 but mock http doesn't like it
|
status: 200, // ideally would be 303 but mock http doesn't like it
|
||||||
headers: {
|
headers: {
|
||||||
Location: `${httpdURL}/file1.pdf`
|
Location: `${httpdURL}/file1.pdf`
|
||||||
|
|
@ -526,7 +534,7 @@ describe('Zotero_Import_Mendeley', function () {
|
||||||
annotation.annotationText = 'Highlight text';
|
annotation.annotationText = 'Highlight text';
|
||||||
annotation.annotationComment = 'Highlight comment';
|
annotation.annotationComment = 'Highlight comment';
|
||||||
annotation.annotationPageLabel = '57';
|
annotation.annotationPageLabel = '57';
|
||||||
annotation.addRelation('mendeleyDB:annotationUUID', '84f12446-3b49-4052-bbdc-832d28e1e072');
|
annotation.addRelation('mendeleyDB:annotationUUID', '339d0202-d99f-48a2-aa0d-9b0c5631af26');
|
||||||
await annotation.saveTx();
|
await annotation.saveTx();
|
||||||
|
|
||||||
let mendeleyNotebook = JSON.parse(
|
let mendeleyNotebook = JSON.parse(
|
||||||
|
|
@ -544,5 +552,85 @@ describe('Zotero_Import_Mendeley', function () {
|
||||||
await attachment.eraseTx();
|
await attachment.eraseTx();
|
||||||
await item.eraseTx();
|
await item.eraseTx();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should import a notebook from Mendeley', async () => {
|
||||||
|
const importer = getImporter({ importNotes: true });
|
||||||
|
|
||||||
|
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
||||||
|
method: 'GET',
|
||||||
|
url: `notes/v1?limit=500`,
|
||||||
|
status: 200,
|
||||||
|
headers: {},
|
||||||
|
json: JSON.parse(
|
||||||
|
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/notebooks.json')
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
||||||
|
method: 'GET',
|
||||||
|
url: 'notes/v1/8041a43b-f740-41ec-be42-a24cc5106d68',
|
||||||
|
status: 200,
|
||||||
|
headers: {},
|
||||||
|
json: JSON.parse(
|
||||||
|
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/notebook.json')
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
await importer.translate({
|
||||||
|
libraryID: Zotero.Libraries.userLibraryID,
|
||||||
|
collections: null,
|
||||||
|
linkFiles: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const note = (await Zotero.Relations
|
||||||
|
.getByPredicateAndObject('item', 'mendeleyDB:notebookUUID', '8041a43b-f740-41ec-be42-a24cc5106d68'))
|
||||||
|
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
|
||||||
|
.shift();
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
note.note,
|
||||||
|
/^<h1>TEST<\/h1>\n<p><span class="highlight" data-annotation="((?:(?:%[0-9A-F]{2}|[^<>'" %])+))">“A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution”<\/span> <span class="citation" data-citation="((?:(?:%[0-9A-F]{2}|[^<>'" %])+))">\(<span class="citation-item">“Item with PDF”, 2005, p. 1<\/span>\)<\/span><\/p>\n<p>Lorem Ipsum<\/p>$/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle incorrect annotations', async () => {
|
||||||
|
let annotationsResponse = JSON.parse(
|
||||||
|
await Zotero.File.getContentsFromURLAsync('resource://zotero-unit-tests/data/mendeleyMock/annotations.json')
|
||||||
|
);
|
||||||
|
delete annotationsResponse.find(a => a.id == '885615a7-170e-4613-af80-0227ea76ae55').type;
|
||||||
|
delete annotationsResponse.find(a => a.id == 'bfbdb972-171d-4b21-8ae6-f156ac9a2b41').color;
|
||||||
|
annotationsResponse.push({ type: 'note', id: 'invalid' });
|
||||||
|
setHTTPResponse(server, 'https://api.mendeley.com/', {
|
||||||
|
method: 'GET',
|
||||||
|
url: `annotations?limit=200`,
|
||||||
|
status: 200,
|
||||||
|
headers: {},
|
||||||
|
json: annotationsResponse
|
||||||
|
});
|
||||||
|
|
||||||
|
const importer = getImporter();
|
||||||
|
await importer.translate({
|
||||||
|
libraryID: Zotero.Libraries.userLibraryID,
|
||||||
|
collections: null,
|
||||||
|
linkFiles: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdf = (await Zotero.Relations
|
||||||
|
.getByPredicateAndObject('item', 'mendeleyDB:fileHash', 'cc22c6611277df346ff8dc7386ba3880b2bafa15'))
|
||||||
|
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
|
||||||
|
.shift();
|
||||||
|
|
||||||
|
const annotations = await pdf.getAnnotations();
|
||||||
|
// Typeless notes and invalid annotations should be discarded.
|
||||||
|
assert.lengthOf(annotations, 5);
|
||||||
|
assert.lengthOf(await Zotero.Relations.getByPredicateAndObject('item', 'mendeleyDB:annotationUUID', '885615a7-170e-4613-af80-0227ea76ae55'), 0);
|
||||||
|
assert.lengthOf(await Zotero.Relations.getByPredicateAndObject('item', 'mendeleyDB:annotationUUID', 'invalid'), 0);
|
||||||
|
// Colorless note annotations should have a default colour.
|
||||||
|
const colorlessNote = (await Zotero.Relations
|
||||||
|
.getByPredicateAndObject('item', 'mendeleyDB:annotationUUID', 'bfbdb972-171d-4b21-8ae6-f156ac9a2b41'))
|
||||||
|
.filter(item => item.libraryID == Zotero.Libraries.userLibraryID && !item.deleted)
|
||||||
|
.shift();
|
||||||
|
assert.equal(colorlessNote.annotationColor, '#ffd400');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1354,8 +1354,8 @@ describe("Connector Server", function () {
|
||||||
var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP);
|
var origRequest = Zotero.HTTP.request.bind(Zotero.HTTP);
|
||||||
var called = 0;
|
var called = 0;
|
||||||
var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) {
|
var stub = sinon.stub(Zotero.HTTP, 'request').callsFake(function (method, url, options) {
|
||||||
// Forward saveSnapshot request
|
// Forward saveSnapshot and PDF requests
|
||||||
if (url.endsWith('saveSnapshot')) {
|
if (url.endsWith('saveSnapshot') || url.endsWith('test.pdf')) {
|
||||||
return origRequest(...arguments);
|
return origRequest(...arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,33 @@ describe("Zotero.Sync.Data.Local", function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle local item becoming child and remote being added to a collection", async function () {
|
||||||
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
|
||||||
|
var collection = await createDataObject('collection');
|
||||||
|
var parentItem = await createDataObject('item');
|
||||||
|
var childItem = await createDataObject('item', { itemType: 'note' });
|
||||||
|
var data = childItem.toJSON();
|
||||||
|
await Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [data]);
|
||||||
|
|
||||||
|
// Add to parent locally
|
||||||
|
childItem.parentID = parentItem.id;
|
||||||
|
await childItem.saveTx();
|
||||||
|
|
||||||
|
// Add to collection remotely
|
||||||
|
data.collections = [collection.key];
|
||||||
|
var json = {
|
||||||
|
key: childItem.key,
|
||||||
|
version: 10,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
await Zotero.Sync.Data.Local.processObjectsFromJSON(
|
||||||
|
'item', libraryID, [json], { stopOnError: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.isTrue(collection.hasItem(parentItem));
|
||||||
|
});
|
||||||
|
|
||||||
it("should automatically resolve collection name conflict", async function () {
|
it("should automatically resolve collection name conflict", async function () {
|
||||||
var libraryID = Zotero.Libraries.userLibraryID;
|
var libraryID = Zotero.Libraries.userLibraryID;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,31 @@ describe("Item Tags Box", function () {
|
||||||
// New empty tag should have focus
|
// New empty tag should have focus
|
||||||
assert.exists(doc.activeElement.closest("[isNew]"));
|
assert.exists(doc.activeElement.closest("[isNew]"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should save tag edits when another item is selected", async function () {
|
||||||
|
let notSelectedItem = await createDataObject('item');
|
||||||
|
var tag = Zotero.Utilities.randomString();
|
||||||
|
var updatedTag = Zotero.Utilities.randomString();
|
||||||
|
|
||||||
|
let selectedItem = await createDataObject('item', { tags: [{ tag }] });
|
||||||
|
var tagsbox = doc.querySelector('#zotero-editpane-tags');
|
||||||
|
var rows = tagsbox.querySelectorAll('.row editable-text');
|
||||||
|
assert.equal(rows.length, 1);
|
||||||
|
|
||||||
|
// type something
|
||||||
|
var firstRow = rows[0];
|
||||||
|
firstRow.focus();
|
||||||
|
firstRow.ref.value = updatedTag;
|
||||||
|
firstRow.ref.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// change the item by clicking on another row in itemTree
|
||||||
|
let promise = waitForItemEvent('modify');
|
||||||
|
win.ZoteroPane.selectItem(notSelectedItem.id);
|
||||||
|
// selectedItem should be modified
|
||||||
|
await promise;
|
||||||
|
// make sure that the tag was actually updated
|
||||||
|
assert.equal(selectedItem.getTags()[0].tag, updatedTag);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -738,4 +738,170 @@ describe("Zotero.Utilities.Internal", function () {
|
||||||
assert.equal(html, 'yes1yes2 yes3');
|
assert.equal(html, 'yes1yes2 yes3');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("OpenURL", function () {
|
||||||
|
var item;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
item = await createDataObject('item', { title: 'Foo Bar', date: '2024-12-19' });
|
||||||
|
})
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
Zotero.Prefs.clear('openURL.resolver');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#resolve()", function () {
|
||||||
|
it("should add trailing '?' if no query string", async function () {
|
||||||
|
Zotero.Prefs.set("openURL.resolver", "https://resolver.ebsco.com/c/abcdef/result");
|
||||||
|
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
|
||||||
|
assert.include(url, 'result?url_ver=Z39.88-2004');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add trailing '&' if already a query string", async function () {
|
||||||
|
Zotero.Prefs.set("openURL.resolver", "https://resolver.ebscohost.com/openurl?custid=abcdef&groupid=main&profile=ftf&authtype=ip,uid");
|
||||||
|
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
|
||||||
|
assert.include(url, 'authtype=ip,uid&url_ver=Z39.88-2004');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add trailing '?' after /login?url=", async function () {
|
||||||
|
Zotero.Prefs.set("openURL.resolver", "https://proxy.school.edu/login?url=https://resolver.ebscohost.com/openurl");
|
||||||
|
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
|
||||||
|
assert.include(url, 'openurl?url_ver=Z39.88-2004');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't add trailing '?' after /login?url= if URL already ends in '?'", async function () {
|
||||||
|
Zotero.Prefs.set("openURL.resolver", "https://proxy.school.edu/login?url=https://resolver.ebscohost.com/openurl?");
|
||||||
|
var url = Zotero.Utilities.Internal.OpenURL.resolve(item);
|
||||||
|
assert.include(url, 'openurl?url_ver=Z39.88-2004');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#renderItemTitle()", function () {
|
||||||
|
function renderToElement(title) {
|
||||||
|
let elem = new DOMParser().parseFromString('<div></div>', 'text/html')
|
||||||
|
.querySelector('div');
|
||||||
|
Zotero.Utilities.Internal.renderItemTitle(title, elem);
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToHTML(title) {
|
||||||
|
return renderToElement(title).innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToTextViaHTML(title) {
|
||||||
|
return renderToElement(title).textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToText(title) {
|
||||||
|
return Zotero.Utilities.Internal.renderItemTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should render a title without tags unchanged", function () {
|
||||||
|
assert.equal(renderToHTML('My Title'), 'My Title');
|
||||||
|
assert.equal(renderToHTML('1 < 2'), '1 < 2');
|
||||||
|
assert.equal(renderToHTML('<<ATTENTION>> READ THIS'), '<<ATTENTION>> READ THIS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render single-level tags", function () {
|
||||||
|
let examples = [
|
||||||
|
'My <i>Title</i> XYZ',
|
||||||
|
'My <b>Title</b> XYZ',
|
||||||
|
'My <sub>Title</sub> XYZ',
|
||||||
|
'My <sup>Title</sup> XYZ',
|
||||||
|
];
|
||||||
|
for (let example of examples) {
|
||||||
|
assert.equal(renderToHTML(example), example);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(renderToHTML('My <span class="nocase">Title</span> XYZ'), 'My <span>Title</span> XYZ');
|
||||||
|
// Citeproc wants no space, DOM output includes space. Confusing!
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <span style="font-variant:small-caps;">Title</span> XYZ'),
|
||||||
|
'My <span style="font-variant: small-caps;">Title</span> XYZ'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a tag nested inside a different tag", function () {
|
||||||
|
let examples = [
|
||||||
|
'My <i><b>Title</b></i> XYZ',
|
||||||
|
'My <b><i>Title</i></b> XYZ',
|
||||||
|
'My <sub><i>Title</i></sub> XYZ',
|
||||||
|
];
|
||||||
|
for (let example of examples) {
|
||||||
|
assert.equal(renderToHTML(example), example);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve mismatched opening and closing tags", function () {
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <i><b>Title</i> XYZ'),
|
||||||
|
'My <i><b>Title</i> XYZ'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <b>Title</i></b> XYZ'),
|
||||||
|
'My <b>Title</i></b> XYZ'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <sub><span class="font-variant:small-caps;">Title</sub> XYZ'),
|
||||||
|
'My <sub><span class="font-variant:small-caps;">Title</sub> XYZ'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <sub><span class="font-variant:small-caps;">Title</sub></span> XYZ'),
|
||||||
|
'My <sub><span class="font-variant:small-caps;">Title</sub></span> XYZ'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve matched but unsupported tags", function () {
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <em>Title</em> XYZ'),
|
||||||
|
'My <em>Title</em> XYZ'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <strong>Title</strong> XYZ'),
|
||||||
|
'My <strong>Title</strong> XYZ'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <span class="font-variant: small-caps;">Title</span> XYZ'), // Extra space
|
||||||
|
'My <span class="font-variant: small-caps;">Title</span> XYZ'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('My <span>Title</span> XYZ'), // Extra space
|
||||||
|
'My <span>Title</span> XYZ'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should invert a tag nested inside itself", function () {
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('<i>A <i>Title</i> in Italics</i>'),
|
||||||
|
'<i>A <span style="font-style: normal;">Title</span> in Italics</i>'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('<b>A <b>Title</b> in Bold</b>'),
|
||||||
|
'<b>A <span style="font-weight: normal;">Title</span> in Bold</b>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should invert a tag nested inside itself, with a different tag in between", function () {
|
||||||
|
assert.equal(
|
||||||
|
renderToHTML('<i>A <span class="nocase"><i>Title</i></span> in Italics</i>'),
|
||||||
|
'<i>A <span><span style="font-style: normal;">Title</span></span> in Italics</i>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the same text directly and via DOM", function () {
|
||||||
|
let examples = [
|
||||||
|
'Basic Title',
|
||||||
|
'Complicated <i>Title</i>',
|
||||||
|
'Complicated <i>Title</i> With More Text',
|
||||||
|
'Nested <i>T<b>i</b>tle</i> With More Text',
|
||||||
|
'Self-Nested <i>T<i>i</i>tle</i> With More Text',
|
||||||
|
'Fe<sub>1−<i>x</i></sub>O',
|
||||||
|
'Title with <marquee>INVALID TAGS</marquee> <strong>like this</strong>',
|
||||||
|
];
|
||||||
|
for (let example of examples) {
|
||||||
|
assert.equal(renderToTextViaHTML(example), renderToText(example));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -768,6 +768,50 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
||||||
win.close();
|
win.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should show an error for a non-DAV URL", async function () {
|
||||||
|
Zotero.HTTP.mock = null;
|
||||||
|
Zotero.Prefs.set("sync.storage.url", davHostPath);
|
||||||
|
|
||||||
|
httpd.registerPathHandler(
|
||||||
|
`${davBasePath}zotero/`,
|
||||||
|
{
|
||||||
|
handle: function (request, response) {
|
||||||
|
// Force Basic Auth
|
||||||
|
if (!request.hasHeader('Authorization')) {
|
||||||
|
response.setStatusLine(null, 401, null);
|
||||||
|
response.setHeader('WWW-Authenticate', 'Basic realm="WebDAV"', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.setStatusLine(null, 200, "OK");
|
||||||
|
response.write("<html><body><p>This is a non-DAV URL.</p></body></html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Begin install procedure
|
||||||
|
var win = await loadPrefPane('sync');
|
||||||
|
var button = win.document.getElementById('storage-verify');
|
||||||
|
|
||||||
|
var spy = sinon.spy(win.Zotero_Preferences.Sync, "verifyStorageServer");
|
||||||
|
var promise1 = waitForDialog(function (dialog) {
|
||||||
|
assert.include(
|
||||||
|
dialog.document.documentElement.textContent,
|
||||||
|
Zotero.getString(
|
||||||
|
'sync.storage.error.webdav.invalidURL',
|
||||||
|
davScheme + '://' + davHostPath + 'zotero/'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
button.click();
|
||||||
|
await promise1;
|
||||||
|
|
||||||
|
var promise2 = spy.returnValues[0];
|
||||||
|
spy.restore();
|
||||||
|
await promise2;
|
||||||
|
|
||||||
|
win.close();
|
||||||
|
});
|
||||||
|
|
||||||
it("should show an error for a 403", function* () {
|
it("should show an error for a 403", function* () {
|
||||||
Zotero.HTTP.mock = null;
|
Zotero.HTTP.mock = null;
|
||||||
httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
httpd.stop(() => defer.resolve());
|
httpd.stop(() => defer.resolve());
|
||||||
yield defer.promise;
|
yield defer.promise;
|
||||||
win.close();
|
win.close();
|
||||||
|
Zotero.HTTP.disableErrorRetry = false;
|
||||||
})
|
})
|
||||||
|
|
||||||
after(function* () {
|
after(function* () {
|
||||||
|
|
@ -211,6 +212,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () {
|
||||||
item.attachmentSyncState = "to_download";
|
item.attachmentSyncState = "to_download";
|
||||||
yield item.saveTx();
|
yield item.saveTx();
|
||||||
|
|
||||||
|
Zotero.HTTP.disableErrorRetry = true;
|
||||||
httpd.registerPathHandler(
|
httpd.registerPathHandler(
|
||||||
`/users/1/items/${item.key}/file`,
|
`/users/1/items/${item.key}/file`,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1692,6 +1692,38 @@ describe("ZoteroPane", function() {
|
||||||
assert.equal(parentItem.getAttachments().length, 4);
|
assert.equal(parentItem.getAttachments().length, 4);
|
||||||
assert.equal(epubAttachment.getField('title'), Zotero.getString('file-type-ebook'));
|
assert.equal(epubAttachment.getField('title'), Zotero.getString('file-type-ebook'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Linked file renaming", function () {
|
||||||
|
before(() => {
|
||||||
|
Zotero.Prefs.set('autoRenameFiles.linked', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
Zotero.Prefs.clear('autoRenameFiles.linked');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only rename and change the title of the first PDF attachment", async function () {
|
||||||
|
let testFile = getTestDataDirectory();
|
||||||
|
testFile.append('test.pdf');
|
||||||
|
|
||||||
|
let tempDir = await getTempDirectory();
|
||||||
|
let copy1 = PathUtils.join(tempDir, 'copy1.pdf');
|
||||||
|
let copy2 = PathUtils.join(tempDir, 'copy2.pdf');
|
||||||
|
|
||||||
|
await IOUtils.copy(testFile.path, copy1);
|
||||||
|
await IOUtils.copy(testFile.path, copy2);
|
||||||
|
|
||||||
|
let parentItem = await createDataObject('item', { title: 'Foo' });
|
||||||
|
|
||||||
|
let [attachment1] = await zp.addAttachmentFromDialog(false, parentItem.id, [copy1]);
|
||||||
|
assert.equal(attachment1.getField('title'), Zotero.getString('file-type-pdf'));
|
||||||
|
assert.equal(attachment1.attachmentFilename, 'Foo.pdf');
|
||||||
|
|
||||||
|
let [attachment2] = await zp.addAttachmentFromDialog(false, parentItem.id, [copy2]);
|
||||||
|
assert.equal(attachment2.getField('title'), 'copy2');
|
||||||
|
assert.equal(attachment2.attachmentFilename, 'copy2.pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#createParentItemsFromSelected()", function () {
|
describe("#createParentItemsFromSelected()", function () {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 4cadb3e1455e6438b0b7c530d509cddca5f3e1c6
|
Subproject commit 484110c925d71ec9212709ac420fa03e4f928da8
|
||||||
2
version
2
version
|
|
@ -1 +1 @@
|
||||||
7.0.11.SOURCE
|
7.0.15.SOURCE
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue