Unify attachment opening code and support EPUB/snapshot note links (#3705)
This commit is contained in:
parent
b1333d0e9d
commit
a7d59a90d3
11 changed files with 696 additions and 465 deletions
|
@ -27,9 +27,6 @@
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
class AttachmentPreview extends XULElementBase {
|
class AttachmentPreview extends XULElementBase {
|
||||||
static fileTypeMap = {
|
static fileTypeMap = {
|
||||||
'application/pdf': 'pdf',
|
|
||||||
'application/epub+zip': 'epub',
|
|
||||||
'text/html': 'snapshot',
|
|
||||||
// TODO: support video and audio
|
// TODO: support video and audio
|
||||||
// 'video/mp4': 'video',
|
// 'video/mp4': 'video',
|
||||||
// 'video/webm': 'video',
|
// 'video/webm': 'video',
|
||||||
|
@ -111,6 +108,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
get previewType() {
|
get previewType() {
|
||||||
|
if (this._item?.attachmentReaderType) {
|
||||||
|
return this._item.attachmentReaderType;
|
||||||
|
}
|
||||||
let contentType = this._item?.attachmentContentType;
|
let contentType = this._item?.attachmentContentType;
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
return "file";
|
return "file";
|
||||||
|
|
|
@ -370,11 +370,6 @@ var Zotero_LocateMenu = new function() {
|
||||||
snapshot: "zotero-menuitem-attachments-snapshot",
|
snapshot: "zotero-menuitem-attachments-snapshot",
|
||||||
multiple: "zotero-menuitem-new-tab",
|
multiple: "zotero-menuitem-new-tab",
|
||||||
};
|
};
|
||||||
const attachmentTypes = {
|
|
||||||
"application/pdf": "pdf",
|
|
||||||
"application/epub+zip": "epub",
|
|
||||||
"text/html": "snapshot",
|
|
||||||
};
|
|
||||||
this._attachmentType = "multiple";
|
this._attachmentType = "multiple";
|
||||||
Object.defineProperty(this, "className", {
|
Object.defineProperty(this, "className", {
|
||||||
get: () => (alternateWindowBehavior ? "zotero-menuitem-new-window" : classNames[this._attachmentType]),
|
get: () => (alternateWindowBehavior ? "zotero-menuitem-new-window" : classNames[this._attachmentType]),
|
||||||
|
@ -404,7 +399,7 @@ var Zotero_LocateMenu = new function() {
|
||||||
this._attachmentType = "multiple";
|
this._attachmentType = "multiple";
|
||||||
}
|
}
|
||||||
else if (attachment) {
|
else if (attachment) {
|
||||||
this._attachmentType = attachmentTypes[attachment.attachmentContentType];
|
this._attachmentType = attachment.attachmentReaderType;
|
||||||
}
|
}
|
||||||
return !!attachment;
|
return !!attachment;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1914,7 +1914,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
||||||
if (!parentItem.isFileAttachment()) {
|
if (!parentItem.isFileAttachment()) {
|
||||||
throw new Error("Annotation parent must be a file attachment");
|
throw new Error("Annotation parent must be a file attachment");
|
||||||
}
|
}
|
||||||
if (!['application/pdf', 'application/epub+zip', 'text/html'].includes(parentItem.attachmentContentType)) {
|
if (!parentItem.attachmentReaderType) {
|
||||||
throw new Error("Annotation parent must be a PDF, EPUB, or HTML snapshot");
|
throw new Error("Annotation parent must be a PDF, EPUB, or HTML snapshot");
|
||||||
}
|
}
|
||||||
let type = this._getLatestField('annotationType');
|
let type = this._getLatestField('annotationType');
|
||||||
|
@ -3103,6 +3103,25 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentContentType', {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Zotero.defineProperty(Zotero.Item.prototype, 'attachmentReaderType', {
|
||||||
|
get() {
|
||||||
|
if (!this.isFileAttachment()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
switch (this.attachmentContentType) {
|
||||||
|
case 'application/pdf':
|
||||||
|
return 'pdf';
|
||||||
|
case 'application/epub+zip':
|
||||||
|
return 'epub';
|
||||||
|
case 'text/html':
|
||||||
|
return 'snapshot';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
Zotero.Item.prototype.getAttachmentCharset = function() {
|
Zotero.Item.prototype.getAttachmentCharset = function() {
|
||||||
Zotero.debug("getAttachmentCharset() deprecated -- use .attachmentCharset");
|
Zotero.debug("getAttachmentCharset() deprecated -- use .attachmentCharset");
|
||||||
return this.attachmentCharset;
|
return this.attachmentCharset;
|
||||||
|
|
501
chrome/content/zotero/xpcom/fileHandlers.js
Normal file
501
chrome/content/zotero/xpcom/fileHandlers.js
Normal file
|
@ -0,0 +1,501 @@
|
||||||
|
/*
|
||||||
|
***** BEGIN LICENSE BLOCK *****
|
||||||
|
|
||||||
|
Copyright © 2018 Center for History and New Media
|
||||||
|
George Mason University, Fairfax, Virginia, USA
|
||||||
|
https://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 *****
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable array-element-newline */
|
||||||
|
|
||||||
|
Zotero.FileHandlers = {
|
||||||
|
async open(item, params) {
|
||||||
|
let { location, openInWindow = false } = params || {};
|
||||||
|
|
||||||
|
let path = await item.getFilePathAsync();
|
||||||
|
if (!path) {
|
||||||
|
Zotero.warn(`File not found: ${item.attachmentPath}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug('Opening ' + path);
|
||||||
|
|
||||||
|
let readerType = item.attachmentReaderType;
|
||||||
|
|
||||||
|
// Not a file that we/external readers handle with page number support -
|
||||||
|
// just open it with the system handler
|
||||||
|
if (!readerType) {
|
||||||
|
Zotero.debug('No associated reader type -- launching default application');
|
||||||
|
Zotero.launchFile(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = Zotero.Prefs.get(`fileHandler.${readerType}`);
|
||||||
|
if (!handler) {
|
||||||
|
Zotero.debug('No external handler for ' + readerType + ' -- opening in Zotero');
|
||||||
|
await Zotero.Reader.open(item.id, location, {
|
||||||
|
openInWindow,
|
||||||
|
allowDuplicate: openInWindow
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let systemHandler = this._getSystemHandler(item.attachmentContentType);
|
||||||
|
|
||||||
|
if (handler === 'system') {
|
||||||
|
handler = systemHandler;
|
||||||
|
Zotero.debug(`System handler is ${handler}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Zotero.debug(`Custom handler is ${handler}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let handlers;
|
||||||
|
if (this._mockHandlers) {
|
||||||
|
handlers = this._mockHandlers[readerType];
|
||||||
|
}
|
||||||
|
else if (Zotero.isMac) {
|
||||||
|
handlers = this._handlersMac[readerType];
|
||||||
|
}
|
||||||
|
else if (Zotero.isWin) {
|
||||||
|
handlers = this._handlersWin[readerType];
|
||||||
|
}
|
||||||
|
else if (Zotero.isLinux) {
|
||||||
|
handlers = this._handlersLinux[readerType];
|
||||||
|
}
|
||||||
|
|
||||||
|
let page = location?.position?.pageIndex ?? undefined;
|
||||||
|
// Add 1 to page index for external readers
|
||||||
|
if (page !== undefined && parseInt(page) == page) {
|
||||||
|
page = parseInt(page) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are handlers for this platform and this reader type...
|
||||||
|
if (handlers) {
|
||||||
|
// First try to open with the custom handler
|
||||||
|
try {
|
||||||
|
for (let [i, { name, open }] of handlers.entries()) {
|
||||||
|
if (name.test(handler)) {
|
||||||
|
Zotero.debug('Opening with handler ' + i);
|
||||||
|
await open(handler, { filePath: path, location, page });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, we don't have special handling for the custom
|
||||||
|
// handler that the user has set. If we have a location, we really
|
||||||
|
// want to open with something we know how to pass a page number to,
|
||||||
|
// so we'll see if we know how to do that for the system handler.
|
||||||
|
if (location) {
|
||||||
|
try {
|
||||||
|
if (systemHandler && handler !== systemHandler) {
|
||||||
|
Zotero.debug(`Custom handler did not match -- falling back to system handler ${systemHandler}`);
|
||||||
|
handler = systemHandler;
|
||||||
|
for (let [i, { name, open }] of handlers.entries()) {
|
||||||
|
if (name.test(handler)) {
|
||||||
|
Zotero.debug('Opening with handler ' + i);
|
||||||
|
await open(handler, { filePath: path, location, page });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// And lastly, the fallback handler for this platform/reader type,
|
||||||
|
// if we have one
|
||||||
|
let fallback = handlers.find(h => h.fallback);
|
||||||
|
if (fallback) {
|
||||||
|
try {
|
||||||
|
Zotero.debug('Opening with fallback');
|
||||||
|
await fallback.open(null, { filePath: path, location, page });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Don't log error if fallback fails
|
||||||
|
// Just move on and try system handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug("Opening handler without page number");
|
||||||
|
|
||||||
|
handler = handler || systemHandler;
|
||||||
|
if (handler) {
|
||||||
|
if (Zotero.isMac) {
|
||||||
|
try {
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await OS.File.exists(handler)) {
|
||||||
|
Zotero.debug(`Opening with handler ${handler}`);
|
||||||
|
Zotero.launchFileWithApplication(path, handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
|
}
|
||||||
|
Zotero.logError(`${handler} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
Zotero.debug('Launching file normally');
|
||||||
|
Zotero.launchFile(path);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_handlersMac: {
|
||||||
|
pdf: [
|
||||||
|
{
|
||||||
|
name: /Preview/,
|
||||||
|
fallback: true,
|
||||||
|
async open(appPath, { filePath, page }) {
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', "Preview", filePath]);
|
||||||
|
if (page !== undefined) {
|
||||||
|
// Go to page using AppleScript
|
||||||
|
let args = [
|
||||||
|
'-e', 'tell app "Preview" to activate',
|
||||||
|
'-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
|
||||||
|
'-e', `tell app "System Events" to keystroke "${page}"`,
|
||||||
|
'-e', 'tell app "System Events" to keystroke return'
|
||||||
|
];
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: /Adobe Acrobat/,
|
||||||
|
async open(appPath, { page }) {
|
||||||
|
if (page !== undefined) {
|
||||||
|
// Go to page using AppleScript
|
||||||
|
let args = [
|
||||||
|
'-e', `tell app "${appPath}" to activate`,
|
||||||
|
'-e', 'tell app "System Events" to keystroke "n" using {command down, shift down}',
|
||||||
|
'-e', `tell app "System Events" to keystroke "${page}"`,
|
||||||
|
'-e', 'tell app "System Events" to keystroke return'
|
||||||
|
];
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: /Skim/,
|
||||||
|
async open(appPath, { filePath, page }) {
|
||||||
|
// Escape double-quotes in path
|
||||||
|
var quoteRE = /"/g;
|
||||||
|
filePath = filePath.replace(quoteRE, '\\"');
|
||||||
|
let args = [
|
||||||
|
'-e', `tell app "${appPath}" to activate`,
|
||||||
|
'-e', `tell app "${appPath}" to open "${filePath}"`
|
||||||
|
];
|
||||||
|
if (page !== undefined) {
|
||||||
|
let filename = OS.Path.basename(filePath)
|
||||||
|
.replace(quoteRE, '\\"');
|
||||||
|
args.push('-e', `tell document "${filename}" of application "${appPath}" to go to page ${page}`);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: /PDF Expert/,
|
||||||
|
async open(appPath, { page }) {
|
||||||
|
// Go to page using AppleScript (same as Preview)
|
||||||
|
let args = [
|
||||||
|
'-e', `tell app "${appPath}" to activate`
|
||||||
|
];
|
||||||
|
if (page !== undefined) {
|
||||||
|
args.push(
|
||||||
|
'-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
|
||||||
|
'-e', `tell app "System Events" to keystroke "${page}"`,
|
||||||
|
'-e', 'tell app "System Events" to keystroke return'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
epub: [
|
||||||
|
{
|
||||||
|
name: /Calibre/i,
|
||||||
|
async open(appPath, { filePath, location }) {
|
||||||
|
if (!appPath.endsWith('ebook-viewer.app')) {
|
||||||
|
appPath += '/Contents/ebook-viewer.app';
|
||||||
|
}
|
||||||
|
let args = ['-a', appPath, filePath];
|
||||||
|
if (location?.position?.value) {
|
||||||
|
args.push('--args', '--open-at=' + location.position.value);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec('/usr/bin/open', args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
_handlersWin: {
|
||||||
|
pdf: [
|
||||||
|
{
|
||||||
|
name: new RegExp(''), // Match any handler
|
||||||
|
async open(appPath, { filePath, page }) {
|
||||||
|
let args = [filePath];
|
||||||
|
if (page !== undefined) {
|
||||||
|
// Include flags to open the PDF on a given page in various apps
|
||||||
|
//
|
||||||
|
// Adobe Acrobat: http://partners.adobe.com/public/developer/en/acrobat/PDFOpenParameters.pdf
|
||||||
|
// PDF-XChange: http://help.tracker-software.com/eu/default.aspx?pageid=PDFXView25:command_line_options
|
||||||
|
args.unshift('/A', 'page=' + page);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec(appPath, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
epub: [
|
||||||
|
{
|
||||||
|
name: /Calibre/i,
|
||||||
|
async open(appPath, { filePath, location }) {
|
||||||
|
if (appPath.toLowerCase().endsWith('calibre.exe')) {
|
||||||
|
appPath = appPath.slice(0, -11) + 'ebook-viewer.exe';
|
||||||
|
}
|
||||||
|
let args = [filePath];
|
||||||
|
if (location?.position?.value) {
|
||||||
|
args.push('--open-at=' + location.position.value);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec(appPath, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
_handlersLinux: {
|
||||||
|
pdf: [
|
||||||
|
{
|
||||||
|
name: /evince|okular/i,
|
||||||
|
fallback: true,
|
||||||
|
async open(appPath, { filePath, page }) {
|
||||||
|
if (appPath) {
|
||||||
|
switch (appPath.toLowerCase()) {
|
||||||
|
case 'okular':
|
||||||
|
appPath = '/usr/bin/okular';
|
||||||
|
|
||||||
|
// It's "Document Viewer" on stock Ubuntu
|
||||||
|
case 'document viewer':
|
||||||
|
case 'evince':
|
||||||
|
appPath = '/usr/bin/evince';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (await OS.File.exists('/usr/bin/okular')) {
|
||||||
|
appPath = '/usr/bin/okular';
|
||||||
|
}
|
||||||
|
else if (await OS.File.exists('/usr/bin/evince')) {
|
||||||
|
appPath = '/usr/bin/evince';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('No PDF reader found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Try to get default from mimeapps.list, etc., in case system default is okular
|
||||||
|
// or evince somewhere other than /usr/bin
|
||||||
|
|
||||||
|
let args = [filePath];
|
||||||
|
if (page !== undefined) {
|
||||||
|
args.unshift('-p', page);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec(appPath, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
epub: [
|
||||||
|
{
|
||||||
|
name: /calibre/i,
|
||||||
|
async open(appPath, { filePath, location }) {
|
||||||
|
if (appPath.toLowerCase().endsWith('calibre')) {
|
||||||
|
appPath = appPath.slice(0, -7) + 'ebook-viewer';
|
||||||
|
}
|
||||||
|
let args = [filePath];
|
||||||
|
if (location?.position?.value) {
|
||||||
|
args.push('--open-at=' + location.position.value);
|
||||||
|
}
|
||||||
|
await Zotero.Utilities.Internal.exec(appPath, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
_getSystemHandler(mimeType) {
|
||||||
|
if (Zotero.isWin) {
|
||||||
|
return this._getSystemHandlerWin(mimeType);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this._getSystemHandlerPOSIX(mimeType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getSystemHandlerWin(mimeType) {
|
||||||
|
// Based on getPDFReader() in ZotFile (GPL)
|
||||||
|
// https://github.com/jlegewie/zotfile/blob/a6c9e02e17b60cbc1f9bb4062486548d9ef583e3/chrome/content/zotfile/utils.js
|
||||||
|
|
||||||
|
var wrk = Components.classes["@mozilla.org/windows-registry-key;1"]
|
||||||
|
.createInstance(Components.interfaces.nsIWindowsRegKey);
|
||||||
|
// Get handler
|
||||||
|
var extension = Zotero.MIME.getPrimaryExtension(mimeType);
|
||||||
|
var tryKeys = [
|
||||||
|
{
|
||||||
|
root: wrk.ROOT_KEY_CURRENT_USER,
|
||||||
|
path: `Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.${extension}\\UserChoice`,
|
||||||
|
value: 'Progid'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: wrk.ROOT_KEY_CLASSES_ROOT,
|
||||||
|
path: `.${extension}`,
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
var progId;
|
||||||
|
for (let key of tryKeys) {
|
||||||
|
try {
|
||||||
|
wrk.open(key.root, key.path, wrk.ACCESS_READ);
|
||||||
|
progId = wrk.readStringValue(key.value);
|
||||||
|
if (progId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!progId) {
|
||||||
|
wrk.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get version specific handler, if it exists
|
||||||
|
try {
|
||||||
|
wrk.open(
|
||||||
|
wrk.ROOT_KEY_CLASSES_ROOT,
|
||||||
|
progId + '\\CurVer',
|
||||||
|
wrk.ACCESS_READ
|
||||||
|
);
|
||||||
|
progId = wrk.readStringValue('') || progId;
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
|
||||||
|
// Get command
|
||||||
|
var success = false;
|
||||||
|
tryKeys = [
|
||||||
|
progId + '\\shell\\Read\\command',
|
||||||
|
progId + '\\shell\\Open\\command'
|
||||||
|
];
|
||||||
|
for (let key of tryKeys) {
|
||||||
|
try {
|
||||||
|
wrk.open(
|
||||||
|
wrk.ROOT_KEY_CLASSES_ROOT,
|
||||||
|
key,
|
||||||
|
wrk.ACCESS_READ
|
||||||
|
);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
wrk.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var command = wrk.readStringValue('').match(/^(?:".+?"|[^"]\S+)/);
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
|
||||||
|
wrk.close();
|
||||||
|
|
||||||
|
if (!command) return false;
|
||||||
|
return command[0].replace(/"/g, '');
|
||||||
|
},
|
||||||
|
|
||||||
|
_getSystemHandlerPOSIX(mimeType) {
|
||||||
|
var handlerService = Cc["@mozilla.org/uriloader/handler-service;1"]
|
||||||
|
.getService(Ci.nsIHandlerService);
|
||||||
|
var handlers = handlerService.enumerate();
|
||||||
|
var handler;
|
||||||
|
while (handlers.hasMoreElements()) {
|
||||||
|
let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
|
||||||
|
if (handlerInfo.type == mimeType) {
|
||||||
|
handler = handlerInfo;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handler) {
|
||||||
|
// We can't get the name of the system default handler unless we add an entry
|
||||||
|
Zotero.debug("Default handler not found -- adding default entry");
|
||||||
|
let mimeService = Components.classes["@mozilla.org/mime;1"]
|
||||||
|
.getService(Components.interfaces.nsIMIMEService);
|
||||||
|
let mimeInfo = mimeService.getFromTypeAndExtension(mimeType, "");
|
||||||
|
mimeInfo.preferredAction = 4;
|
||||||
|
mimeInfo.alwaysAskBeforeHandling = false;
|
||||||
|
handlerService.store(mimeInfo);
|
||||||
|
|
||||||
|
// And once we do that, we can get the name (but not the path, unfortunately)
|
||||||
|
let handlers = handlerService.enumerate();
|
||||||
|
while (handlers.hasMoreElements()) {
|
||||||
|
let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
|
||||||
|
if (handlerInfo.type == mimeType) {
|
||||||
|
handler = handlerInfo;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handler) {
|
||||||
|
Zotero.debug(`Default handler is ${handler.defaultDescription}`);
|
||||||
|
return handler.defaultDescription;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Zotero.OpenPDF = {
|
||||||
|
openToPage: async function (pathOrItem, page, annotationKey) {
|
||||||
|
Zotero.warn('Zotero.OpenPDF.openToPage() is deprecated -- use Zotero.FileHandlers.open()');
|
||||||
|
if (typeof pathOrItem === 'string') {
|
||||||
|
throw new Error('Zotero.OpenPDF.openToPage() requires an item -- update your code!');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Zotero.FileHandlers.open(pathOrItem, {
|
||||||
|
location: {
|
||||||
|
annotationID: annotationKey,
|
||||||
|
position: {
|
||||||
|
pageIndex: page,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,353 +0,0 @@
|
||||||
/*
|
|
||||||
***** BEGIN LICENSE BLOCK *****
|
|
||||||
|
|
||||||
Copyright © 2018 Center for History and New Media
|
|
||||||
George Mason University, Fairfax, Virginia, USA
|
|
||||||
https://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 *****
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable array-element-newline */
|
|
||||||
|
|
||||||
Zotero.OpenPDF = {
|
|
||||||
openToPage: async function (pathOrItem, page, annotationKey) {
|
|
||||||
var handler = Zotero.Prefs.get("fileHandler.pdf");
|
|
||||||
|
|
||||||
var path;
|
|
||||||
if (pathOrItem == 'string') {
|
|
||||||
Zotero.logError("Zotero.OpenPDF.openToPage() now takes a Zotero.Item rather than a path "
|
|
||||||
+ "-- please update your code");
|
|
||||||
path = pathOrItem;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let item = pathOrItem;
|
|
||||||
let library = Zotero.Libraries.get(item.libraryID);
|
|
||||||
// Zotero PDF reader
|
|
||||||
if (!handler) {
|
|
||||||
let location = {
|
|
||||||
annotationID: annotationKey,
|
|
||||||
pageIndex: page && page - 1
|
|
||||||
};
|
|
||||||
await Zotero.Reader.open(item.id, location);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = await item.getFilePathAsync();
|
|
||||||
if (!path) {
|
|
||||||
Zotero.warn(`${path} not found`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var opened = false;
|
|
||||||
|
|
||||||
if (handler != 'system') {
|
|
||||||
Zotero.debug(`Custom handler is ${handler}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Zotero.isMac) {
|
|
||||||
if (!this._openWithHandlerMac(handler, path, page)) {
|
|
||||||
// Try to detect default app
|
|
||||||
handler = this._getPDFHandlerName();
|
|
||||||
if (!this._openWithHandlerMac(handler, path, page)) {
|
|
||||||
// Fall back to Preview
|
|
||||||
this._openWithPreview(path, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opened = true;
|
|
||||||
}
|
|
||||||
else if (Zotero.isWin) {
|
|
||||||
if (handler == 'system') {
|
|
||||||
handler = this._getPDFHandlerWindows();
|
|
||||||
if (handler) {
|
|
||||||
Zotero.debug(`Default handler is ${handler}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (handler) {
|
|
||||||
// Include flags to open the PDF on a given page in various apps
|
|
||||||
//
|
|
||||||
// Adobe Acrobat: http://partners.adobe.com/public/developer/en/acrobat/PDFOpenParameters.pdf
|
|
||||||
// PDF-XChange: http://help.tracker-software.com/eu/default.aspx?pageid=PDFXView25:command_line_options
|
|
||||||
let args = ['/A', 'page=' + page, path];
|
|
||||||
Zotero.Utilities.Internal.exec(handler, args);
|
|
||||||
opened = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Zotero.debug("No handler found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Zotero.isLinux) {
|
|
||||||
if (handler == 'system') {
|
|
||||||
handler = await this._getPDFHandlerLinux();
|
|
||||||
if (handler) {
|
|
||||||
Zotero.debug(`Resolved handler is ${handler}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (handler && (handler.includes('evince') || handler.includes('okular'))) {
|
|
||||||
this._openWithEvinceOrOkular(handler, path, page);
|
|
||||||
opened = true;
|
|
||||||
}
|
|
||||||
// Fall back to okular and then evince if unknown handler
|
|
||||||
else if (await OS.File.exists('/usr/bin/okular')) {
|
|
||||||
this._openWithEvinceOrOkular('/usr/bin/okular', path, page);
|
|
||||||
opened = true;
|
|
||||||
}
|
|
||||||
else if (await OS.File.exists('/usr/bin/evince')) {
|
|
||||||
this._openWithEvinceOrOkular('/usr/bin/evince', path, page);
|
|
||||||
opened = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Zotero.debug("No handler found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return opened;
|
|
||||||
},
|
|
||||||
|
|
||||||
_getPDFHandlerName: function () {
|
|
||||||
var handlerService = Cc["@mozilla.org/uriloader/handler-service;1"]
|
|
||||||
.getService(Ci.nsIHandlerService);
|
|
||||||
var handlers = handlerService.enumerate();
|
|
||||||
var handler;
|
|
||||||
while (handlers.hasMoreElements()) {
|
|
||||||
let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
|
|
||||||
if (handlerInfo.type == 'application/pdf') {
|
|
||||||
handler = handlerInfo;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!handler) {
|
|
||||||
// We can't get the name of the system default handler unless we add an entry
|
|
||||||
Zotero.debug("Default handler not found -- adding default entry");
|
|
||||||
let mimeService = Components.classes["@mozilla.org/mime;1"]
|
|
||||||
.getService(Components.interfaces.nsIMIMEService);
|
|
||||||
let mimeInfo = mimeService.getFromTypeAndExtension("application/pdf", "");
|
|
||||||
mimeInfo.preferredAction = 4;
|
|
||||||
mimeInfo.alwaysAskBeforeHandling = false;
|
|
||||||
handlerService.store(mimeInfo);
|
|
||||||
|
|
||||||
// And once we do that, we can get the name (but not the path, unfortunately)
|
|
||||||
let handlers = handlerService.enumerate();
|
|
||||||
while (handlers.hasMoreElements()) {
|
|
||||||
let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
|
|
||||||
if (handlerInfo.type == 'application/pdf') {
|
|
||||||
handler = handlerInfo;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (handler) {
|
|
||||||
Zotero.debug(`Default handler is ${handler.defaultDescription}`);
|
|
||||||
return handler.defaultDescription;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Mac
|
|
||||||
//
|
|
||||||
_openWithHandlerMac: function (handler, path, page) {
|
|
||||||
if (!handler) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (handler.includes('Preview')) {
|
|
||||||
this._openWithPreview(path, page);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (handler.includes('Adobe Acrobat')) {
|
|
||||||
this._openWithAcrobat(handler, path, page);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (handler.includes('Skim')) {
|
|
||||||
this._openWithSkim(handler, path, page);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (handler.includes('PDF Expert')) {
|
|
||||||
this._openWithPDFExpert(handler, path, page);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
_openWithPreview: async function (filePath, page) {
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', "Preview", filePath]);
|
|
||||||
// Go to page using AppleScript
|
|
||||||
let args = [
|
|
||||||
'-e', 'tell app "Preview" to activate',
|
|
||||||
'-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
|
|
||||||
'-e', `tell app "System Events" to keystroke "${page}"`,
|
|
||||||
'-e', 'tell app "System Events" to keystroke return'
|
|
||||||
];
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
|
||||||
},
|
|
||||||
|
|
||||||
_openWithAcrobat: async function (appPath, filePath, page) {
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', appPath, filePath]);
|
|
||||||
// Go to page using AppleScript
|
|
||||||
let args = [
|
|
||||||
'-e', `tell app "${appPath}" to activate`,
|
|
||||||
'-e', 'tell app "System Events" to keystroke "n" using {command down, shift down}',
|
|
||||||
'-e', `tell app "System Events" to keystroke "${page}"`,
|
|
||||||
'-e', 'tell app "System Events" to keystroke return'
|
|
||||||
];
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
|
||||||
},
|
|
||||||
|
|
||||||
_openWithSkim: async function (appPath, filePath, page) {
|
|
||||||
// Escape double-quotes in path
|
|
||||||
var quoteRE = /"/g;
|
|
||||||
filePath = filePath.replace(quoteRE, '\\"');
|
|
||||||
let filename = OS.Path.basename(filePath).replace(quoteRE, '\\"');
|
|
||||||
let args = [
|
|
||||||
'-e', `tell app "${appPath}" to activate`,
|
|
||||||
'-e', `tell app "${appPath}" to open "${filePath}"`
|
|
||||||
];
|
|
||||||
args.push('-e', `tell document "${filename}" of application "${appPath}" to go to page ${page}`);
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
|
||||||
},
|
|
||||||
|
|
||||||
_openWithPDFExpert: async function (appPath, filePath, page) {
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', appPath, filePath]);
|
|
||||||
// Go to page using AppleScript (same as Preview)
|
|
||||||
let args = [
|
|
||||||
'-e', `tell app "${appPath}" to activate`,
|
|
||||||
'-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
|
|
||||||
'-e', `tell app "System Events" to keystroke "${page}"`,
|
|
||||||
'-e', 'tell app "System Events" to keystroke return'
|
|
||||||
];
|
|
||||||
await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Windows
|
|
||||||
//
|
|
||||||
/**
|
|
||||||
* Get path to default pdf reader application on windows
|
|
||||||
*
|
|
||||||
* From getPDFReader() in ZotFile (GPL)
|
|
||||||
* https://github.com/jlegewie/zotfile/blob/master/chrome/content/zotfile/utils.js
|
|
||||||
*
|
|
||||||
* @return {String|false} - Path to default pdf reader application, or false if none
|
|
||||||
*/
|
|
||||||
_getPDFHandlerWindows: function () {
|
|
||||||
var wrk = Components.classes["@mozilla.org/windows-registry-key;1"]
|
|
||||||
.createInstance(Components.interfaces.nsIWindowsRegKey);
|
|
||||||
// Get handler for PDFs
|
|
||||||
var tryKeys = [
|
|
||||||
{
|
|
||||||
root: wrk.ROOT_KEY_CURRENT_USER,
|
|
||||||
path: 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.pdf\\UserChoice',
|
|
||||||
value: 'Progid'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
root: wrk.ROOT_KEY_CLASSES_ROOT,
|
|
||||||
path: '.pdf',
|
|
||||||
value: ''
|
|
||||||
}
|
|
||||||
];
|
|
||||||
var progId;
|
|
||||||
for (let i = 0; !progId && i < tryKeys.length; i++) {
|
|
||||||
try {
|
|
||||||
wrk.open(
|
|
||||||
tryKeys[i].root,
|
|
||||||
tryKeys[i].path,
|
|
||||||
wrk.ACCESS_READ
|
|
||||||
);
|
|
||||||
progId = wrk.readStringValue(tryKeys[i].value);
|
|
||||||
}
|
|
||||||
catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!progId) {
|
|
||||||
wrk.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get version specific handler, if it exists
|
|
||||||
try {
|
|
||||||
wrk.open(
|
|
||||||
wrk.ROOT_KEY_CLASSES_ROOT,
|
|
||||||
progId + '\\CurVer',
|
|
||||||
wrk.ACCESS_READ
|
|
||||||
);
|
|
||||||
progId = wrk.readStringValue('') || progId;
|
|
||||||
}
|
|
||||||
catch (e) {}
|
|
||||||
|
|
||||||
// Get command
|
|
||||||
var success = false;
|
|
||||||
tryKeys = [
|
|
||||||
progId + '\\shell\\Read\\command',
|
|
||||||
progId + '\\shell\\Open\\command'
|
|
||||||
];
|
|
||||||
for (let i = 0; !success && i < tryKeys.length; i++) {
|
|
||||||
try {
|
|
||||||
wrk.open(
|
|
||||||
wrk.ROOT_KEY_CLASSES_ROOT,
|
|
||||||
tryKeys[i],
|
|
||||||
wrk.ACCESS_READ
|
|
||||||
);
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
wrk.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var command = wrk.readStringValue('').match(/^(?:".+?"|[^"]\S+)/);
|
|
||||||
}
|
|
||||||
catch (e) {}
|
|
||||||
|
|
||||||
wrk.close();
|
|
||||||
|
|
||||||
if (!command) return false;
|
|
||||||
return command[0].replace(/"/g, '');
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Linux
|
|
||||||
//
|
|
||||||
_getPDFHandlerLinux: async function () {
|
|
||||||
var name = this._getPDFHandlerName();
|
|
||||||
switch (name.toLowerCase()) {
|
|
||||||
case 'okular':
|
|
||||||
return `/usr/bin/${name}`;
|
|
||||||
|
|
||||||
// It's "Document Viewer" on stock Ubuntu
|
|
||||||
case 'document viewer':
|
|
||||||
case 'evince':
|
|
||||||
return `/usr/bin/evince`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Try to get default from mimeapps.list, etc., in case system default is okular
|
|
||||||
// or evince somewhere other than /usr/bin
|
|
||||||
var homeDir = OS.Constants.Path.homeDir;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
_openWithEvinceOrOkular: function (appPath, filePath, page) {
|
|
||||||
var args = ['-p', page, filePath];
|
|
||||||
Zotero.Utilities.Internal.exec(appPath, args);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -48,11 +48,9 @@ class ReaderInstance {
|
||||||
this._pendingWriteStateTimeout = null;
|
this._pendingWriteStateTimeout = null;
|
||||||
this._pendingWriteStateFunction = null;
|
this._pendingWriteStateFunction = null;
|
||||||
|
|
||||||
switch (this._item.attachmentContentType) {
|
this._type = this._item.attachmentReaderType;
|
||||||
case 'application/pdf': this._type = 'pdf'; break;
|
if (!this._type) {
|
||||||
case 'application/epub+zip': this._type = 'epub'; break;
|
throw new Error('Unsupported attachment type');
|
||||||
case 'text/html': this._type = 'snapshot'; break;
|
|
||||||
default: throw new Error('Unsupported attachment type');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Proxy(this, {
|
return new Proxy(this, {
|
||||||
|
@ -93,7 +91,7 @@ class ReaderInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecondViewState() {
|
getSecondViewState() {
|
||||||
let state = this._iframeWindow.wrappedJSObject.getSecondViewState();
|
let state = this._iframeWindow?.wrappedJSObject?.getSecondViewState?.();
|
||||||
return state ? JSON.parse(JSON.stringify(state)) : undefined;
|
return state ? JSON.parse(JSON.stringify(state)) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -998,6 +998,13 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
file.launch();
|
file.launch();
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
// macOS only: if there's no associated application, launch() will throw, but
|
||||||
|
// the OS will show a dialog asking the user to choose an application. We don't
|
||||||
|
// want to show the Firefox dialog in that case.
|
||||||
|
if (Zotero.isMac && file.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Zotero.debug(e, 2);
|
Zotero.debug(e, 2);
|
||||||
Zotero.debug("launch() not supported -- trying fallback executable", 2);
|
Zotero.debug("launch() not supported -- trying fallback executable", 2);
|
||||||
|
|
||||||
|
@ -1013,18 +1020,19 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
Zotero.debug(e);
|
Zotero.debug(e);
|
||||||
Zotero.debug("Launching via executable failed -- passing to loadUrl()");
|
Zotero.debug("Launching via executable failed -- passing to loadURI()");
|
||||||
|
|
||||||
// If nsIFile.launch() isn't available and the fallback
|
// If nsIFile.launch() isn't available and the fallback
|
||||||
// executable doesn't exist, we just let the Firefox external
|
// executable doesn't exist, we just let the Firefox external
|
||||||
// helper app window handle it
|
// helper app window handle it
|
||||||
var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
|
var uri = Services.io.newFileURI(file);
|
||||||
.getService(Components.interfaces.nsIFileProtocolHandler);
|
|
||||||
var uri = nsIFPH.newFileURI(file);
|
|
||||||
|
|
||||||
var nsIEPS = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].
|
var nsIEPS = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].
|
||||||
getService(Components.interfaces.nsIExternalProtocolService);
|
getService(Components.interfaces.nsIExternalProtocolService);
|
||||||
nsIEPS.loadUrl(uri);
|
nsIEPS.loadURI(
|
||||||
|
uri,
|
||||||
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4908,64 +4908,15 @@ var ZoteroPane = new function()
|
||||||
await item.saveTx();
|
await item.saveTx();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['application/pdf', 'application/epub+zip', 'text/html'].includes(contentType)) {
|
let openInWindow = Zotero.Prefs.get('openReaderInNewWindow');
|
||||||
let type;
|
let useAlternateWindowBehavior = event?.shiftKey || extraData?.forceAlternateWindowBehavior;
|
||||||
if (contentType === 'application/pdf') {
|
if (useAlternateWindowBehavior) {
|
||||||
type = 'pdf';
|
openInWindow = !openInWindow;
|
||||||
}
|
|
||||||
else if (contentType === 'application/epub+zip') {
|
|
||||||
type = 'epub';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
type = 'snapshot';
|
|
||||||
}
|
|
||||||
let handler = Zotero.Prefs.get('fileHandler.' + type);
|
|
||||||
|
|
||||||
// Zotero PDF reader
|
|
||||||
if (!handler) {
|
|
||||||
let openInWindow = Zotero.Prefs.get('openReaderInNewWindow');
|
|
||||||
let useAlternateWindowBehavior = event?.shiftKey || extraData?.forceAlternateWindowBehavior;
|
|
||||||
if (useAlternateWindowBehavior) {
|
|
||||||
openInWindow = !openInWindow;
|
|
||||||
}
|
|
||||||
await Zotero.Reader.open(
|
|
||||||
item.id,
|
|
||||||
extraData && extraData.location,
|
|
||||||
{
|
|
||||||
openInWindow,
|
|
||||||
allowDuplicate: openInWindow
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Try to open external PDF reader to page number if specified
|
|
||||||
// TODO: Implement for EPUBs if readers support it
|
|
||||||
else if (type == 'pdf') {
|
|
||||||
let pageIndex = extraData?.location?.position?.pageIndex;
|
|
||||||
if (pageIndex !== undefined) {
|
|
||||||
await Zotero.OpenPDF.openToPage(
|
|
||||||
item,
|
|
||||||
parseInt(pageIndex) + 1
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Custom PDF handler
|
|
||||||
// TODO: Remove this and unify with Zotero.OpenPDF
|
|
||||||
if (handler != 'system') {
|
|
||||||
try {
|
|
||||||
if (await OS.File.exists(handler)) {
|
|
||||||
Zotero.launchFileWithApplication(path, handler);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.logError(e);
|
|
||||||
}
|
|
||||||
Zotero.logError(`${handler} not found -- launching file normally`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Zotero.launchFile(path);
|
await Zotero.FileHandlers.open(item, {
|
||||||
|
location: extraData?.location,
|
||||||
|
openInWindow,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < itemIDs.length; i++) {
|
for (let i = 0; i < itemIDs.length; i++) {
|
||||||
|
|
|
@ -1135,7 +1135,7 @@ function ZoteroProtocolHandler() {
|
||||||
* Also supports ZotFile format:
|
* Also supports ZotFile format:
|
||||||
* zotero://open-pdf/[libraryID]_[key]/[page]
|
* zotero://open-pdf/[libraryID]_[key]/[page]
|
||||||
*/
|
*/
|
||||||
var OpenPDFExtension = {
|
var OpenExtension = {
|
||||||
noContent: true,
|
noContent: true,
|
||||||
|
|
||||||
doAction: async function (uri) {
|
doAction: async function (uri) {
|
||||||
|
@ -1145,8 +1145,7 @@ function ZoteroProtocolHandler() {
|
||||||
if (!uriPath) {
|
if (!uriPath) {
|
||||||
return 'Invalid URL';
|
return 'Invalid URL';
|
||||||
}
|
}
|
||||||
uriPath = uriPath.substr('//open-pdf/'.length);
|
uriPath = uriPath.replace(/^\/\/open(-pdf)?\//, '');
|
||||||
var mimeType, content = '';
|
|
||||||
|
|
||||||
var params = {
|
var params = {
|
||||||
objectType: 'item'
|
objectType: 'item'
|
||||||
|
@ -1174,11 +1173,7 @@ function ZoteroProtocolHandler() {
|
||||||
|
|
||||||
Zotero.API.parseParams(params);
|
Zotero.API.parseParams(params);
|
||||||
var results = await Zotero.API.getResultsFromParams(params);
|
var results = await Zotero.API.getResultsFromParams(params);
|
||||||
var page = params.page;
|
var { annotation, page, cfi, sel } = params;
|
||||||
if (parseInt(page) != page) {
|
|
||||||
page = null;
|
|
||||||
}
|
|
||||||
var annotation = params.annotation;
|
|
||||||
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
Zotero.warn(`No item found for ${uriPath}`);
|
Zotero.warn(`No item found for ${uriPath}`);
|
||||||
|
@ -1198,32 +1193,43 @@ function ZoteroProtocolHandler() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path.toLowerCase().endsWith('.pdf')
|
try {
|
||||||
&& Zotero.MIME.sniffForMIMEType(await Zotero.File.getSample(path)) != 'application/pdf') {
|
if (page) {
|
||||||
Zotero.warn(`${path} is not a PDF`);
|
await Zotero.FileHandlers.open(item, {
|
||||||
return;
|
location: {
|
||||||
|
position: {
|
||||||
|
pageIndex: page
|
||||||
|
},
|
||||||
|
annotationID: annotation
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (cfi) {
|
||||||
|
await Zotero.FileHandlers.open(item, {
|
||||||
|
location: {
|
||||||
|
position: {
|
||||||
|
type: 'FragmentSelector',
|
||||||
|
conformsTo: 'http://www.idpf.org/epub/linking/cfi/epub-cfi.html',
|
||||||
|
value: cfi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (sel) {
|
||||||
|
await Zotero.FileHandlers.open(item, {
|
||||||
|
location: {
|
||||||
|
position: {
|
||||||
|
type: 'CssSelector',
|
||||||
|
value: sel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
Zotero.logError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
var opened = false;
|
|
||||||
if (page || annotation) {
|
|
||||||
try {
|
|
||||||
opened = await Zotero.OpenPDF.openToPage(item, page, annotation);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
Zotero.logError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If something went wrong, just open PDF without page
|
|
||||||
if (!opened) {
|
|
||||||
Zotero.debug("Launching PDF without page number");
|
|
||||||
let zp = Zotero.getActiveZoteroPane();
|
|
||||||
// TODO: Open pane if closed (macOS)
|
|
||||||
if (zp) {
|
|
||||||
zp.viewAttachment([item.id]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Zotero.Notifier.trigger('open', 'file', item.id);
|
Zotero.Notifier.trigger('open', 'file', item.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1241,7 +1247,8 @@ function ZoteroProtocolHandler() {
|
||||||
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
|
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
|
||||||
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
|
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
|
||||||
this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension;
|
this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension;
|
||||||
this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenPDFExtension;
|
this._extensions[ZOTERO_SCHEME + "://open"] = OpenExtension;
|
||||||
|
this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -113,7 +113,7 @@ const xpcomFilesLocal = [
|
||||||
'locateManager',
|
'locateManager',
|
||||||
'mime',
|
'mime',
|
||||||
'notifier',
|
'notifier',
|
||||||
'openPDF',
|
'fileHandlers',
|
||||||
'plugins',
|
'plugins',
|
||||||
'reader',
|
'reader',
|
||||||
'progressQueue',
|
'progressQueue',
|
||||||
|
|
105
test/tests/fileHandlersTest.js
Normal file
105
test/tests/fileHandlersTest.js
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
describe("Zotero.FileHandlers", () => {
|
||||||
|
describe("open()", () => {
|
||||||
|
var win;
|
||||||
|
|
||||||
|
function clearPrefs() {
|
||||||
|
Zotero.Prefs.clear('fileHandler.pdf');
|
||||||
|
Zotero.Prefs.clear('fileHandler.epub');
|
||||||
|
Zotero.Prefs.clear('fileHandler.snapshot');
|
||||||
|
Zotero.Prefs.clear('openReaderInNewWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
clearPrefs();
|
||||||
|
win = await loadZoteroPane();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
clearPrefs();
|
||||||
|
delete Zotero.FileHandlers._mockHandlers;
|
||||||
|
for (let reader of Zotero.Reader._readers) {
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
win.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open a PDF internally when no handler is set", async function () {
|
||||||
|
let pdf = await importFileAttachment('wonderland_short.pdf');
|
||||||
|
await Zotero.FileHandlers.open(pdf, {
|
||||||
|
location: { position: { pageIndex: 2 } }
|
||||||
|
});
|
||||||
|
assert.ok(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open a PDF in a new window when no handler is set and openInWindow is passed", async function () {
|
||||||
|
let pdf = await importFileAttachment('wonderland_short.pdf');
|
||||||
|
await Zotero.FileHandlers.open(pdf, {
|
||||||
|
location: { position: { pageIndex: 2 } },
|
||||||
|
openInWindow: true
|
||||||
|
});
|
||||||
|
assert.notOk(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
|
||||||
|
assert.isNotEmpty(Zotero.Reader.getWindowStates());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use matching handler", async function () {
|
||||||
|
let pdf = await importFileAttachment('wonderland_short.pdf');
|
||||||
|
let wasRun = false;
|
||||||
|
let readerOpenSpy = sinon.spy(Zotero.Reader, 'open');
|
||||||
|
Zotero.FileHandlers._mockHandlers = {
|
||||||
|
pdf: [
|
||||||
|
{
|
||||||
|
name: /mock/,
|
||||||
|
async open() {
|
||||||
|
wasRun = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
Zotero.Prefs.set('fileHandler.pdf', 'mock');
|
||||||
|
|
||||||
|
await Zotero.FileHandlers.open(pdf);
|
||||||
|
assert.isTrue(wasRun);
|
||||||
|
assert.isFalse(readerOpenSpy.called);
|
||||||
|
assert.notOk(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
|
||||||
|
assert.isEmpty(Zotero.Reader.getWindowStates());
|
||||||
|
|
||||||
|
readerOpenSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to fallback handler when location is passed", async function () {
|
||||||
|
let pdf = await importFileAttachment('wonderland_short.pdf');
|
||||||
|
let wasRun = false;
|
||||||
|
let readerOpenSpy = sinon.spy(Zotero.Reader, 'open');
|
||||||
|
Zotero.FileHandlers._mockHandlers = {
|
||||||
|
pdf: [
|
||||||
|
{
|
||||||
|
name: /mock/,
|
||||||
|
fallback: true,
|
||||||
|
async open(appPath) {
|
||||||
|
assert.notOk(appPath); // appPath won't be set when called as fallback
|
||||||
|
wasRun = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set our custom handler to something nonexistent,
|
||||||
|
// and stub the system handler to something nonexistent as well
|
||||||
|
Zotero.Prefs.set('fileHandler.pdf', 'some nonexistent tool');
|
||||||
|
let getSystemHandlerStub = sinon.stub(Zotero.FileHandlers, '_getSystemHandler');
|
||||||
|
getSystemHandlerStub.returns('some other nonexistent tool');
|
||||||
|
|
||||||
|
await Zotero.FileHandlers.open(pdf, { location: {} });
|
||||||
|
assert.isTrue(wasRun);
|
||||||
|
assert.isFalse(readerOpenSpy.called);
|
||||||
|
assert.notOk(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
|
||||||
|
assert.isEmpty(Zotero.Reader.getWindowStates());
|
||||||
|
|
||||||
|
readerOpenSpy.restore();
|
||||||
|
getSystemHandlerStub.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue