diff --git a/.gitmodules b/.gitmodules index 9f01abdd82..f56e6c9180 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,8 +31,8 @@ url = https://github.com/gildas-lormeau/SingleFile.git [submodule "pdf-reader"] path = pdf-reader - url = https://github.com/zotero/pdf-reader.git - branch = master + url = https://github.com/zotero/pdf-reader2.git + branch = reader2 [submodule "pdf-worker"] path = pdf-worker url = https://github.com/zotero/pdf-worker.git diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js index cfc76b0be4..570b764996 100644 --- a/chrome/content/zotero/contextPane.js +++ b/chrome/content/zotero/contextPane.js @@ -117,7 +117,7 @@ var ZoteroContextPane = new function () { _itemToggle.addEventListener('click', _toggleItemButton); _notesToggle.addEventListener('click', _toggleNotesButton); Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth; - Zotero.Reader.onChangeSidebarOpen = _updatePaneWidth; + Zotero.Reader.onToggleSidebar = _updatePaneWidth; }; this.destroy = function () { @@ -126,7 +126,7 @@ var ZoteroContextPane = new function () { window.removeEventListener('resize', _update); Zotero.Notifier.unregisterObserver(this._notifierID); Zotero.Reader.onChangeSidebarWidth = () => {}; - Zotero.Reader.onChangeSidebarOpen = () => {}; + Zotero.Reader.onToggleSidebar = () => {}; _contextPaneInner.innerHTML = ''; _itemContexts = []; _notesContexts = []; diff --git a/chrome/content/zotero/reader.xhtml b/chrome/content/zotero/reader.xhtml index 4d3921aa62..729417e76c 100644 --- a/chrome/content/zotero/reader.xhtml +++ b/chrome/content/zotero/reader.xhtml @@ -90,9 +90,9 @@ - - - + + + @@ -124,25 +124,19 @@ - + - @@ -153,93 +147,105 @@ full-screen-api.allow-trusted-requests-only=false and then hide all other visible window elements like toolbar, note sidebar, tabs, etc. --> - - - + - + @@ -252,28 +258,28 @@ @@ -312,7 +318,7 @@ type="content" primary="true" transparent="transparent" - src="resource://zotero/pdf-reader/viewer.html" + src="resource://zotero/pdf-reader/reader.html" flex="1"/> diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js index bc188b65e4..9215e59e17 100644 --- a/chrome/content/zotero/standalone/standalone.js +++ b/chrome/content/zotero/standalone/standalone.js @@ -32,7 +32,11 @@ const ZoteroStandalone = new function() { const FONT_SIZES = ["1.0", "1.15", "1.3", "1.5", "1.7", "1.9", "2.1"]; //const NOTE_FONT_SIZES = ["11", "12", "13", "14", "18", "24", "36", "48", "64", "72", "96"]; const NOTE_FONT_SIZE_DEFAULT = "12"; - + + Object.defineProperty(this, 'currentReader', { + get: () => Zotero.Reader.getByTabID(Zotero_Tabs.selectedID) + }); + /** * Run when standalone window first opens */ @@ -61,7 +65,16 @@ const ZoteroStandalone = new function() { this.updateQuickCopyOptions(); }, 0); // "library" or "reader" - this.switchMenuType(extraData[ids[0]].type); + let type = extraData[ids[0]].type; + this.switchMenuType(type); + if (type === 'reader') { + let reader = Zotero.Reader.getByTabID(ids[0]); + if (reader) { + // "pdf", "epub", "snapshot" + let subtype = reader.type; + this.switchReaderSubtype(subtype); + } + } setTimeout(() => ZoteroPane.updateToolbarPosition(), 0); } } @@ -135,11 +148,13 @@ const ZoteroStandalone = new function() { document.querySelectorAll('.menu-type-' + type).forEach(el => el.hidden = false); }; - this.onReaderCmd = function (cmd) { - let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); - reader.menuCmd(cmd); + this.switchReaderSubtype = function (subtype) { + document.querySelectorAll( + '.menu-type-reader.pdf, .menu-type-reader.epub, .menu-type-reader.snapshot' + ).forEach(el => el.hidden = true); + document.querySelectorAll('.menu-type-reader.' + subtype).forEach(el => el.hidden = false); }; - + this.onFileMenuOpen = function () { var active = false; try { @@ -381,10 +396,12 @@ const ZoteroStandalone = new function() { var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); if (reader) { - this.updateMenuItemEnabled('go-menuitem-first-page', reader.allowNavigateFirstPage()); - this.updateMenuItemEnabled('go-menuitem-last-page', reader.allowNavigateLastPage()); - this.updateMenuItemEnabled('go-menuitem-back', reader.allowNavigateBack()); - this.updateMenuItemEnabled('go-menuitem-forward', reader.allowNavigateForward()); + if (['pdf', 'epub'].includes(reader.type)) { + this.updateMenuItemEnabled('go-menuitem-first-page', reader.canNavigateToFirstPage); + this.updateMenuItemEnabled('go-menuitem-last-page', reader.canNavigateToLastPage); + } + this.updateMenuItemEnabled('go-menuitem-back', reader.canNavigateBack); + this.updateMenuItemEnabled('go-menuitem-forward', reader.canNavigateForward); } }; @@ -393,19 +410,20 @@ const ZoteroStandalone = new function() { // PDF Reader var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); if (reader) { - var { state } = reader; - this.updateMenuItemCheckmark('view-menuitem-vertical-scrolling', state.scrollMode == 0); - this.updateMenuItemCheckmark('view-menuitem-horizontal-scrolling', state.scrollMode == 1); - this.updateMenuItemCheckmark('view-menuitem-wrapped-scrolling', state.scrollMode == 2); - this.updateMenuItemCheckmark('view-menuitem-no-spreads', state.spreadMode == 0); - this.updateMenuItemCheckmark('view-menuitem-odd-spreads', state.spreadMode == 1); - this.updateMenuItemCheckmark('view-menuitem-even-spreads', state.spreadMode == 2); - this.updateMenuItemCheckmark('view-menuitem-hand-tool', reader.isHandToolActive()); - this.updateMenuItemCheckmark('view-menuitem-zoom-auto', reader.isZoomAutoActive()); - this.updateMenuItemCheckmark('view-menuitem-zoom-page-width', reader.isZoomPageWidthActive()); - this.updateMenuItemCheckmark('view-menuitem-zoom-page-height', reader.isZoomPageHeightActive()); - this.updateMenuItemCheckmark('view-menuitem-split-vertically', reader.isSplitVerticallyActive()); - this.updateMenuItemCheckmark('view-menuitem-split-horizontally', reader.isSplitHorizontallyActive()); + if (reader.type === 'pdf') { + this.updateMenuItemCheckmark('view-menuitem-hand-tool', reader.toolType === 'hand'); + this.updateMenuItemCheckmark('view-menuitem-vertical-scrolling', reader.scrollMode === 0); + this.updateMenuItemCheckmark('view-menuitem-horizontal-scrolling', reader.scrollMode === 1); + this.updateMenuItemCheckmark('view-menuitem-wrapped-scrolling', reader.scrollMode === 2); + this.updateMenuItemCheckmark('view-menuitem-no-spreads', reader.spreadMode === 0); + this.updateMenuItemCheckmark('view-menuitem-odd-spreads', reader.spreadMode === 1); + this.updateMenuItemCheckmark('view-menuitem-even-spreads', reader.spreadMode === 2); + this.updateMenuItemCheckmark('view-menuitem-zoom-auto', reader.zoomAutoEnabled); + this.updateMenuItemCheckmark('view-menuitem-zoom-page-width', reader.zoomPageWidthEnabled); + this.updateMenuItemCheckmark('view-menuitem-zoom-page-height', reader.zoomPageHeightEnabled); + } + this.updateMenuItemCheckmark('view-menuitem-split-vertically', reader.splitType === 'vertical'); + this.updateMenuItemCheckmark('view-menuitem-split-horizontally', reader.splitType === 'horizontal'); } // Layout mode diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index d33c6b1aef..b253bcab60 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -26,15 +26,13 @@ import FilePicker from 'zotero/modules/filePicker'; class ReaderInstance { - constructor() { + constructor(options) { this.pdfStateFileName = '.zotero-pdf-state'; this.annotationItemIDs = []; - this.onChangeSidebarWidth = null; - this.state = null; + this._item = options.item; this._instanceID = Zotero.Utilities.randomString(); this._window = null; this._iframeWindow = null; - this._itemID = null; this._title = ''; this._isReaderInitialized = false; this._showItemPaneToggle = false; @@ -42,6 +40,43 @@ class ReaderInstance { this._resolveInitPromise = resolve; this._rejectInitPromise = reject; }); + + switch (this._item.attachmentContentType) { + case 'application/pdf': this._type = 'pdf'; break; + case 'application/epub+zip': this._type = 'epub'; break; + case 'text/html': this._type = 'snapshot'; break; + default: throw new Error('Unsupported attachment type'); + } + + return new Proxy(this, { + get(target, prop) { + if (target[prop] === undefined + && target._internalReader + && target._internalReader[prop] !== undefined) { + if (typeof target._internalReader[prop] === 'function') { + return function (...args) { + return target._internalReader[prop](...args); + }; + } + return target._internalReader[prop]; + } + return target[prop]; + }, + set(originalTarget, prop, value) { + let target = originalTarget; + if (!originalTarget.hasOwnProperty(prop) + && originalTarget._internalReader + && target._internalReader[prop] !== undefined) { + target = originalTarget._internalReader; + } + target[prop] = value; + return true; + } + }); + } + + get type() { + return this._type; } focus() { @@ -93,20 +128,10 @@ class ReaderInstance { return true; } - async open({ itemID, state, location, secondViewState }) { - let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); - let library = Zotero.Libraries.get(libraryID); - await library.waitForDataLoad('item'); - - let item = Zotero.Items.get(itemID); - if (!item) { - return false; - } - this.state = state; - this._itemID = item.id; + async _open({ state, location, secondViewState }) { // Set `ReaderTab` title as fast as possible - await this.updateTitle(); - let path = await item.getFilePathAsync(); + this.updateTitle(); + let path = await this._item.getFilePathAsync(); // Check file size, otherwise we get uncatchable error: // JavaScript error: resource://gre/modules/osfile/osfile_native.jsm, line 60: RangeError: invalid array length // See more https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length @@ -117,942 +142,54 @@ class ReaderInstance { } let buf = await OS.File.read(path, {}); buf = new Uint8Array(buf).buffer; - let annotationItems = item.getAnnotations(); + let annotationItems = this._item.getAnnotations(); let annotations = (await Promise.all(annotationItems.map(x => this._getAnnotation(x)))).filter(x => x); // TODO: Remove after some time // Migrate Mendeley colors to Zotero PDF reader colors - let migrated = await this.migrateMendeleyColors(libraryID, annotations); + let migrated = await this.migrateMendeleyColors(this._item.libraryID, annotations); if (migrated) { - annotationItems = item.getAnnotations(); + annotationItems = this._item.getAnnotations(); annotations = (await Promise.all(annotationItems.map(x => this._getAnnotation(x)))).filter(x => x); } this.annotationItemIDs = annotationItems.map(x => x.id); state = state || await this._getState(); - this._postMessage({ - action: 'open', - buf, - annotations, - state, - secondViewState, - location, - readOnly: this._isReadOnly(), - authorName: item.library.libraryType === 'group' ? Zotero.Users.getCurrentName() : '', - showItemPaneToggle: this._showItemPaneToggle, - sidebarWidth: this._sidebarWidth, - sidebarOpen: this._sidebarOpen, - bottomPlaceholderHeight: this._bottomPlaceholderHeight, - rtl: Zotero.rtl, - fontSize: Zotero.Prefs.get('fontSize'), - localizedStrings: { - ...Zotero.Intl.getPrefixedStrings('general.'), - ...Zotero.Intl.getPrefixedStrings('pdfReader.') - } - }, [buf]); - // Set title once again, because `ReaderWindow` isn't loaded the first time - await this.updateTitle(); - this._prefObserverIDs = [ - Zotero.Prefs.registerObserver('fontSize', this._handleFontSizePrefChange), - Zotero.Prefs.registerObserver('tabs.title.reader', this._handleTabTitlePrefChange) - ]; - return true; - } - - uninit() { - if (this._prefObserverIDs) { - this._prefObserverIDs.forEach(id => Zotero.Prefs.unregisterObserver(id)); - } - } - - get itemID() { - return this._itemID; - } - - async updateTitle() { - let type = Zotero.Prefs.get('tabs.title.reader'); - let item = Zotero.Items.get(this._itemID); - let readerTitle = item.getDisplayTitle(); - let parentItem = item.parentItem; - // If type is "filename", then readerTitle already has it - if (parentItem && type !== 'filename') { - let attachment = await parentItem.getBestAttachment(); - if (attachment && attachment.id === this._itemID) { - let parts = []; - // Windows displays bidi control characters as placeholders in window titles, so strip them - // See https://github.com/mozilla-services/screenshots/issues/4863 - let unformatted = Zotero.isWin; - let creator = parentItem.getField('firstCreator', unformatted); - let year = parentItem.getField('year'); - let title = parentItem.getDisplayTitle(); - // If creator is missing fall back to titleCreatorYear - if (type === 'creatorYearTitle' && creator) { - parts = [creator, year, title]; - } - else if (type === 'title') { - parts = [title]; - } - // If type is titleCreatorYear, or is missing, or another type falls back - else { - parts = [title, creator, year]; - } - readerTitle = parts.filter(x => x).join(' - '); - } - } - this._title = readerTitle; - this._setTitleValue(readerTitle); - } - - async setAnnotations(items) { - let annotations = []; - for (let item of items) { - let annotation = await this._getAnnotation(item); - if (annotation) { - annotations.push(annotation); - } - } - if (annotations.length) { - let data = { action: 'setAnnotations', annotations }; - this._postMessage(data); - } - } - - unsetAnnotations(keys) { - let data = { action: 'unsetAnnotations', ids: keys }; - this._postMessage(data); - } - - async navigate(location) { - this._postMessage({ action: 'navigate', location }); - } - - enableAddToNote(enable) { - this._postMessage({ action: 'enableAddToNote', enable }); - } - - setSidebarWidth(width) { - this._postMessage({ action: 'setSidebarWidth', width }); - } - - setSidebarOpen(open) { - this._postMessage({ action: 'setSidebarOpen', open }); - } - - focusLastToolbarButton() { - this._iframeWindow.focus(); - this._postMessage({ action: 'focusLastToolbarButton' }); - } - - tabToolbar(reverse) { - this._postMessage({ action: 'tabToolbar', reverse }); - // Avoid toolbar find button being focused for a short moment - setTimeout(() => this._iframeWindow.focus()); - } - - focusFirst() { - this._postMessage({ action: 'focusFirst' }); - setTimeout(() => this._iframeWindow.focus()); - } - - async setBottomPlaceholderHeight(height) { - await this._initPromise; - this._postMessage({ action: 'setBottomPlaceholderHeight', height }); - } - - async setToolbarPlaceholderWidth(width) { - await this._initPromise; - this._postMessage({ action: 'setToolbarPlaceholderWidth', width }); - } - - isHandToolActive() { - return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfCursorTools.handTool.active; - } - - isZoomAutoActive() { - return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentScaleValue === 'auto'; - } - - isZoomPageWidthActive() { - return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentScaleValue === 'page-width'; - } - - isZoomPageHeightActive() { - return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentScaleValue === 'page-fit'; - } - - isSplitVerticallyActive() { - return this._iframeWindow.wrappedJSObject.getSplitType() === 'vertical'; - } - - isSplitHorizontallyActive() { - return this._iframeWindow.wrappedJSObject.getSplitType() === 'horizontal'; - } - - allowNavigateFirstPage() { - return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentPageNumber > 1; - } - - allowNavigateLastPage() { - return this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentPageNumber < this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.pagesCount; - } - - allowNavigateBack() { - try { - let { uid } = this._iframeWindow.history.state; - if (uid == 0) { - return false; - } - } - catch (e) { - } - return true; - } - - allowNavigateForward() { - try { - let { uid } = this._iframeWindow.history.state; - let length = this._iframeWindow.history.length; - if (uid == length - 1) { - return false; - } - } - catch (e) { - } - return true; - } - - promptToTransferAnnotations() { - let ps = Services.prompt; - let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING - + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; - let index = ps.confirmEx( - null, - Zotero.getString('pdfReader.promptTransferFromPDF.title'), - Zotero.getString('pdfReader.promptTransferFromPDF.text', Zotero.appName), - buttonFlags, - Zotero.getString('general.continue'), - null, null, null, {} - ); - return !index; - } - - promptToDeletePages(num) { - let ps = Services.prompt; - let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING - + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; - let index = ps.confirmEx( - null, - Zotero.getString('pdfReader.promptDeletePages.title'), - Zotero.getString( - 'pdfReader.promptDeletePages.text', - new Intl.NumberFormat().format(num), - num - ), - buttonFlags, - Zotero.getString('general.continue'), - null, null, null, {} - ); - return !index; - } - - async reload(data) { - let item = Zotero.Items.get(this._itemID); - let path = await item.getFilePathAsync(); - let buf = await OS.File.read(path, {}); - buf = new Uint8Array(buf).buffer; - this._postMessage({ action: 'reload', buf, data }, [buf]); - } - - async menuCmd(cmd) { - if (cmd === 'transferFromPDF') { - if (this.promptToTransferAnnotations(true)) { - try { - await Zotero.PDFWorker.import(this._itemID, true, '', true); - } - catch (e) { - if (e.name === 'PasswordException') { - Zotero.alert(null, Zotero.getString('general.error'), - Zotero.getString('pdfReader.promptPasswordProtected')); - } - throw e; - } - } - } - else if (cmd === 'export') { - let zp = Zotero.getActiveZoteroPane(); - zp.exportPDF(this._itemID); - return; - } - else if (cmd === 'showInLibrary') { - let win = Zotero.getMainWindow(); - if (win) { - let item = Zotero.Items.get(this._itemID); - let id = item.parentID || item.id; - win.ZoteroPane.selectItems([id]); - win.Zotero_Tabs.select('zotero-pane'); - win.focus(); - } - return; - } - else if (cmd === 'rotateLeft') { - await this._rotateCurrentPage(270); - return; - } - else if (cmd === 'rotateRight') { - await this._rotateCurrentPage(90); - return; - } - else if (cmd === 'rotate180') { - await this._rotateCurrentPage(180); - return; - } - else if (cmd === 'splitVertically') { - this._splitVertically(); - } - else if (cmd === 'splitHorizontally') { - this._splitHorizontally(); - } - - let data = { - action: 'menuCmd', - cmd - }; - this._postMessage(data); - } - - _initIframeWindow() { - this._iframeWindow.addEventListener('message', this._handleMessage); - this._iframeWindow.addEventListener('error', (event) => { - Zotero.logError(event.error); - }); - this._iframeWindow.wrappedJSObject.zoteroSetDataTransferAnnotations = (dataTransfer, annotations) => { - // A small hack to force serializeAnnotations to include image annotation - // even if image isn't saved and imageAttachmentKey isn't available - for (let annotation of annotations) { - if (annotation.image && !annotation.imageAttachmentKey) { - annotation.imageAttachmentKey = 'none'; - delete annotation.image; - } - } - let res = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations); - let tmpNote = new Zotero.Item('note'); - tmpNote.libraryID = Zotero.Libraries.userLibraryID; - tmpNote.setNote(res.html); - let items = [tmpNote]; - let format = Zotero.QuickCopy.getNoteFormat(); - Zotero.debug('Copying/dragging annotation(s) with ' + format); - format = Zotero.QuickCopy.unserializeSetting(format); - // Basically the same code is used in itemTree.jsx onDragStart - try { - if (format.mode === 'export') { - // If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown - // and Note HTML translators instead - if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { - let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, options: format.markdownOptions }; - let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, options: format.htmlOptions }; - Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => { - if (!worked) { - return; - } - Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => { - if (!worked) { - return; - } - dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n')); - dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n')); - }); - }); - } - else { - Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => { - if (!worked) { - return; - } - var text = obj.string.replace(/\r\n/g, '\n'); - // For Note HTML translator use body content only - if (format.id === Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { - // Use body content only - let parser = new DOMParser(); - let doc = parser.parseFromString(text, 'text/html'); - text = doc.body.innerHTML; - } - dataTransfer.setData('text/plain', text); - }); - } - } - } - catch (e) { - Zotero.debug(e); - } - }; - this._iframeWindow.wrappedJSObject.zoteroConfirmDeletion = function (plural) { - let ps = Services.prompt; - let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING - + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; - let index = ps.confirmEx( - null, - '', - Zotero.getString('pdfReader.deleteAnnotation.' + (plural ? 'plural' : 'singular')), - buttonFlags, - Zotero.getString('general.delete'), - null, null, null, {} - ); - return !index; - }; - - this._iframeWindow.wrappedJSObject.zoteroCopyImage = async (dataURL) => { - let parts = dataURL.split(','); - if (!parts[0].includes('base64')) { - return; - } - let mime = parts[0].match(/:(.*?);/)[1]; - let bstr = atob(parts[1]); - let n = bstr.length; - let u8arr = new Uint8Array(n); - while (n--) { - u8arr[n] = bstr.charCodeAt(n); - } - let imgTools = Components.classes["@mozilla.org/image/tools;1"] - .getService(Components.interfaces.imgITools); - let transferable = Components.classes['@mozilla.org/widget/transferable;1'] - .createInstance(Components.interfaces.nsITransferable); - let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1'] - .getService(Components.interfaces.nsIClipboard); - let img = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mime); - transferable.init(null); - let kNativeImageMime = 'application/x-moz-nativeimage'; - transferable.addDataFlavor(kNativeImageMime); - transferable.setTransferData(kNativeImageMime, img); - clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard); - }; - - this._iframeWindow.wrappedJSObject.zoteroSaveImageAs = async (dataURL) => { - let fp = new FilePicker(); - fp.init(this._iframeWindow, Zotero.getString('pdfReader.saveImageAs'), fp.modeSave); - fp.appendFilter("PNG", "*.png"); - fp.defaultString = Zotero.getString('fileTypes.image').toLowerCase() + '.png'; - let rv = await fp.show(); - if (rv === fp.returnOK || rv === fp.returnReplace) { - let outputPath = fp.file; - let parts = dataURL.split(','); - if (parts[0].includes('base64')) { - let bstr = atob(parts[1]); - let n = bstr.length; - let u8arr = new Uint8Array(n); - while (n--) { - u8arr[n] = bstr.charCodeAt(n); - } - await OS.File.writeAtomic(outputPath, u8arr); - } - } - }; - } - - async _setState(state) { - this.state = state; - let item = Zotero.Items.get(this._itemID); - if (item) { - item.setAttachmentLastPageIndex(state.pageIndex); - let file = Zotero.Attachments.getStorageDirectory(item); - if (!await OS.File.exists(file.path)) { - await Zotero.Attachments.createDirectoryForItem(item); - } - file.append(this.pdfStateFileName); - // Using `writeAtomic` instead of `putContentsAsync` to avoid - // using temp file that causes conflicts on simultaneous writes (on slow systems) - await OS.File.writeAtomic(file.path, JSON.stringify(state)); - } - } - - async _getState() { - let item = Zotero.Items.get(this._itemID); - let file = Zotero.Attachments.getStorageDirectory(item); - file.append(this.pdfStateFileName); - file = file.path; - let state; - try { - if (await OS.File.exists(file)) { - state = JSON.parse(await Zotero.File.getContentsAsync(file)); - } - } - catch (e) { - Zotero.logError(e); - } - - let pageIndex = item.getAttachmentLastPageIndex(); - if (state) { - if (Number.isInteger(pageIndex) && state.pageIndex !== pageIndex) { - state.pageIndex = pageIndex; - delete state.top; - delete state.left; - } - return state; - } - else if (Number.isInteger(pageIndex)) { - return { pageIndex }; - } - return null; - } - - _isReadOnly() { - let item = Zotero.Items.get(this._itemID); - return !item.isEditable() - || item.deleted - || item.parentItem && item.parentItem.deleted; - } - - _handleFontSizePrefChange = () => { - this._postMessage({ action: 'setFontSize', fontSize: Zotero.Prefs.get('fontSize') }); - }; - - _handleTabTitlePrefChange = async () => { - await this.updateTitle(); - }; - - _dataURLtoBlob(dataurl) { - let parts = dataurl.split(','); - let mime = parts[0].match(/:(.*?);/)[1]; - if (parts[0].indexOf('base64') !== -1) { - let bstr = atob(parts[1]); - let n = bstr.length; - let u8arr = new Uint8Array(n); - while (n--) { - u8arr[n] = bstr.charCodeAt(n); - } - return new this._iframeWindow.Blob([u8arr], { type: mime }); - } - } - - _getColorIcon(color, selected) { - let stroke = selected ? 'lightgray' : 'transparent'; - let fill = '%23' + color.slice(1); - return `data:image/svg+xml,`; - } - - async _rotateCurrentPage(degrees) { - let pageIndex = this._iframeWindow.wrappedJSObject.PDFViewerApplication.pdfViewer.currentPageNumber - 1; - this._postMessage({ action: 'reloading' }); - await Zotero.PDFWorker.rotatePages(this._itemID, [pageIndex], degrees, true); - await this.reload({ rotatedPageIndexes: [pageIndex] }); - } - - _splitVertically() { - if (this.isSplitVerticallyActive()) { - this._iframeWindow.wrappedJSObject.unsplitView(); - } - else { - this._iframeWindow.wrappedJSObject.splitView(); - } - setTimeout(() => this._updateSecondViewState(), 500); - } - - _splitHorizontally() { - if (this.isSplitHorizontallyActive()) { - this._iframeWindow.wrappedJSObject.unsplitView(); - } - else { - this._iframeWindow.wrappedJSObject.splitView(true); - } - setTimeout(() => this._updateSecondViewState(), 500); - } - - _openTagsPopup(item, selector) { - let menupopup = this._window.document.createXULElement('menupopup'); - menupopup.addEventListener('popuphidden', function (event) { - if (event.target === menupopup) { - menupopup.remove(); - } - }); - menupopup.className = 'tags-popup'; - menupopup.style.font = 'inherit'; - menupopup.style.minWidth = '300px'; - menupopup.setAttribute('ignorekeys', true); - let tagsbox = new (this._window.customElements.get('tags-box')); - menupopup.appendChild(tagsbox); - tagsbox.setAttribute('flex', '1'); - this._popupset.appendChild(menupopup); - let element = this._iframeWindow.document.querySelector(selector); - menupopup.openPopup(element, 'overlap', 0, 0, true); - tagsbox.mode = 'edit'; - tagsbox.item = item; - if (tagsbox.mode == 'edit' && tagsbox.count == 0) { - tagsbox.newTag(); - } - } - - _openPagePopup(data, secondView) { - let popup = this._window.document.createXULElement('menupopup'); - this._popupset.appendChild(popup); - popup.addEventListener('popuphidden', function () { - popup.remove(); - }); - let menuitem; - if (data.text) { - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('general.copy')); - menuitem.addEventListener('command', () => { - this._window.document.getElementById('menu_copy').click(); - }); - popup.appendChild(menuitem); - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - } - // Zoom in - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomIn')); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomIn' }, [], secondView); - }); - popup.appendChild(menuitem); - // Zoom out - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomOut')); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomOut' }, [], secondView); - }); - popup.appendChild(menuitem); - // Zoom 'Auto' - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomAuto')); - menuitem.setAttribute('type', 'checkbox'); - menuitem.setAttribute('checked', data.isZoomAuto); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomAuto' }, [], secondView); - }); - popup.appendChild(menuitem); - // Zoom 'Page Width' - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomPageWidth')); - menuitem.setAttribute('type', 'checkbox'); - menuitem.setAttribute('checked', data.isZoomPageWidth); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomPageWidth' }, [], secondView); - }); - popup.appendChild(menuitem); - // Zoom 'Page Height' - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.zoomPageHeight')); - menuitem.setAttribute('type', 'checkbox'); - menuitem.setAttribute('checked', data.isZoomPageHeight); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'zoomPageHeight' }, [], secondView); - }); - popup.appendChild(menuitem); - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - // Split Horizontally - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.splitHorizontally')); - menuitem.setAttribute('type', 'checkbox'); - menuitem.setAttribute('checked', this.isSplitHorizontallyActive()); - menuitem.addEventListener('command', () => this._splitHorizontally()); - popup.appendChild(menuitem); - // Split Vertically - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.splitVertically')); - menuitem.setAttribute('type', 'checkbox'); - menuitem.setAttribute('checked', this.isSplitVerticallyActive()); - menuitem.addEventListener('command', () => this._splitVertically()); - popup.appendChild(menuitem); - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - // Next page - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.nextPage')); - menuitem.setAttribute('disabled', !data.enableNextPage); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'nextPage' }, [], secondView); - }); - popup.appendChild(menuitem); - // Previous page - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.previousPage')); - menuitem.setAttribute('disabled', !data.enablePrevPage); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'prevPage' }, [], secondView); - }); - popup.appendChild(menuitem); - popup.openPopupAtScreen(data.x, data.y, true); - } - - _openAnnotationPopup(data) { - let popup = this._window.document.createXULElement('menupopup'); - this._popupset.appendChild(popup); - popup.addEventListener('popuphidden', function () { - popup.remove(); - }); - let menuitem; - // Add to note - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.addToNote')); - let hasActiveEditor = this._window.ZoteroContextPane && this._window.ZoteroContextPane.getActiveEditor(); - menuitem.setAttribute('disabled', !hasActiveEditor || !data.enableAddToNote); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'addToNote', - ids: data.ids - }); - }); - popup.appendChild(menuitem); - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - // Colors - for (let color of data.colors) { - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString(color[0])); - menuitem.className = 'menuitem-iconic'; - menuitem.setAttribute('disabled', data.readOnly); - menuitem.setAttribute('checked', color[1] === data.selectedColor); - menuitem.setAttribute('image', this._getColorIcon(color[1])); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'setAnnotationColor', - ids: data.ids, - color: color[1] - }); - }); - popup.appendChild(menuitem); - } - // Separator - if (data.enableEditPageNumber || data.enableEditHighlightedText) { - popup.appendChild(this._window.document.createXULElement('menuseparator')); - } - // Change page number - if (data.enableEditPageNumber) { - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.editPageNumber')); - menuitem.setAttribute('disabled', data.readOnly); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'openPageLabelPopup', - data - }); - }); - popup.appendChild(menuitem); - } - // Edit highlighted text - if (data.enableEditHighlightedText) { - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.editHighlightedText')); - menuitem.setAttribute('disabled', data.readOnly); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'editHighlightedText', - data - }); - }); - popup.appendChild(menuitem); - } - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - - if (data.enableImageOptions) { - // Copy Image - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.copyImage')); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'copyImage', data }); - }); - popup.appendChild(menuitem); - // Save Image As… - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.saveImageAs')); - menuitem.addEventListener('command', () => { - this._postMessage({ action: 'popupCmd', cmd: 'saveImageAs', data }); - }); - popup.appendChild(menuitem); - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - } - - // Delete - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('general.delete')); - menuitem.setAttribute('disabled', data.readOnly); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'deleteAnnotation', - ids: data.ids - }); - }); - popup.appendChild(menuitem); - - if (data.x) { - popup.openPopupAtScreen(data.x, data.y, false); - } - else if (data.selector) { - let element = this._iframeWindow.document.querySelector(data.selector); - popup.openPopup(element, 'after_start', 0, 0, true); - } - } - - _openColorPopup(data) { - let popup = this._window.document.createXULElement('menupopup'); - this._popupset.appendChild(popup); - popup.addEventListener('popuphidden', function () { - popup.remove(); - }); - let menuitem; - for (let color of data.colors) { - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString(color[0])); - menuitem.className = 'menuitem-iconic'; - menuitem.setAttribute('checked', color[1] === data.selectedColor); - menuitem.setAttribute('image', this._getColorIcon(color[1])); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'setColor', - color: color[1] - }); - }); - popup.appendChild(menuitem); - } - let element = this._iframeWindow.document.getElementById(data.elementID); - popup.openPopup(element, 'after_start', 0, 0, true); - } - - _openThumbnailPopup(data) { - let popup = this._window.document.createXULElement('menupopup'); - this._popupset.appendChild(popup); - popup.addEventListener('popuphidden', function () { - popup.remove(); - }); - let menuitem; - // Rotate Left - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.rotateLeft')); - menuitem.setAttribute('disabled', this._isReadOnly()); - menuitem.addEventListener('command', async () => { - this._postMessage({ action: 'reloading' }); - await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 270, true); - await this.reload({ rotatedPageIndexes: data.pageIndexes }); - }); - popup.appendChild(menuitem); - // Rotate Right - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.rotateRight')); - menuitem.setAttribute('disabled', this._isReadOnly()); - menuitem.addEventListener('command', async () => { - this._postMessage({ action: 'reloading' }); - await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 90, true); - await this.reload({ rotatedPageIndexes: data.pageIndexes }); - }); - popup.appendChild(menuitem); - // Rotate 180 - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('pdfReader.rotate180')); - menuitem.setAttribute('disabled', this._isReadOnly()); - menuitem.addEventListener('command', async () => { - this._postMessage({ action: 'reloading' }); - await Zotero.PDFWorker.rotatePages(this._itemID, data.pageIndexes, 180, true); - await this.reload({ rotatedPageIndexes: data.pageIndexes }); - }); - popup.appendChild(menuitem); - // Separator - popup.appendChild(this._window.document.createXULElement('menuseparator')); - // Delete - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('general.delete')); - menuitem.setAttribute('disabled', this._isReadOnly()); - menuitem.addEventListener('command', async () => { - if (this.promptToDeletePages(data.pageIndexes.length)) { - this._postMessage({ action: 'reloading' }); - try { - await Zotero.PDFWorker.deletePages(this._itemID, data.pageIndexes, true); - } - catch (e) { - } - await this.reload(); - } - }); - popup.appendChild(menuitem); - popup.openPopupAtScreen(data.x, data.y, true); - } - - _openSelectorPopup(data) { - let popup = this._window.document.createXULElement('menupopup'); - this._popupset.appendChild(popup); - popup.addEventListener('popuphidden', function () { - popup.remove(); - }); - let menuitem; - // Clear Selection - menuitem = this._window.document.createXULElement('menuitem'); - menuitem.setAttribute('label', Zotero.getString('general.clearSelection')); - menuitem.setAttribute('disabled', !data.enableClearSelection); - menuitem.addEventListener('command', () => { - this._postMessage({ - action: 'popupCmd', - cmd: 'clearSelector', - ids: data.ids - }); - }); - popup.appendChild(menuitem); - popup.openPopupAtScreen(data.x, data.y, true); - } - - async _postMessage(message, transfer, secondView) { await this._waitForReader(); - this._iframeWindow.postMessage({ itemID: this._itemID, message, secondView }, this._iframeWindow.origin, transfer); - } - _updateSecondViewState() { - if (this.tabID) { - let win = Zotero.getMainWindow(); - if (win) { - win.Zotero_Tabs.setSecondViewState(this.tabID, this.getSecondViewState()); - } - } - } - _handleMessage = async (event) => { - let message; - let secondViewIframeWindow = this._iframeWindow.document.getElementById('secondViewIframe'); - if (secondViewIframeWindow) { - secondViewIframeWindow = secondViewIframeWindow.contentWindow; - } try { - if (event.source !== this._iframeWindow - && event.source !== secondViewIframeWindow) { - return; - } - // Clone data to avoid the dead object error when the window is closed - let data = JSON.parse(JSON.stringify(event.data)); - let { secondView } = data; - // Filter messages coming from previous reader instances, - // except for `setAnnotation` to still allow saving it - if (data.itemID !== this._itemID && data.message.action !== 'setAnnotation') { - return; - } - message = data.message; - - if (secondView) { - switch (message.action) { - case 'openPagePopup': break; - case 'setState': { - this._updateSecondViewState(); - return; - } - default: return; - } - } - - switch (message.action) { - case 'initialized': { - this._resolveInitPromise(); - return; - } - case 'saveAnnotations': { - let attachment = Zotero.Items.get(data.itemID); - let { annotations } = message; + this._internalReader = this._iframeWindow.wrappedJSObject.createReader(Components.utils.cloneInto({ + type: this._type, + buf: new Uint8Array(buf), + annotations, + primaryViewState: state, + secondaryViewState: secondViewState, + location, + readOnly: this._isReadOnly(), + authorName: this._item.library.libraryType === 'group' ? Zotero.Users.getCurrentName() : '', + showItemPaneToggle: this._showItemPaneToggle, + sidebarWidth: this._sidebarWidth, + sidebarOpen: this._sidebarOpen, + bottomPlaceholderHeight: this._bottomPlaceholderHeight, + rtl: Zotero.rtl, + fontSize: Zotero.Prefs.get('fontSize'), + localizedStrings: { + ...Zotero.Intl.getPrefixedStrings('general.'), + ...Zotero.Intl.getPrefixedStrings('pdfReader.') + }, + showAnnotations: true, + onOpenContextMenu: () => { + // Functions can only be passed over wrappedJSObject (we call back onClick for context menu items) + this._openContextMenu(this._iframeWindow.wrappedJSObject.contextMenuParams); + }, + onAddToNote: (annotations) => { + this._addToNote(annotations); + }, + onSaveAnnotations: async (annotations) => { + let attachment = Zotero.Items.get(this.itemID); let notifierQueue = new Zotero.Notifier.Queue(); try { for (let annotation of annotations) { @@ -1087,21 +224,25 @@ class ReaderInstance { } } } + catch (e) { + // Enter read-only mode if annotation saving fails + this._internalReader.setReadOnly(true); + throw e; + } finally { await Zotero.Notifier.commit(notifierQueue); } - return; - } - case 'deleteAnnotations': { - let { ids: keys } = message; - let attachment = Zotero.Items.get(this._itemID); + }, + onDeleteAnnotations: async (ids) => { + let keys = ids; + let attachment = this._item; let libraryID = attachment.libraryID; let notifierQueue = new Zotero.Notifier.Queue(); try { for (let key of keys) { let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key); // Make sure the annotation actually belongs to the current PDF - if (annotation && annotation.isAnnotation() && annotation.parentID === this._itemID) { + if (annotation && annotation.isAnnotation() && annotation.parentID === this._item.id) { this.annotationItemIDs = this.annotationItemIDs.filter(id => id !== annotation.id); await annotation.eraseTx({ notifierQueue }); } @@ -1110,57 +251,29 @@ class ReaderInstance { finally { await Zotero.Notifier.commit(notifierQueue); } - return; - } - // Save rendered image when annotation isn't modified - case 'saveImage': { - let { annotation } = message; - let { image, id: key } = annotation; - let attachment = Zotero.Items.get(this._itemID); - let libraryID = attachment.libraryID; - let item = Zotero.Items.getByLibraryAndKey(attachment.libraryID, key); - if (item) { - let blob = this._dataURLtoBlob(image); - await Zotero.Annotations.saveCacheImage({ libraryID, key }, blob); + }, + onChangeViewState: async (state, primary) => { + state = JSON.parse(JSON.stringify(state)); + if (primary) { + await this._setState(state); } - return; - } - case 'setState': { - let { state } = message; - await this._setState(state); - return; - } - case 'openTagsPopup': { - let { id: key, selector } = message; - let attachment = Zotero.Items.get(this._itemID); + else if (this.tabID) { + let win = Zotero.getMainWindow(); + if (win) { + win.Zotero_Tabs.setSecondViewState(this.tabID, state); + } + } + }, + onOpenTagsPopup: (id, x, y) => { + let key = id; + let attachment = Zotero.Items.get(this._item.id); let libraryID = attachment.libraryID; let annotation = Zotero.Items.getByLibraryAndKey(libraryID, key); if (annotation) { - this._openTagsPopup(annotation, selector); + this._openTagsPopup(annotation, x, y); } - return; - } - case 'openPagePopup': { - this._openPagePopup(message.data, secondView); - return; - } - case 'openAnnotationPopup': { - this._openAnnotationPopup(message.data); - return; - } - case 'openColorPopup': { - this._openColorPopup(message.data); - return; - } - case 'openThumbnailPopup': { - this._openThumbnailPopup(message.data); - return; - } - case 'openSelectorPopup': { - this._openSelectorPopup(message.data); - return; - } - case 'closePopup': { + }, + onClosePopup: () => { // Note: This currently only closes tags popup when annotations are // disappearing from pdf-reader sidebar for (let child of Array.from(this._popupset.children)) { @@ -1168,86 +281,535 @@ class ReaderInstance { child.hidePopup(); } } - return; - } - case 'openURL': { - let { url } = message; + }, + onOpenLink: (url) => { let win = Services.wm.getMostRecentWindow('navigator:browser'); if (win) { win.ZoteroPane.loadURI(url); } - return; - } - case 'addToNote': { - let { annotations } = message; - this._addToNote(annotations); - return; - } - case 'save': { - let zp = Zotero.getActiveZoteroPane(); - zp.exportPDF(this._itemID); - return; - } - case 'toggleNoteSidebar': { - let { isToggled } = message; - this._toggleNoteSidebar(isToggled); - return; - } - case 'changeSidebarWidth': { - let { width } = message; - if (this.onChangeSidebarWidth) { - this.onChangeSidebarWidth(width); + }, + onToggleSidebar: (open) => { + if (this._onToggleSidebar) { + this._onToggleSidebar(open); } - return; - } - case 'changeSidebarOpen': { - let { open } = message; - if (this.onChangeSidebarOpen) { - this.onChangeSidebarOpen(open); + }, + onChangeSidebarWidth: (width) => { + if (this._onChangeSidebarWidth) { + this._onChangeSidebarWidth(width); } - return; - } - case 'focusSplitButton': { + }, + onFocusSplitButton: () => { if (this instanceof ReaderTab) { let win = Zotero.getMainWindow(); if (win) { win.document.getElementById('zotero-tb-toggle-item-pane').focus(); } } - return; - } - case 'focusContextPane': { + }, + onFocusContextPane: () => { if (this instanceof ReaderWindow || !this._window.ZoteroContextPane.focus()) { this.focusFirst(); } - return; + }, + onSetDataTransferAnnotations: (dataTransfer, annotations, fromText) => { + // A little hack to force serializeAnnotations to include image annotation + // even if image isn't saved and imageAttachmentKey isn't available + for (let annotation of annotations) { + annotation.attachmentItemID = this._item.id; + } + dataTransfer.setData('zotero/annotation', JSON.stringify(annotations)); + // Don't set Markdown or HTML if copying or dragging text + if (fromText) { + return; + } + for (let annotation of annotations) { + if (annotation.image && !annotation.imageAttachmentKey) { + annotation.imageAttachmentKey = 'none'; + delete annotation.image; + } + } + let res = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations); + let tmpNote = new Zotero.Item('note'); + tmpNote.libraryID = Zotero.Libraries.userLibraryID; + tmpNote.setNote(res.html); + let items = [tmpNote]; + let format = Zotero.QuickCopy.getNoteFormat(); + Zotero.debug(`Copying/dragging (${annotations.length}) annotation(s) with ${format}`); + format = Zotero.QuickCopy.unserializeSetting(format); + // Basically the same code is used in itemTree.jsx onDragStart + try { + if (format.mode === 'export') { + // If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown + // and Note HTML translators instead + if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { + let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, options: format.markdownOptions }; + let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, options: format.htmlOptions }; + Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => { + if (!worked) { + return; + } + Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => { + if (!worked) { + return; + } + dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n')); + dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n')); + }); + }); + } + else { + Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => { + if (!worked) { + return; + } + var text = obj.string.replace(/\r\n/g, '\n'); + // For Note HTML translator use body content only + if (format.id === Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { + // Use body content only + let parser = new DOMParser(); + let doc = parser.parseFromString(text, 'text/html'); + text = doc.body.innerHTML; + } + dataTransfer.setData('text/plain', text); + }); + } + } + } + catch (e) { + Zotero.debug(e); + } + }, + onConfirm: function (title, text, confirmationButtonTitle) { + let ps = Services.prompt; + let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; + let index = ps.confirmEx(null, title, text, buttonFlags, + confirmationButtonTitle, null, null, null, {}); + return !index; + }, + onCopyImage: async (dataURL) => { + let parts = dataURL.split(','); + if (!parts[0].includes('base64')) { + return; + } + let mime = parts[0].match(/:(.*?);/)[1]; + let bstr = atob(parts[1]); + let n = bstr.length; + let u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + let imgTools = Components.classes["@mozilla.org/image/tools;1"] + .getService(Components.interfaces.imgITools); + let transferable = Components.classes['@mozilla.org/widget/transferable;1'] + .createInstance(Components.interfaces.nsITransferable); + let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1'] + .getService(Components.interfaces.nsIClipboard); + let img = imgTools.decodeImageFromArrayBuffer(u8arr.buffer, mime); + transferable.init(null); + let kNativeImageMime = 'application/x-moz-nativeimage'; + transferable.addDataFlavor(kNativeImageMime); + transferable.setTransferData(kNativeImageMime, img); + clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard); + }, + onSaveImageAs: async (dataURL) => { + let fp = new FilePicker(); + fp.init(this._iframeWindow, Zotero.getString('pdfReader.saveImageAs'), fp.modeSave); + fp.appendFilter("PNG", "*.png"); + fp.defaultString = Zotero.getString('fileTypes.image').toLowerCase() + '.png'; + let rv = await fp.show(); + if (rv === fp.returnOK || rv === fp.returnReplace) { + let outputPath = fp.file; + let parts = dataURL.split(','); + if (parts[0].includes('base64')) { + let bstr = atob(parts[1]); + let n = bstr.length; + let u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + await OS.File.writeAtomic(outputPath, u8arr); + } + } + }, + onRotatePages: async (pageIndexes, degrees) => { + this._internalReader.freeze(); + try { + await Zotero.PDFWorker.rotatePages(this._item.id, pageIndexes, degrees, true); + } + catch (e) { + } + await this.reload(); + this._internalReader.unfreeze(); + }, + onDeletePages: async (pageIndexes) => { + if (this._promptToDeletePages(pageIndexes.length)) { + this._internalReader.freeze(); + try { + await Zotero.PDFWorker.deletePages(this._item.id, pageIndexes, true); + } + catch (e) { + } + await this.reload(); + this._internalReader.unfreeze(); + } } + }, this._iframeWindow, { cloneFunctions: true })); + } + catch (e) { + Zotero.logError(e); + if (this._internalReader) { + let errorMessage = `${Zotero.getString('general.error')}: '${e.message}'`; + this._internalReader.displayError(errorMessage); + } + throw e; + } + + this._resolveInitPromise(); + + // Set title once again, because `ReaderWindow` isn't loaded the first time + this.updateTitle(); + + this._prefObserverIDs = [ + Zotero.Prefs.registerObserver('fontSize', this._handleFontSizeChange), + Zotero.Prefs.registerObserver('tabs.title.reader', this._handleTabTitlePrefChange) + ]; + + return true; + } + + uninit() { + if (this._prefObserverIDs) { + this._prefObserverIDs.forEach(id => Zotero.Prefs.unregisterObserver(id)); + } + } + + get itemID() { + return this._item.id; + } + + async updateTitle() { + let type = Zotero.Prefs.get('tabs.title.reader'); + let item = Zotero.Items.get(this._item.id); + let readerTitle = item.getDisplayTitle(); + let parentItem = item.parentItem; + // If type is "filename", then readerTitle already has it + if (parentItem && type !== 'filename') { + let attachment = await parentItem.getBestAttachment(); + if (attachment && attachment.id === this._item.id) { + let parts = []; + // Windows displays bidi control characters as placeholders in window titles, so strip them + // See https://github.com/mozilla-services/screenshots/issues/4863 + let unformatted = Zotero.isWin; + let creator = parentItem.getField('firstCreator', unformatted); + let year = parentItem.getField('year'); + let title = parentItem.getDisplayTitle(); + // If creator is missing fall back to titleCreatorYear + if (type === 'creatorYearTitle' && creator) { + parts = [creator, year, title]; + } + else if (type === 'title') { + parts = [title]; + } + // If type is titleCreatorYear, or is missing, or another type falls back + else { + parts = [title, creator, year]; + } + readerTitle = parts.filter(x => x).join(' - '); + } + } + this._title = readerTitle; + this._setTitleValue(readerTitle); + } + + async setAnnotations(items) { + let annotations = []; + for (let item of items) { + let annotation = await this._getAnnotation(item); + if (annotation) { + annotations.push(annotation); + } + } + if (annotations.length) { + this._internalReader.setAnnotations(Components.utils.cloneInto(annotations, this._iframeWindow)); + } + } + + unsetAnnotations(keys) { + this._internalReader.unsetAnnotations(Components.utils.cloneInto(keys, this._iframeWindow)); + } + + async navigate(location) { + this._internalReader.navigate(Components.utils.cloneInto(location, this._iframeWindow)); + } + + async enableAddToNote(enable) { + await this._initPromise; + this._internalReader.enableAddToNote(enable); + } + + focusLastToolbarButton() { + this._iframeWindow.focus(); + // this._postMessage({ action: 'focusLastToolbarButton' }); + } + + tabToolbar(reverse) { + // this._postMessage({ action: 'tabToolbar', reverse }); + // Avoid toolbar find button being focused for a short moment + setTimeout(() => this._iframeWindow.focus()); + } + + focusFirst() { + // this._postMessage({ action: 'focusFirst' }); + setTimeout(() => this._iframeWindow.focus()); + } + + async setBottomPlaceholderHeight(height) { + await this._initPromise; + this._internalReader.setBottomPlaceholderHeight(height); + } + + async setToolbarPlaceholderWidth(width) { + await this._initPromise; + this._internalReader.setToolbarPlaceholderWidth(width); + } + + promptToTransferAnnotations() { + let ps = Services.prompt; + let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; + let index = ps.confirmEx( + null, + Zotero.getString('pdfReader.promptTransferFromPDF.title'), + Zotero.getString('pdfReader.promptTransferFromPDF.text', Zotero.appName), + buttonFlags, + Zotero.getString('general.continue'), + null, null, null, {} + ); + return !index; + } + + _promptToDeletePages(num) { + let ps = Services.prompt; + let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; + let index = ps.confirmEx( + null, + Zotero.getString('pdfReader.promptDeletePages.title'), + Zotero.getString( + 'pdfReader.promptDeletePages.text', + new Intl.NumberFormat().format(num), + num + ), + buttonFlags, + Zotero.getString('general.continue'), + null, null, null, {} + ); + return !index; + } + + async reload() { + let item = Zotero.Items.get(this._item.id); + let path = await item.getFilePathAsync(); + let buf = await OS.File.read(path, {}); + buf = new Uint8Array(buf).buffer; + this._internalReader.reload(Components.utils.cloneInto(buf, this._iframeWindow)); + } + + async transferFromPDF() { + if (this.promptToTransferAnnotations(true)) { + try { + await Zotero.PDFWorker.import(this._item.id, true, '', true); + } + catch (e) { + if (e.name === 'PasswordException') { + Zotero.alert(null, Zotero.getString('general.error'), + Zotero.getString('pdfReader.promptPasswordProtected')); + } + throw e; + } + } + } + + export() { + let zp = Zotero.getActiveZoteroPane(); + zp.exportPDF(this._item.id); + } + + showInLibrary() { + let win = Zotero.getMainWindow(); + if (win) { + let item = Zotero.Items.get(this._item.id); + let id = item.parentID || item.id; + win.ZoteroPane.selectItems([id]); + win.Zotero_Tabs.select('zotero-pane'); + win.focus(); + } + } + + async _setState(state) { + let item = Zotero.Items.get(this._item.id); + if (item) { + item.setAttachmentLastPageIndex(state.pageIndex); + let file = Zotero.Attachments.getStorageDirectory(item); + if (!await OS.File.exists(file.path)) { + await Zotero.Attachments.createDirectoryForItem(item); + } + file.append(this.pdfStateFileName); + // Using `writeAtomic` instead of `putContentsAsync` to avoid + // using temp file that causes conflicts on simultaneous writes (on slow systems) + await OS.File.writeAtomic(file.path, JSON.stringify(state)); + } + } + + async _getState() { + let item = Zotero.Items.get(this._item.id); + let file = Zotero.Attachments.getStorageDirectory(item); + file.append(this.pdfStateFileName); + file = file.path; + let state; + try { + if (await OS.File.exists(file)) { + state = JSON.parse(await Zotero.File.getContentsAsync(file)); } } catch (e) { Zotero.logError(e); - let crash = message && ['setAnnotation'].includes(message.action); - this._postMessage({ - action: crash ? 'crash' : 'error', - message: `${Zotero.getString('general.error')}: '${message ? message.action : ''}'`, - moreInfo: { - message: e.message, - stack: e.stack, - fileName: e.fileName, - lineNumber: e.lineNumber - } - }); - throw e; + } + + let pageIndex = item.getAttachmentLastPageIndex(); + if (state) { + if (Number.isInteger(pageIndex) && state.pageIndex !== pageIndex) { + state.pageIndex = pageIndex; + delete state.top; + delete state.left; + } + return state; + } + else if (Number.isInteger(pageIndex)) { + return { pageIndex }; + } + return null; + } + + _isReadOnly() { + let item = Zotero.Items.get(this._item.id); + return !item.isEditable() + || item.deleted + || item.parentItem && item.parentItem.deleted; + } + + _handleFontSizeChange = () => { + this._internalReader.setFontSize(Zotero.Prefs.get('fontSize')); + }; + + _dataURLtoBlob(dataurl) { + let parts = dataurl.split(','); + let mime = parts[0].match(/:(.*?);/)[1]; + if (parts[0].indexOf('base64') !== -1) { + let bstr = atob(parts[1]); + let n = bstr.length; + let u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new this._iframeWindow.Blob([u8arr], { type: mime }); } } + _getColorIcon(color, selected) { + let stroke = selected ? 'lightgray' : 'transparent'; + let fill = '%23' + color.slice(1); + return `data:image/svg+xml,`; + } + + _openTagsPopup(item, x, y) { + let menupopup = this._window.document.createXULElement('menupopup'); + menupopup.addEventListener('popuphidden', function (event) { + if (event.target === menupopup) { + menupopup.remove(); + } + }); + menupopup.className = 'tags-popup'; + menupopup.style.font = 'inherit'; + menupopup.style.minWidth = '300px'; + menupopup.setAttribute('ignorekeys', true); + let tagsbox = new (this._window.customElements.get('tags-box')); + menupopup.appendChild(tagsbox); + tagsbox.setAttribute('flex', '1'); + this._popupset.appendChild(menupopup); + let rect = this._iframe.getBoundingClientRect(); + x += rect.left; + y += rect.top; + setTimeout(() => menupopup.openPopup(null, 'before_start', x, y, true)); + tagsbox.mode = 'edit'; + tagsbox.item = item; + if (tagsbox.mode == 'edit' && tagsbox.count == 0) { + tagsbox.newTag(); + } + } + + async _openContextMenu({ x, y, itemGroups }) { + let popup = this._window.document.createXULElement('menupopup'); + this._popupset.appendChild(popup); + popup.addEventListener('popuphidden', function () { + popup.remove(); + }); + let appendItems = (parentNode, itemGroups) => { + for (let itemGroup of itemGroups) { + for (let item of itemGroup) { + if (item.groups) { + let menu = parentNode.ownerDocument.createXULElement('menu'); + menu.setAttribute('label', item.label); + let menupopup = parentNode.ownerDocument.createXULElement('menupopup'); + menu.append(menupopup); + appendItems(menupopup, item.groups); + parentNode.appendChild(menu); + } + else { + let menuitem = parentNode.ownerDocument.createXULElement('menuitem'); + menuitem.setAttribute('label', item.label); + menuitem.setAttribute('disabled', item.disabled); + if (item.checked) { + menuitem.setAttribute('type', 'checkbox'); + menuitem.setAttribute('checked', item.checked); + } + if (item.color) { + menuitem.className = 'menuitem-iconic'; + menuitem.setAttribute('image', this._getColorIcon(item.color)); + } + menuitem.addEventListener('command', () => item.onCommand()); + parentNode.appendChild(menuitem); + } + } + if (itemGroups.indexOf(itemGroup) !== itemGroups.length - 1) { + let separator = parentNode.ownerDocument.createXULElement('menuseparator'); + parentNode.appendChild(separator); + } + } + }; + appendItems(popup, itemGroups); + let rect = this._iframe.getBoundingClientRect(); + x += rect.left; + y += rect.top; + setTimeout(() => popup.openPopup(null, 'before_start', x, y, true)); + } + + _updateSecondViewState() { + if (this.tabID) { + let win = Zotero.getMainWindow(); + if (win) { + win.Zotero_Tabs.setSecondViewState(this.tabID, this.getSecondViewState()); + } + } + } async _waitForReader() { if (this._isReaderInitialized) { return; } let n = 0; - while (!this._iframeWindow || !this._iframeWindow.wrappedJSObject.isReady) { + while (!this._iframeWindow) { if (n >= 500) { throw new Error('Waiting for reader failed'); } @@ -1285,24 +847,25 @@ class ReaderInstance { } class ReaderTab extends ReaderInstance { - constructor({ itemID, title, sidebarWidth, sidebarOpen, bottomPlaceholderHeight, index, tabID, background, preventJumpback }) { - super(); - this._itemID = itemID; - this._sidebarWidth = sidebarWidth; - this._sidebarOpen = sidebarOpen; - this._bottomPlaceholderHeight = bottomPlaceholderHeight; + constructor(options) { + super(options); + this._sidebarWidth = options.sidebarWidth; + this._sidebarOpen = options.sidebarOpen; + this._bottomPlaceholderHeight = options.bottomPlaceholderHeight; this._showItemPaneToggle = true; + this._onToggleSidebar = options.onToggleSidebar; + this._onChangeSidebarWidth = options.onChangeSidebarWidth; this._window = Services.wm.getMostRecentWindow('navigator:browser'); let { id, container } = this._window.Zotero_Tabs.add({ - id: tabID, + id: options.tabID, type: 'reader', - title: title || '', - index, + title: options.title || '', + index: options.index, data: { - itemID + itemID: this._item.id }, - select: !background, - preventJumpback: preventJumpback + select: !options.background, + preventJumpback: options.preventJumpback }); this.tabID = id; this._tabContainer = container; @@ -1311,7 +874,7 @@ class ReaderTab extends ReaderInstance { this._iframe.setAttribute('class', 'reader'); this._iframe.setAttribute('flex', '1'); this._iframe.setAttribute('type', 'content'); - this._iframe.setAttribute('src', 'resource://zotero/pdf-reader/viewer.html'); + this._iframe.setAttribute('src', 'resource://zotero/pdf-reader/reader.html'); this._tabContainer.appendChild(this._iframe); this._iframe.docShell.windowDraggingAllowed = true; @@ -1321,7 +884,7 @@ class ReaderTab extends ReaderInstance { this._window.addEventListener('DOMContentLoaded', (event) => { if (this._iframe && this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) { this._iframeWindow = this._iframe.contentWindow; - this._initIframeWindow(); + this._iframeWindow.addEventListener('error', event => Zotero.logError(event.error)); } }); @@ -1351,6 +914,8 @@ class ReaderTab extends ReaderInstance { catch(e) { } }); + + this._open({ location: options.location, secondViewState: options.secondViewState }); } close() { @@ -1358,17 +923,7 @@ class ReaderTab extends ReaderInstance { this._window.Zotero_Tabs.close(this.tabID); } } - - _toggleNoteSidebar(isToggled) { - let itemPane = this._window.document.getElementById('zotero-item-pane'); - if (itemPane.hidden) { - itemPane.hidden = false; - } - else { - itemPane.hidden = true; - } - } - + _setTitleValue(title) { this._window.Zotero_Tabs.rename(this.tabID, title); } @@ -1388,15 +943,12 @@ class ReaderTab extends ReaderInstance { class ReaderWindow extends ReaderInstance { - constructor({ sidebarWidth, sidebarOpen, bottomPlaceholderHeight }) { - super(); - this._sidebarWidth = sidebarWidth; - this._sidebarOpen = sidebarOpen; + constructor(options) { + super(options); + this._sidebarWidth = options.sidebarWidth; + this._sidebarOpen = options.sidebarOpen; this._bottomPlaceholderHeight = 0; - this.init(); - } - init() { let win = Services.wm.getMostRecentWindow('navigator:browser'); if (!win) return; @@ -1408,20 +960,32 @@ class ReaderWindow extends ReaderInstance { if (event.target === this._window.document) { this._window.addEventListener('keypress', this._handleKeyPress); this._popupset = this._window.document.getElementById('zotero-reader-popupset'); - this._window.menuCmd = this.menuCmd.bind(this); this._window.onGoMenuOpen = this._onGoMenuOpen.bind(this); this._window.onViewMenuOpen = this._onViewMenuOpen.bind(this); + this._window.reader = this; this._iframe = this._window.document.getElementById('reader'); this._iframe.docShell.windowDraggingAllowed = true; } if (this._iframe.contentWindow && this._iframe.contentWindow.document === event.target) { this._iframeWindow = this._window.document.getElementById('reader').contentWindow; - this._initIframeWindow(); + this._iframeWindow.addEventListener('error', event => Zotero.logError(event.error)); } + + this._switchReaderSubtype(this._type); }); + + this._open({ state: options.state, location: options.location, secondViewState: options.secondViewState }); } + _switchReaderSubtype(subtype) { + // Do the same as in standalone.js + this._window.document.querySelectorAll( + '.menu-type-reader.pdf, .menu-type-reader.epub, .menu-type-reader.snapshot' + ).forEach(el => el.hidden = true); + this._window.document.querySelectorAll('.menu-type-reader.' + subtype).forEach(el => el.hidden = false); + }; + close() { this._window.close(); } @@ -1438,18 +1002,20 @@ class ReaderWindow extends ReaderInstance { } _onViewMenuOpen() { - this._window.document.getElementById('view-menuitem-vertical-scrolling').setAttribute('checked', this.state.scrollMode == 0); - this._window.document.getElementById('view-menuitem-horizontal-scrolling').setAttribute('checked', this.state.scrollMode == 1); - this._window.document.getElementById('view-menuitem-wrapped-scrolling').setAttribute('checked', this.state.scrollMode == 2); - this._window.document.getElementById('view-menuitem-no-spreads').setAttribute('checked', this.state.spreadMode == 0); - this._window.document.getElementById('view-menuitem-odd-spreads').setAttribute('checked', this.state.spreadMode == 1); - this._window.document.getElementById('view-menuitem-even-spreads').setAttribute('checked', this.state.spreadMode == 2); - this._window.document.getElementById('view-menuitem-hand-tool').setAttribute('checked', this.isHandToolActive()); - this._window.document.getElementById('view-menuitem-zoom-auto').setAttribute('checked', this.isZoomAutoActive()); - this._window.document.getElementById('view-menuitem-zoom-page-width').setAttribute('checked', this.isZoomPageWidthActive()); - this._window.document.getElementById('view-menuitem-zoom-page-height').setAttribute('checked', this.isZoomPageHeightActive()); - this._window.document.getElementById('view-menuitem-split-vertically').setAttribute('checked', this.isSplitVerticallyActive()); - this._window.document.getElementById('view-menuitem-split-horizontally').setAttribute('checked', this.isSplitHorizontallyActive()); + if (this._type === 'pdf') { + this._window.document.getElementById('view-menuitem-vertical-scrolling').setAttribute('checked', this._internalReader.scrollMode === 0); + this._window.document.getElementById('view-menuitem-horizontal-scrolling').setAttribute('checked', this._internalReader.scrollMode === 1); + this._window.document.getElementById('view-menuitem-wrapped-scrolling').setAttribute('checked', this._internalReader.scrollMode === 2); + this._window.document.getElementById('view-menuitem-no-spreads').setAttribute('checked', this._internalReader.spreadMode === 0); + this._window.document.getElementById('view-menuitem-odd-spreads').setAttribute('checked', this._internalReader.spreadMode === 1); + this._window.document.getElementById('view-menuitem-even-spreads').setAttribute('checked', this._internalReader.spreadMode === 2); + this._window.document.getElementById('view-menuitem-hand-tool').setAttribute('checked', this._internalReader.toolType === 'hand'); + this._window.document.getElementById('view-menuitem-zoom-auto').setAttribute('checked', this._internalReader.zoomAutoEnabled); + this._window.document.getElementById('view-menuitem-zoom-page-width').setAttribute('checked', this._internalReader.zoomPageWidthEnabled); + this._window.document.getElementById('view-menuitem-zoom-page-height').setAttribute('checked', this._internalReader.zoomPageHeightEnabled); + } + this._window.document.getElementById('view-menuitem-split-vertically').setAttribute('checked', this._internalReader.splitType === 'vertical'); + this._window.document.getElementById('view-menuitem-split-horizontally').setAttribute('checked', this._internalReader.splitType === 'horizontal'); } _onGoMenuOpen() { @@ -1474,10 +1040,12 @@ class ReaderWindow extends ReaderInstance { menuItemBack.setAttribute('key', 'key_back'); menuItemForward.setAttribute('key', 'key_forward'); - this._window.document.getElementById('go-menuitem-first-page').setAttribute('disabled', !this.allowNavigateFirstPage()); - this._window.document.getElementById('go-menuitem-last-page').setAttribute('disabled', !this.allowNavigateLastPage()); - this._window.document.getElementById('go-menuitem-back').setAttribute('disabled', !this.allowNavigateBack()); - this._window.document.getElementById('go-menuitem-forward').setAttribute('disabled', !this.allowNavigateForward()); + if (['pdf', 'epub'].includes(this._type)) { + this._window.document.getElementById('go-menuitem-first-page').setAttribute('disabled', !this._internalReader.canNavigateToFirstPage); + this._window.document.getElementById('go-menuitem-last-page').setAttribute('disabled', !this._internalReader.canNavigateToLastPage); + } + this._window.document.getElementById('go-menuitem-back').setAttribute('disabled', !this._internalReader.canNavigateBack); + this._window.document.getElementById('go-menuitem-forward').setAttribute('disabled', !this._internalReader.canNavigateForward); } } @@ -1490,8 +1058,8 @@ class Reader { this._readers = []; this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'reader'); this.onChangeSidebarWidth = null; - this.onChangeSidebarOpen = null; - + this.onToggleSidebar = null; + this._debounceSidebarWidthUpdate = Zotero.Utilities.debounce(() => { let readers = this._readers.filter(r => r instanceof ReaderTab); for (let reader of readers) { @@ -1546,11 +1114,11 @@ class Reader { this._setSidebarState(); } - setSidebarOpen(open) { + toggleSidebar(open) { this._sidebarOpen = open; let readers = this._readers.filter(r => r instanceof ReaderTab); for (let reader of readers) { - reader.setSidebarOpen(open); + reader.toggleSidebar(open); } this._setSidebarState(); } @@ -1577,7 +1145,7 @@ class Reader { else if (event === 'select') { let reader = Zotero.Reader.getByTabID(ids[0]); if (reader) { - this.triggerAnnotationsImportCheck(reader._itemID); + this.triggerAnnotationsImportCheck(reader.itemID); } } @@ -1588,12 +1156,12 @@ class Reader { // Listen for parent item, PDF attachment and its annotations updates else if (type === 'item') { for (let reader of this._readers.slice()) { - if (event === 'delete' && ids.includes(reader._itemID)) { + if (event === 'delete' && ids.includes(reader.itemID)) { reader.close(); } // Ignore other notifications if the attachment no longer exists - let item = Zotero.Items.get(reader._itemID); + let item = Zotero.Items.get(reader.itemID); if (item) { if (event === 'trash' && (ids.includes(item.id) || ids.includes(item.parentItemID))) { reader.close(); @@ -1616,7 +1184,7 @@ class Reader { reader.setAnnotations(affectedAnnotations); } // Update title if the PDF attachment or the parent item changes - if (ids.includes(reader._itemID) || ids.includes(item.parentItemID)) { + if (ids.includes(reader.itemID) || ids.includes(item.parentItemID)) { reader.updateTitle(); } } @@ -1634,7 +1202,7 @@ class Reader { .filter(r => r instanceof ReaderWindow) .map(r => ({ type: 'reader', - itemID: r._itemID, + itemID: r.itemID, title: r._title, secondViewState: r.getSecondViewState() })); @@ -1647,13 +1215,22 @@ class Reader { } async open(itemID, location, { title, tabIndex, tabID, openInBackground, openInWindow, allowDuplicate, secondViewState, preventJumpback } = {}) { + let { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); + let library = Zotero.Libraries.get(libraryID); + await library.waitForDataLoad('item'); + + let item = Zotero.Items.get(itemID); + if (!item) { + throw new Error('Item does not exist'); + } + this._loadSidebarState(); this.triggerAnnotationsImportCheck(itemID); let reader; // If duplicating is not allowed, and no reader instance is loaded for itemID, // try to find an unloaded tab and select it. Zotero.Reader.open will then be called again - if (!allowDuplicate && !this._readers.find(r => r._itemID === itemID)) { + if (!allowDuplicate && !this._readers.find(r => r.itemID === itemID)) { let win = Zotero.getMainWindow(); if (win) { let existingTabID = win.Zotero_Tabs.getTabIDByItemID(itemID); @@ -1665,10 +1242,10 @@ class Reader { } if (openInWindow) { - reader = this._readers.find(r => r._itemID === itemID && (r instanceof ReaderWindow)); + reader = this._readers.find(r => r.itemID === itemID && (r instanceof ReaderWindow)); } else if (!allowDuplicate) { - reader = this._readers.find(r => r._itemID === itemID); + reader = this._readers.find(r => r.itemID === itemID); } if (reader) { @@ -1682,23 +1259,25 @@ class Reader { } else if (openInWindow) { reader = new ReaderWindow({ + item, + location, + secondViewState, sidebarWidth: this._sidebarWidth, sidebarOpen: this._sidebarOpen, bottomPlaceholderHeight: this._bottomPlaceholderHeight }); this._readers.push(reader); - if (!(await reader.open({ itemID, location, secondViewState }))) { - return; - } Zotero.Session.debounceSave(); - reader._window.addEventListener('unload', () => { + reader._window.addEventListener('close', () => { this._readers.splice(this._readers.indexOf(reader), 1); Zotero.Session.debounceSave(); }); } else { reader = new ReaderTab({ - itemID, + item, + location, + secondViewState, title, index: tabIndex, tabID, @@ -1706,26 +1285,23 @@ class Reader { sidebarWidth: this._sidebarWidth, sidebarOpen: this._sidebarOpen, bottomPlaceholderHeight: this._bottomPlaceholderHeight, - preventJumpback: preventJumpback + preventJumpback: preventJumpback, + onToggleSidebar: (open) => { + this._sidebarOpen = open; + this.toggleSidebar(open); + if (this.onToggleSidebar) { + this.onToggleSidebar(open); + } + }, + onChangeSidebarWidth: (width) => { + this._sidebarWidth = width; + this._debounceSidebarWidthUpdate(); + if (this.onChangeSidebarWidth) { + this.onChangeSidebarWidth(width); + } + } }); this._readers.push(reader); - if (!(await reader.open({ itemID, location, secondViewState }))) { - return; - } - reader.onChangeSidebarWidth = (width) => { - this._sidebarWidth = width; - this._debounceSidebarWidthUpdate(); - if (this.onChangeSidebarWidth) { - this.onChangeSidebarWidth(width); - } - }; - reader.onChangeSidebarOpen = (open) => { - this._sidebarOpen = open; - this.setSidebarOpen(open); - if (this.onChangeSidebarOpen) { - this.onChangeSidebarOpen(open); - } - }; } if (!openInBackground) { @@ -1742,7 +1318,8 @@ class Reader { */ async triggerAnnotationsImportCheck(itemID) { let item = await Zotero.Items.getAsync(itemID); - if (!item.isEditable() + if (!item.isPDFAttachment() + || !item.isEditable() || item.deleted || item.parentItem && item.parentItem.deleted ) { diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index bae390c826..111710abaa 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -4718,7 +4718,7 @@ var ZoteroPane = new function() await item.saveTx(); } } - if (contentType === 'application/pdf') { + if (['application/pdf', 'application/epub+zip', 'text/html'].includes(contentType)) { let item = await Zotero.Items.getAsync(itemID); let library = Zotero.Libraries.get(item.libraryID); let pdfHandler = Zotero.Prefs.get("fileHandler.pdf"); diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml index 943f5f3a49..701135959d 100644 --- a/chrome/content/zotero/zoteroPane.xhtml +++ b/chrome/content/zotero/zoteroPane.xhtml @@ -186,17 +186,17 @@ command="cmd_zotero_newCollection"/> - - - - + oncommand="ZoteroStandalone.currentReader.export()"/> + + + @@ -271,25 +271,19 @@ - + - - + - + @@ -502,28 +496,28 @@ - + diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index f029446796..73fdc6170d 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -332,4 +332,3 @@ - diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index d6b5607167..597d03cf84 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -1417,7 +1417,6 @@ pdfReader.promptDeletePages.title = Delete Pages pdfReader.promptDeletePages.text = Are you sure you want to delete %1$S page from the PDF file?;Are you sure you want to delete %1$S pages from the PDF file? pdfReader.rotateLeft = Rotate Left pdfReader.rotateRight = Rotate Right -pdfReader.rotate180 = Rotate 180° pdfReader.editPageNumber = Edit Page Number… pdfReader.editHighlightedText = Edit Highlighted Text pdfReader.copyImage = Copy Image diff --git a/pdf-reader b/pdf-reader index c963b7e3f9..d5afdb0b6b 160000 --- a/pdf-reader +++ b/pdf-reader @@ -1 +1 @@ -Subproject commit c963b7e3f9bb8aa4bfeb39dfc0fbce3a4a33985e +Subproject commit d5afdb0b6bf4177be9f8d42d952f46beebf47e15