www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 0f4e5ef508fd3ee58bfe5f429ad3f491cbf73196
parent f7e411d56149b2aacf9f1f93351c523bac16613c
Author: Dan Stillman <dstillman@zotero.org>
Date:   Sat,  2 Jun 2018 04:10:49 -0400

Mendeley import

Accept Mendeley SQLite databases via File → Import… and perform a
direct import, including collections, timestamps, notes, attachments,
and extracted annotations.

When a Mendeley database is present, File → Import… shows a wizard that
lets you choose between a file and Mendeley for the source, and choosing
the latter shows a list of available databases in the Mendeley data
directory.

Known fields that aren't valid for a type are stored in Extra.

Files in the Mendeley 'Downloaded' folder are stored. Files elsewhere
are linked.

Diffstat:
Mchrome/content/zotero/fileInterface.js | 301++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Achrome/content/zotero/import/importWizard.js | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/import/importWizard.xul | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/import/mendeley/mendeleyImport.js | 830+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/import/mendeley/mendeleySchemaMap.js | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/data/collection.js | 2+-
Mchrome/content/zotero/xpcom/data/dataObject.js | 4++--
Mchrome/content/zotero/xpcom/data/relations.js | 3++-
Mchrome/content/zotero/xpcom/mime.js | 5+++--
Mchrome/content/zotero/zoteroPane.xul | 2+-
Mchrome/locale/en-US/zotero/zotero.dtd | 8++++++++
Mchrome/locale/en-US/zotero/zotero.properties | 4++++
Achrome/skin/default/zotero/importWizard.css | 44++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 1512 insertions(+), 57 deletions(-)

diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js @@ -23,6 +23,8 @@ ***** END LICENSE BLOCK ***** */ +Components.utils.import("resource://gre/modules/osfile.jsm") + /****Zotero_File_Exporter**** ** * A class to handle exporting of items, collections, or the entire library @@ -206,10 +208,117 @@ var Zotero_File_Interface = new function() { } } + + this.startImport = async function () { + // Show the wizard if a Mendeley database is found + var mendeleyDBs = await this.findMendeleyDatabases(); + var showWizard = !!mendeleyDBs.length; + if (showWizard) { + this.showImportWizard(); + } + // Otherwise just show the filepicker + else { + await this.importFile(null, true); + } + } + + + this.getMendeleyDirectory = function () { + Components.classes["@mozilla.org/net/osfileconstantsservice;1"] + .getService(Components.interfaces.nsIOSFileConstantsService) + .init(); + var path = OS.Constants.Path.homeDir; + if (Zotero.isMac) { + path = OS.Path.join(path, 'Library', 'Application Support', 'Mendeley Desktop'); + } + else if (Zotero.isWin) { + path = OS.Path.join(path, 'AppData', 'Local', 'Mendeley Ltd', 'Desktop'); + } + else if (Zotero.isLinux) { + path = OS.Path.join(path, '.local', 'share', 'data', 'Mendeley Ltd.', 'Mendeley Desktop'); + } + else { + throw new Error("Invalid platform"); + } + return path; + }; + + + this.findMendeleyDatabases = async function () { + var dbs = []; + try { + var dir = this.getMendeleyDirectory(); + if (!await OS.File.exists(dir)) { + Zotero.debug(`${dir} does not exist`); + return dbs; + } + await Zotero.File.iterateDirectory(dir, function* (iterator) { + while (true) { + let entry = yield iterator.next(); + if (entry.isDir) continue; + // online.sqlite, counterintuitively, is the default database before you sign in + if (entry.name == 'online.sqlite' || entry.name.endsWith('@www.mendeley.com.sqlite')) { + dbs.push({ + name: entry.name, + path: entry.path, + lastModified: null, + size: null + }); + } + } + }); + for (let i = 0; i < dbs.length; i++) { + let dbPath = OS.Path.join(dir, dbs[i].name); + let info = await OS.File.stat(dbPath); + dbs[i].size = info.size; + dbs[i].lastModified = info.lastModificationDate; + } + dbs.sort((a, b) => { + return b.lastModified - a.lastModified; + }); + } + catch (e) { + Zotero.logError(e); + } + return dbs; + }; + + + this.showImportWizard = function () { + try { + let win = Services.ww.openWindow(null, "chrome://zotero/content/import/importWizard.xul", + "importFile", "chrome,dialog=yes,centerscreen,width=600,height=400", null); + } + catch (e) { + Zotero.debug(e, 1); + throw e; + } + }; + + /** * Creates Zotero.Translate instance and shows file picker for file import + * + * @param {Object} options + * @param {nsIFile|string|null} [options.file=null] - File to import, or none to show a filepicker + * @param {Boolean} [options.createNewCollection=false] - Put items in a new collection + * @param {Function} [options.onBeforeImport] - Callback to receive translation object, useful + * for displaying progress in a different way. This also causes an error to be throw + * instead of shown in the main window. */ - this.importFile = Zotero.Promise.coroutine(function* (file, createNewCollection) { + this.importFile = Zotero.Promise.coroutine(function* (options = {}) { + if (!options) { + options = {}; + } + if (typeof options == 'string' || options instanceof Components.interfaces.nsIFile) { + Zotero.debug("WARNING: importFile() now takes a single options object -- update your code"); + options = { file: options }; + } + + var file = options.file ? Zotero.File.pathToFile(options.file) : null; + var createNewCollection = options.createNewCollection; + var onBeforeImport = options.onBeforeImport; + if(createNewCollection === undefined) { createNewCollection = true; } else if(!createNewCollection) { @@ -231,21 +340,51 @@ var Zotero_File_Interface = new function() { fp.appendFilters(nsIFilePicker.filterAll); var collation = Zotero.getLocaleCollation(); - translators.sort((a, b) => collation.compareString(1, a.label, b.label)) - for (let translator of translators) { - fp.appendFilter(translator.label, "*." + translator.target); + + // Add Mendeley DB, which isn't a translator + let mendeleyFilter = { + label: "Mendeley Database", // TODO: Localize + target: "*.sqlite" + }; + let filters = [...translators]; + filters.push(mendeleyFilter); + + filters.sort((a, b) => collation.compareString(1, a.label, b.label)); + for (let filter of filters) { + fp.appendFilter(filter.label, "*." + filter.target); } var rv = fp.show(); + Zotero.debug(rv); if (rv !== nsIFilePicker.returnOK && rv !== nsIFilePicker.returnReplace) { return false; } file = fp.file; + + Zotero.debug(`File is ${file.path}`); } - + + var defaultNewCollectionPrefix = Zotero.getString("fileInterface.imported"); + + // Check if the file is an SQLite database + var sample = yield Zotero.File.getSample(file.path); + if (Zotero.MIME.sniffForMIMEType(sample) == 'application/x-sqlite3' + // Blacklist the current Zotero database, which would cause a hang + && file.path != Zotero.DataDirectory.getDatabase()) { + // Mendeley import doesn't use the real translation architecture, but we create a + // translation object with the same interface + translation = yield _getMendeleyTranslation(); + defaultNewCollectionPrefix = "Mendeley Import"; + } + translation.setLocation(file); - yield _finishImport(translation, createNewCollection); + return _finishImport({ + translation, + createNewCollection, + defaultNewCollectionPrefix, + onBeforeImport + }); }); @@ -287,17 +426,31 @@ var Zotero_File_Interface = new function() { }); - var _finishImport = Zotero.Promise.coroutine(function* (translation, createNewCollection) { + var _finishImport = Zotero.Promise.coroutine(function* (options) { + var t = performance.now(); + + var translation = options.translation; + var createNewCollection = options.createNewCollection; + var defaultNewCollectionPrefix = options.defaultNewCollectionPrefix; + var onBeforeImport = options.onBeforeImport; + + var showProgressWindow = !onBeforeImport; + let translators = yield translation.getTranslators(); - - if(!translators.length) { - var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK) - + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING); - var index = ps.confirmEx( + + // Unrecognized file + if (!translators.length) { + if (onBeforeImport) { + yield onBeforeImport(false); + } + + let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; + let index = ps.confirmEx( null, - "", + Zotero.getString('general.error'), Zotero.getString("fileInterface.unsupportedFormat"), buttonFlags, null, @@ -305,17 +458,17 @@ var Zotero_File_Interface = new function() { null, null, {} ); if (index == 1) { - ZoteroPane_Local.loadURI("http://zotero.org/support/kb/importing"); + Zotero.launchURL("https://www.zotero.org/support/kb/importing"); } - return; + return false; } - + let importCollection = null, libraryID = Zotero.Libraries.userLibraryID; try { libraryID = ZoteroPane.getSelectedLibraryID(); importCollection = ZoteroPane.getSelectedCollection(); } catch(e) {} - + if(createNewCollection) { // Create a new collection to take imported items let collectionName; @@ -330,8 +483,9 @@ var Zotero_File_Interface = new function() { break; } } - } else { - collectionName = Zotero.getString("fileInterface.imported")+" "+(new Date()).toLocaleString(); + } + else { + collectionName = defaultNewCollectionPrefix + " " + (new Date()).toLocaleString(); } importCollection = new Zotero.Collection; importCollection.libraryID = libraryID; @@ -342,22 +496,29 @@ var Zotero_File_Interface = new function() { translation.setTranslator(translators[0]); // Show progress popup - var progressWin = new Zotero.ProgressWindow({ - closeOnClick: false - }); - progressWin.changeHeadline(Zotero.getString('fileInterface.importing')); - var icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png'; - let progress = new progressWin.ItemProgress( - icon, translation.path ? OS.Path.basename(translation.path) : translators[0].label - ); - progressWin.show(); + var progressWin; + var progress; + if (showProgressWindow) { + progressWin = new Zotero.ProgressWindow({ + closeOnClick: false + }); + progressWin.changeHeadline(Zotero.getString('fileInterface.importing')); + let icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png'; + progress = new progressWin.ItemProgress( + icon, translation.path ? OS.Path.basename(translation.path) : translators[0].label + ); + progressWin.show(); + + translation.setHandler("itemDone", function () { + progress.setProgress(translation.getProgress()); + }); + + yield Zotero.Promise.delay(0); + } + else { + yield onBeforeImport(translation); + } - translation.setHandler("itemDone", function () { - progress.setProgress(translation.getProgress()); - }); - - yield Zotero.Promise.delay(0); - let failed = false; try { yield translation.translate({ @@ -365,6 +526,10 @@ var Zotero_File_Interface = new function() { collections: importCollection ? [importCollection.id] : null }); } catch(e) { + if (!showProgressWindow) { + throw e; + } + progressWin.close(); Zotero.logError(e); Zotero.alert( @@ -372,26 +537,62 @@ var Zotero_File_Interface = new function() { Zotero.getString('general.error'), Zotero.getString("fileInterface.importError") ); - return; + return false; } - // Show popup on completion var numItems = translation.newItems.length; - progressWin.changeHeadline(Zotero.getString('fileInterface.importComplete')); - if (numItems == 1) { - var icon = translation.newItems[0].getImageSrc(); + + // Show popup on completion + if (showProgressWindow) { + progressWin.changeHeadline(Zotero.getString('fileInterface.importComplete')); + let icon; + if (numItems == 1) { + icon = translation.newItems[0].getImageSrc(); + } + else { + icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png'; + } + let text = Zotero.getString(`fileInterface.itemsWereImported`, numItems, numItems); + progress.setIcon(icon); + progress.setText(text); + // For synchronous translators, which don't update progress + progress.setProgress(100); + progressWin.startCloseTimer(5000); } - else { - var icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png'; - } - var text = Zotero.getString(`fileInterface.itemsWereImported`, numItems, numItems); - progress.setIcon(icon); - progress.setText(text); - // For synchronous translators, which don't update progress - progress.setProgress(100); - progressWin.startCloseTimer(5000); + + Zotero.debug(`Imported ${numItems} item(s) in ${performance.now() - t} ms`); + + return true; }); + + var _getMendeleyTranslation = async function () { + if (true) { + Components.utils.import("chrome://zotero/content/import/mendeley/mendeleyImport.js"); + } + // TEMP: Load uncached from ~/zotero-client for development + else { + Components.utils.import("resource://gre/modules/FileUtils.jsm"); + let file = FileUtils.getDir("Home", []); + file = OS.Path.join( + file.path, + 'zotero-client', 'chrome', 'content', 'zotero', 'import', 'mendeley', 'mendeleyImport.js' + ); + let fileURI = OS.Path.toFileURI(file); + let xmlhttp = await Zotero.HTTP.request( + 'GET', + fileURI, + { + dontCache: true, + responseType: 'text' + } + ); + eval(xmlhttp.response); + } + return new Zotero_Import_Mendeley(); + } + + /** * Creates a bibliography from a collection or saved search */ diff --git a/chrome/content/zotero/import/importWizard.js b/chrome/content/zotero/import/importWizard.js @@ -0,0 +1,202 @@ +var Zotero_Import_Wizard = { + _wizard: null, + _dbs: null, + _file: null, + _translation: null, + + + init: function () { + this._wizard = document.getElementById('import-wizard'); + + Zotero.Translators.init(); // async + }, + + + onModeChosen: async function () { + var wizard = this._wizard; + + this._disableCancel(); + wizard.canRewind = false; + wizard.canAdvance = false; + + var mode = document.getElementById('import-source').selectedItem.id; + try { + switch (mode) { + case 'radio-import-source-file': + await this.doImport(); + break; + + case 'radio-import-source-mendeley': + this._dbs = await Zotero_File_Interface.findMendeleyDatabases(); + // This shouldn't happen, because we only show the wizard if there are databases + if (!this._dbs.length) { + throw new Error("No databases found"); + } + if (this._dbs.length > 1 || true) { + this._populateFileList(this._dbs); + document.getElementById('file-options-header').textContent + = Zotero.getString('fileInterface.chooseAppDatabaseToImport', 'Mendeley') + wizard.goTo('page-file-options'); + wizard.canRewind = true; + this._enableCancel(); + } + break; + + default: + throw new Error(`Unknown mode ${mode}`); + } + } + catch (e) { + this._onDone( + Zotero.getString('general.error'), + Zotero.getString('fileInterface.importError'), + true + ); + throw e; + } + }, + + + onFileSelected: async function () { + this._wizard.canAdvance = true; + }, + + + onFileChosen: async function () { + var index = document.getElementById('file-list').selectedIndex; + this._file = this._dbs[index].path; + this._disableCancel(); + this._wizard.canRewind = false; + this._wizard.canAdvance = false; + await this.doImport(); + }, + + + onBeforeImport: async function (translation) { + // Unrecognized translator + if (!translation) { + // Allow error dialog to be displayed, and then close window + setTimeout(function () { + window.close(); + }); + return; + } + + this._translation = translation; + + // Switch to progress pane + this._wizard.goTo('page-progress'); + var pm = document.getElementById('import-progressmeter'); + + translation.setHandler('itemDone', function () { + pm.value = translation.getProgress(); + }); + }, + + + doImport: async function () { + try { + let result = await Zotero_File_Interface.importFile({ + file: this._file, + onBeforeImport: this.onBeforeImport.bind(this) + }); + + // Cancelled by user or due to error + if (!result) { + window.close(); + return; + } + + let numItems = this._translation.newItems.length; + this._onDone( + Zotero.getString('fileInterface.importComplete'), + Zotero.getString(`fileInterface.itemsWereImported`, numItems, numItems) + ); + } + catch (e) { + this._onDone( + Zotero.getString('general.error'), + Zotero.getString('fileInterface.importError'), + true + ); + throw e; + } + }, + + + reportError: function () { + Zotero.getActiveZoteroPane().reportErrors(); + window.close(); + }, + + + _populateFileList: async function (files) { + var listbox = document.getElementById('file-list'); + + // Remove existing entries + var items = listbox.getElementsByTagName('listitem'); + for (let item of items) { + listbox.removeChild(item); + } + + for (let file of files) { + let li = document.createElement('listitem'); + + let name = document.createElement('listcell'); + // Simply filenames + let nameStr = file.name + .replace(/\.sqlite$/, '') + .replace(/@www\.mendeley\.com$/, ''); + if (nameStr == 'online') { + nameStr = Zotero.getString('dataDir.default', 'online.sqlite'); + } + name.setAttribute('label', nameStr + ' '); + li.appendChild(name); + + let lastModified = document.createElement('listcell'); + lastModified.setAttribute('label', file.lastModified.toLocaleString() + ' '); + li.appendChild(lastModified); + + let size = document.createElement('listcell'); + size.setAttribute( + 'label', + Zotero.getString('general.nMegabytes', (file.size / 1024 / 1024).toFixed(1)) + ' ' + ); + li.appendChild(size); + + listbox.appendChild(li); + } + + if (files.length == 1) { + listbox.selectedIndex = 0; + } + }, + + + _enableCancel: function () { + this._wizard.getButton('cancel').disabled = false; + }, + + + _disableCancel: function () { + this._wizard.getButton('cancel').disabled = true; + }, + + + _onDone: function (label, description, showReportErrorButton) { + var wizard = this._wizard; + wizard.getPageById('page-done').setAttribute('label', label); + document.getElementById('result-description').textContent = description; + + if (showReportErrorButton) { + let button = document.getElementById('result-report-error'); + button.setAttribute('label', Zotero.getString('errorReport.reportError')); + button.hidden = false; + } + + // When done, move to last page and allow closing + wizard.canAdvance = true; + wizard.goTo('page-done'); + wizard.canRewind = false; + } +}; diff --git a/chrome/content/zotero/import/importWizard.xul b/chrome/content/zotero/import/importWizard.xul @@ -0,0 +1,62 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://zotero/skin/importWizard.css" type="text/css"?> + +<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd"> + +<wizard id="import-wizard" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&zotero.import;" + onload="Zotero_Import_Wizard.init()"> + + <script src="../include.js"/> + <script src="../fileInterface.js"/> + <script src="importWizard.js"/> + + <wizardpage pageid="page-start" + label="&zotero.import.whereToImportFrom;" + next="page-progress" + onpageadvanced="Zotero_Import_Wizard.onModeChosen(); return false;"> + <radiogroup id="import-source"> + <radio id="radio-import-source-file" label="&zotero.import.source.file;"/> + <radio id="radio-import-source-mendeley" label="Mendeley"/> + </radiogroup> + </wizardpage> + + <wizardpage pageid="page-file-options" + next="page-progress" + onpagerewound="var w = document.getElementById('import-wizard'); w.goTo('page-start'); w.canAdvance = true; return false;" + onpageadvanced="Zotero_Import_Wizard.onFileChosen()"> + <description id="file-options-header"/> + <listbox id="file-list" onselect="Zotero_Import_Wizard.onFileSelected()"> + <listhead> + <listheader label="&zotero.import.database;"/> + <listheader label="&zotero.import.lastModified;"/> + <listheader label="&zotero.import.size;"/> + </listhead> + + <listcols> + <listcol flex="1"/> + <listcol/> + <listcol/> + </listcols> + </listbox> + </wizardpage> + + <wizardpage pageid="page-progress" + label="&zotero.import.importing;" + onpageshow="document.getElementById('import-wizard').canRewind = false;" + next="page-done"> + <progressmeter id="import-progressmeter" mode="determined"/> + </wizardpage> + + <wizardpage pageid="page-done"> + <description id="result-description"/> + <hbox> + <button id="result-report-error" + oncommand="Zotero_Import_Wizard.reportError()" + hidden="true"/> + </hbox> + </wizardpage> +</wizard> diff --git a/chrome/content/zotero/import/mendeley/mendeleyImport.js b/chrome/content/zotero/import/mendeley/mendeleyImport.js @@ -0,0 +1,830 @@ +var EXPORTED_SYMBOLS = ["Zotero_Import_Mendeley"]; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/osfile.jsm"); +Services.scriptloader.loadSubScript("chrome://zotero/content/include.js"); + +var Zotero_Import_Mendeley = function () { + this.newItems = []; + + this._db; + this._file; + this._itemDone; + this._progress = 0; + this._progressMax; +}; + +Zotero_Import_Mendeley.prototype.setLocation = function (file) { + this._file = file.path || file; +}; + +Zotero_Import_Mendeley.prototype.setHandler = function (name, handler) { + switch (name) { + case 'itemDone': + this._itemDone = handler; + break; + } +}; + +Zotero_Import_Mendeley.prototype.getProgress = function () { + return this._progress / this._progressMax * 100; +}; + +Zotero_Import_Mendeley.prototype.getTranslators = async function () { + return [{ + label: Zotero.getString('fileInterface.appDatabase', 'Mendeley') + }]; +}; + +Zotero_Import_Mendeley.prototype.setTranslator = function () {}; + +Zotero_Import_Mendeley.prototype.translate = async function (options) { + if (true) { + Services.scriptloader.loadSubScript("chrome://zotero/content/import/mendeley/mendeleySchemaMap.js"); + } + // TEMP: Load uncached from ~/zotero-client for development + else { + Components.utils.import("resource://gre/modules/FileUtils.jsm"); + let file = FileUtils.getDir("Home", []); + file = OS.Path.join(file.path, 'zotero-client', 'chrome', 'content', 'zotero', 'import', 'mendeley', 'mendeleySchemaMap.js'); + let fileURI = OS.Path.toFileURI(file); + let xmlhttp = await Zotero.HTTP.request( + 'GET', + fileURI, + { + dontCache: true, + responseType: 'text' + } + ); + eval(xmlhttp.response); + } + + const libraryID = options.libraryID || Zotero.Libraries.userLibraryID; + const { key: rootCollectionKey } = options.collections + ? Zotero.Collections.getLibraryAndKeyFromID(options.collections[0]) + : {}; + + // TODO: Get appropriate version based on schema version + const mapVersion = 83; + map = map[mapVersion]; + + const mendeleyGroupID = 0; + + // Disable syncing while we're importing + var resumeSync = Zotero.Sync.Runner.delayIndefinite(); + + this._db = new Zotero.DBConnection(this._file); + + try { + if (!await this._isValidDatabase()) { + throw new Error("Not a valid Mendeley database"); + } + + // Collections + let folders = await this._getFolders(mendeleyGroupID); + let collectionJSON = this._foldersToAPIJSON(folders, rootCollectionKey); + let folderKeys = this._getFolderKeys(collectionJSON); + await this._saveCollections(libraryID, collectionJSON); + + // + // Items + // + let documents = await this._getDocuments(mendeleyGroupID); + this._progressMax = documents.length; + // Get various attributes mapped to document ids + let urls = await this._getDocumentURLs(mendeleyGroupID); + let creators = await this._getDocumentCreators(mendeleyGroupID, map.creatorTypes); + let tags = await this._getDocumentTags(mendeleyGroupID); + let collections = await this._getDocumentCollections( + mendeleyGroupID, + documents, + rootCollectionKey, + folderKeys + ); + let files = await this._getDocumentFiles(mendeleyGroupID); + let annotations = await this._getDocumentAnnotations(mendeleyGroupID); + for (let document of documents) { + // Save each document with its attributes + let itemJSON = await this._documentToAPIJSON( + map, + document, + urls.get(document.id), + creators.get(document.id), + tags.get(document.id), + collections.get(document.id), + annotations.get(document.id) + ); + let documentIDMap = await this._saveItems(libraryID, itemJSON); + // Save the document's attachments and extracted annotations for any of them + let docFiles = files.get(document.id); + if (docFiles) { + await this._saveFilesAndAnnotations( + docFiles, + libraryID, + documentIDMap.get(document.id), + annotations.get(document.id) + ); + } + this.newItems.push(Zotero.Items.get(documentIDMap.get(document.id))); + this._progress++; + if (this._itemDone) { + this._itemDone(); + } + } + } + finally { + try { + await this._db.closeDatabase(); + } + catch (e) { + Zotero.logError(e); + } + + resumeSync(); + } +}; + +Zotero_Import_Mendeley.prototype._isValidDatabase = async function () { + var tables = [ + 'DocumentContributors', + 'DocumentFiles', + 'DocumentFolders', + 'DocumentKeywords', + 'DocumentTags', + 'DocumentUrls', + 'Documents', + 'Files', + 'Folders', + 'RemoteDocuments', + 'RemoteFolders' + ]; + for (let table of tables) { + if (!await this._db.tableExists(table)) { + return false; + } + } + return true; +}; + +// +// Collections +// +Zotero_Import_Mendeley.prototype._getFolders = async function (groupID) { + return this._db.queryAsync( + `SELECT F.*, RF.remoteUuid FROM Folders F ` + + `JOIN RemoteFolders RF ON (F.id=RF.folderId) ` + + `WHERE groupId=?`, + groupID + ); +}; + +/** + * Get flat array of collection API JSON with parentCollection set + * + * The returned objects include an extra 'id' property for matching collections to documents. + */ +Zotero_Import_Mendeley.prototype._foldersToAPIJSON = function (folderRows, parentKey) { + var maxDepth = 50; + return this._getFolderDescendents(-1, parentKey, folderRows, maxDepth); +}; + +Zotero_Import_Mendeley.prototype._getFolderDescendents = function (folderID, folderKey, folderRows, maxDepth) { + if (maxDepth == 0) return [] + var descendents = []; + var children = folderRows + .filter(f => f.parentId == folderID) + .map(f => { + let c = { + folderID: f.id, + remoteUUID: f.remoteUuid, + key: Zotero.DataObjectUtilities.generateKey(), + name: f.name, + parentCollection: folderKey + }; + if (f.remoteUuid) { + c.relations = { + 'mendeleyDB:remoteFolderUUID': f.remoteUuid + }; + } + return c; + }); + + for (let child of children) { + descendents.push( + child, + ...this._getFolderDescendents(child.folderID, child.key, folderRows, maxDepth - 1) + ); + } + return descendents; +}; + +Zotero_Import_Mendeley.prototype._getFolderKeys = function (collections) { + var map = new Map(); + for (let collection of collections) { + map.set(collection.folderID, collection.key); + } + return map; +}; + +/** + * @param {Integer} libraryID + * @param {Object[]} json + */ +Zotero_Import_Mendeley.prototype._saveCollections = async function (libraryID, json) { + var idMap = new Map(); + for (let collectionJSON of json) { + let collection = new Zotero.Collection; + collection.libraryID = libraryID; + if (collectionJSON.key) { + collection.key = collectionJSON.key; + await collection.loadPrimaryData(); + } + + // Remove external ids before saving + let toSave = Object.assign({}, collectionJSON); + delete toSave.folderID; + delete toSave.remoteUUID; + + collection.fromJSON(toSave); + await collection.saveTx({ + skipSelect: true + }); + idMap.set(collectionJSON.folderID, collection.id); + } + return idMap; +}; + + +// +// Items +// +Zotero_Import_Mendeley.prototype._getDocuments = async function (groupID) { + return this._db.queryAsync( + `SELECT D.*, RD.remoteUuid FROM Documents D ` + + `JOIN RemoteDocuments RD ON (D.id=RD.documentId) ` + + `WHERE groupId=? AND inTrash='false'`, + groupID + ); +}; + +/** + * Get a Map of document ids to arrays of URLs + */ +Zotero_Import_Mendeley.prototype._getDocumentURLs = async function (groupID) { + var rows = await this._db.queryAsync( + `SELECT documentId, CAST(url AS TEXT) AS url FROM DocumentUrls DU ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=? ORDER BY position`, + groupID + ); + var map = new Map(); + for (let row of rows) { + let docURLs = map.get(row.documentId); + if (!docURLs) docURLs = []; + docURLs.push(row.url); + map.set(row.documentId, docURLs); + } + return map; +}; + +/** + * Get a Map of document ids to arrays of creator API JSON + * + * @param {Integer} groupID + * @param {Object} creatorTypeMap - Mapping of Mendeley creator types to Zotero creator types + */ +Zotero_Import_Mendeley.prototype._getDocumentCreators = async function (groupID, creatorTypeMap) { + var rows = await this._db.queryAsync( + `SELECT * FROM DocumentContributors ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=?`, + groupID + ); + var map = new Map(); + for (let row of rows) { + let docCreators = map.get(row.documentId); + if (!docCreators) docCreators = []; + docCreators.push(this._makeCreator( + creatorTypeMap[row.contribution] || 'author', + row.firstNames, + row.lastName + )); + map.set(row.documentId, docCreators); + } + return map; +}; + +/** + * Get a Map of document ids to arrays of tag API JSON + */ +Zotero_Import_Mendeley.prototype._getDocumentTags = async function (groupID) { + var rows = await this._db.queryAsync( + // Manual tags + `SELECT documentId, tag, 0 AS type FROM DocumentTags ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=? ` + + `UNION ` + // Automatic tags + + `SELECT documentId, keyword AS tag, 1 AS type FROM DocumentKeywords ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=?`, + [groupID, groupID] + ); + var map = new Map(); + for (let row of rows) { + let docTags = map.get(row.documentId); + if (!docTags) docTags = []; + docTags.push({ + tag: row.tag, + type: row.type + }); + map.set(row.documentId, docTags); + } + return map; +}; + +/** + * Get a Map of document ids to arrays of collection keys + */ +Zotero_Import_Mendeley.prototype._getDocumentCollections = async function (groupID, documents, rootCollectionKey, folderKeys) { + var rows = await this._db.queryAsync( + `SELECT documentId, folderId FROM DocumentFolders DF ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=?`, + groupID + ); + var map = new Map( + // Add all documents to root collection if specified + documents.map(d => [d.id, rootCollectionKey ? [rootCollectionKey] : []]) + ); + for (let row of rows) { + let keys = map.get(row.documentId); + keys.push(folderKeys.get(row.folderId)); + map.set(row.documentId, keys); + } + return map; +}; + +/** + * Get a Map of document ids to file metadata + */ +Zotero_Import_Mendeley.prototype._getDocumentFiles = async function (groupID) { + var rows = await this._db.queryAsync( + `SELECT documentId, hash, remoteFileUuid, localUrl FROM DocumentFiles ` + + `JOIN Files USING (hash) ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=?`, + groupID + ); + var map = new Map(); + for (let row of rows) { + let docFiles = map.get(row.documentId); + if (!docFiles) docFiles = []; + docFiles.push({ + hash: row.hash, + uuid: row.remoteFileUuid, + fileURL: row.localUrl + }); + map.set(row.documentId, docFiles); + } + return map; +}; + +/** + * Get a Map of document ids to arrays of annotations + */ +Zotero_Import_Mendeley.prototype._getDocumentAnnotations = async function (groupID) { + var rows = await this._db.queryAsync( + `SELECT documentId, uuid, fileHash, page, note, color ` + + `FROM FileNotes ` + + `JOIN RemoteDocuments USING (documentId) ` + + `WHERE groupId=? ` + + `ORDER BY page, y, x`, + groupID + ); + var map = new Map(); + for (let row of rows) { + let docAnnotations = map.get(row.documentId); + if (!docAnnotations) docAnnotations = []; + docAnnotations.push({ + uuid: row.uuid, + hash: row.fileHash, + note: row.note, + page: row.page, + color: row.color + }); + map.set(row.documentId, docAnnotations); + } + return map; +}; + +/** + * Create API JSON array with item and any child attachments or notes + */ +Zotero_Import_Mendeley.prototype._documentToAPIJSON = async function (map, documentRow, urls, creators, tags, collections, annotations) { + var parent = { + key: Zotero.DataObjectUtilities.generateKey() + }; + var children = []; + + parent.itemType = map.itemTypes[documentRow.type]; + if (!parent.itemType) { + Zotero.warn(`Unmapped item type ${documentRow.type}`); + } + if (!parent.itemType || parent.itemType == 'document') { + parent.itemType = this._guessItemType(documentRow); + Zotero.debug(`Guessing type ${parent.itemType}`); + } + var itemTypeID = Zotero.ItemTypes.getID(parent.itemType); + + for (let [mField, zField] of Object.entries(map.fields)) { + // If not mapped, skip + if (!zField) { + continue; + } + let val = documentRow[mField]; + // If no value, skip + if (!val) { + continue; + } + + if (typeof zField == 'string') { + this._processField(parent, children, zField, val); + } + // Function embedded in map file + else if (typeof zField == 'function') { + let [field, val] = zField(documentRow[mField], parent); + this._processField(parent, children, field, val); + } + } + + // URLs + if (urls) { + for (let i = 0; i < urls.length; i++) { + let url = urls[i]; + let isPDF = url.includes('pdf'); + if (i == 0 && !isPDF) { + parent.url = url; + } + else { + children.push({ + itemType: 'attachment', + parentItem: parent.key, + linkMode: 'linked_url', + url, + title: isPDF ? 'PDF' : '', + contentType: isPDF ? 'application/pdf' : '' + }); + } + } + } + + // Combine date parts if present + if (documentRow.year) { + parent.date = documentRow.year.toString().substr(0, 4).padStart(4, '0'); + if (documentRow.month) { + parent.date += '-' + documentRow.month.toString().substr(0, 2).padStart(2, '0'); + if (documentRow.day) { + parent.date += '-' + documentRow.day.toString().substr(0, 2).padStart(2, '0'); + } + } + } + + for (let field in parent) { + switch (field) { + case 'itemType': + case 'key': + case 'parentItem': + case 'note': + case 'creators': + case 'dateAdded': + case 'dateModified': + continue; + } + + // Move unknown/invalid fields to Extra + let fieldID = Zotero.ItemFields.getID(field) + && Zotero.ItemFields.getFieldIDFromTypeAndBase(parent.itemType, field); + if (!fieldID) { + Zotero.warn(`Moving '${field}' to Extra for type ${parent.itemType}`); + parent.extra = this._addExtraField(parent.extra, field, parent[field]); + delete parent[field]; + continue; + } + let newField = Zotero.ItemFields.getName(fieldID); + if (field != newField) { + parent[newField] = parent[field]; + delete parent[field]; + } + } + + if (!parent.dateModified) { + parent.dateModified = parent.dateAdded; + } + + if (creators) { + // Add main creators before any added by fields (e.g., seriesEditor) + parent.creators = [...creators, ...(parent.creators || [])]; + + // If item type has a different primary type, use that for author to prevent a warning + let primaryCreatorType = Zotero.CreatorTypes.getName( + Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID) + ); + if (primaryCreatorType != 'author') { + for (let creator of parent.creators) { + if (creator.creatorType == 'author') { + creator.creatorType = primaryCreatorType; + } + } + } + + for (let creator of parent.creators) { + // seriesEditor isn't valid on some item types (e.g., book) + if (creator.creatorType == 'seriesEditor' + && !Zotero.CreatorTypes.isValidForItemType( + Zotero.CreatorTypes.getID('seriesEditor'), itemTypeID)) { + creator.creatorType = 'editor'; + } + } + } + if (tags) parent.tags = tags; + if (collections) parent.collections = collections; + + // Copy date added/modified to child item + var parentDateAdded = parent.dateAdded; + var parentDateModified = parent.dateModified; + for (let child of children) { + child.dateAdded = parentDateAdded; + child.dateModified = parentDateModified; + } + + // Don't set an explicit key if no children + if (!children.length) { + delete parent.key; + } + + parent.relations = { + 'mendeleyDB:documentUUID': documentRow.uuid.replace(/^\{/, '').replace(/\}$/, '') + }; + if (documentRow.remoteUuid) { + parent.relations['mendeleyDB:remoteDocumentUUID'] = documentRow.remoteUuid; + } + + parent.documentID = documentRow.id; + + var json = [parent, ...children]; + //Zotero.debug(json); + return json; +}; + +/** + * Try to figure out item type based on available fields + */ +Zotero_Import_Mendeley.prototype._guessItemType = function (documentRow) { + if (documentRow.issn || documentRow.issue) { + return 'journalArticle'; + } + if (documentRow.isbn) { + return 'book'; + } + return 'document'; +}; + +Zotero_Import_Mendeley.prototype._extractSubfield = function (field) { + var sub = field.match(/([a-z]+)\[([^\]]+)]/); + return sub ? { field: sub[1], subfield: sub[2] } : { field }; +}; + +Zotero_Import_Mendeley.prototype._processField = function (parent, children, zField, val) { + var { field, subfield } = this._extractSubfield(zField); + if (subfield) { + // Combine 'city' and 'country' into 'place' + if (field == 'place') { + if (subfield == 'city') { + parent.place = val + (parent.place ? ', ' + parent.place : ''); + } + else if (subfield == 'country') { + parent.place = (parent.place ? ', ' + parent.place : '') + val; + } + } + // Convert some item fields as creators + else if (field == 'creator') { + if (!parent.creators) { + parent.creators = []; + } + parent.creators.push(this._makeCreator(subfield, null, val)); + } + else if (field == 'extra') { + parent.extra = this._addExtraField(parent.extra, subfield, val); + } + // Functions + else if (field == 'func') { + // Convert unix timestamps to ISO dates + if (subfield.startsWith('fromUnixtime')) { + let [, zField] = subfield.split(':'); + parent[zField] = Zotero.Date.dateToISO(new Date(val)); + } + // If 'pages' isn't valid for itemType, use 'numPages' instead + else if (subfield == 'pages') { + let itemTypeID = Zotero.ItemTypes.getID(parent.itemType); + if (!Zotero.ItemFields.isValidForType('pages', itemTypeID) + && Zotero.ItemFields.isValidForType('numPages', itemTypeID)) { + zField = 'numPages'; + } + else { + zField = 'pages'; + } + parent[zField] = val; + } + // Notes become child items + else if (subfield == 'note') { + children.push({ + parentItem: parent.key, + itemType: 'note', + note: this._convertNote(val) + }); + } + else { + Zotero.warn(`Unknown function subfield: ${subfield}`); + return; + } + } + else { + Zotero.warn(`Unknown field: ${field}[${subfield}]`); + } + } + else { + // These are added separately so that they're available for notes + if (zField == 'dateAdded' || zField == 'dateModified') { + return; + } + parent[zField] = val; + } +}; + +Zotero_Import_Mendeley.prototype._makeCreator = function (creatorType, firstName, lastName) { + var creator = { creatorType }; + if (firstName) { + creator.firstName = firstName; + creator.lastName = lastName; + } + else { + creator.name = lastName; + } + return creator; +}; + +Zotero_Import_Mendeley.prototype._addExtraField = function (extra, field, val) { + // Strip the field if it appears at the beginning of the value (to avoid "DOI: DOI: 10...") + if (typeof val == 'string') { + val = val.replace(new RegExp(`^${field}:\s*`, 'i'), ""); + } + extra = extra ? extra + '\n' : ''; + if (field != 'arXiv') { + field = field[0].toUpperCase() + field.substr(1); + field = field.replace(/([a-z])([A-Z][a-z])/, "$1 $2"); + } + return extra + `${field}: ${val}`; +}; + +Zotero_Import_Mendeley.prototype._convertNote = function (note) { + return note + // Add newlines after <br> + .replace(/<br\s*\/>/g, '<br\/>\n') + // + // Legacy pre-HTML stuff + // + // <m:linebreak> + .replace(/<m:linebreak><\/m:linebreak>/g, '<br/>') + // <m:bold> + .replace(/<(\/)?m:bold>/g, '<$1b>') + // <m:italic> + .replace(/<(\/)?m:italic>/g, '<$1i>') + // <m:center> + .replace(/<m:center>/g, '<p style="text-align: center;">') + .replace(/<\/m:center>/g, '</p>') + // <m:underline> + .replace(/<m:underline>/g, '<span style="text-decoration: underline;">') + .replace(/<\/m:underline>/g, '</span>'); +}; + +Zotero_Import_Mendeley.prototype._saveItems = async function (libraryID, json) { + var idMap = new Map(); + await Zotero.DB.executeTransaction(async function () { + for (let itemJSON of json) { + let item = new Zotero.Item; + item.libraryID = libraryID; + if (itemJSON.key) { + item.key = itemJSON.key; + await item.loadPrimaryData(); + } + + // Remove external id before save + let toSave = Object.assign({}, itemJSON); + delete toSave.documentID; + + item.fromJSON(toSave); + await item.save({ + skipSelect: true, + skipDateModifiedUpdate: true + }); + if (itemJSON.documentID) { + idMap.set(itemJSON.documentID, item.id); + } + } + }.bind(this)); + return idMap; +}; + +/** + * Saves attachments and extracted annotations for a given document + */ +Zotero_Import_Mendeley.prototype._saveFilesAndAnnotations = async function (files, libraryID, parentItemID, annotations) { + for (let file of files) { + try { + if (!file.fileURL) continue; + + let path = OS.Path.fromFileURI(file.fileURL); + + let attachment; + if (await OS.File.exists(path)) { + let options = { + libraryID, + parentItemID, + file: path + }; + // If file is in Mendeley downloads folder, import it + if (OS.Path.dirname(path).endsWith(OS.Path.join('Mendeley Desktop', 'Downloaded'))) { + attachment = await Zotero.Attachments.importFromFile(options); + } + // Otherwise link it + else { + attachment = await Zotero.Attachments.linkFromFile(options); + } + attachment.relations = { + 'mendeleyDB:fileHash': file.hash, + 'mendeleyDB:fileUUID': file.uuid + }; + await attachment.saveTx({ + skipSelect: true + }); + } + else { + Zotero.warn(path + " not found -- not importing"); + } + + if (annotations) { + await this._saveAnnotations( + // We have annotations from all files for this document, so limit to just those on + // this file + annotations.filter(a => a.hash == file.hash), + parentItemID, + attachment ? attachment.id : null + ); + } + } + catch (e) { + Zotero.logError(e); + } + } +} + +Zotero_Import_Mendeley.prototype._saveAnnotations = async function (annotations, parentItemID, attachmentItemID) { + if (!annotations.length) return; + var noteStrings = []; + var parentItem = Zotero.Items.get(parentItemID); + var libraryID = parentItem.libraryID; + if (attachmentItemID) { + var attachmentItem = Zotero.Items.get(attachmentItemID); + var attachmentURIPath = Zotero.API.getLibraryPrefix(libraryID) + '/items/' + attachmentItem.key; + } + + for (let annotation of annotations) { + if (!annotation.note || !annotation.note.trim()) continue; + + let linkStr; + let linkText = `note on p. ${annotation.page}`; + if (attachmentItem) { + let url = `zotero://open-pdf/${attachmentURIPath}?page=${annotation.page}`; + linkStr = `<a href="${url}">${linkText}</a>`; + } + else { + linkStr = linkText; + } + + noteStrings.push( + Zotero.Utilities.text2html(annotation.note.trim()) + + `<p class="pdf-link" style="margin-top: -0.5em; margin-bottom: 2em; font-size: .9em; text-align: right;">(${linkStr})</p>` + ); + } + + if (!noteStrings.length) return; + + let note = new Zotero.Item('note'); + note.libraryID = libraryID; + note.parentItemID = parentItemID; + note.setNote('<h1>' + Zotero.getString('extractedAnnotations') + '</h1>\n' + noteStrings.join('\n')); + return note.saveTx({ + skipSelect: true + }); +}; diff --git a/chrome/content/zotero/import/mendeley/mendeleySchemaMap.js b/chrome/content/zotero/import/mendeley/mendeleySchemaMap.js @@ -0,0 +1,102 @@ +var map = { + 83: { + itemTypes: { + Bill: "bill", + Book: "book", + BookSection: "bookSection", + Case: "case", + ComputerProgram: "computerProgram", + ConferenceProceedings: "conferencePaper", + EncyclopediaArticle: "encyclopediaArticle", + Film: "film", + Generic: "document", + JournalArticle: "journalArticle", + MagazineArticle: "magazineArticle", + NewspaperArticle: "newspaperArticle", + Patent: "patent", + Report: "report", + Statute: "statute", + TelevisionBroadcast: "tvBroadcast", + Thesis: "thesis", + WebPage: "webpage", + WorkingPaper: "report" + }, + fields: { + id: "", + uuid: "", + reviewedArticle: "", + revisionNumber: "", + publisher: "publisher", + reprintEdition: "", + series: "seriesTitle", + seriesNumber: "seriesNumber", + sections: "section", + seriesEditor: "creator[seriesEditor]", // falls back to editor if necessary + owner: "", + pages: "func[pages]", + month: "", // handled explicitly + originalPublication: "", + publication: "publicationTitle", + publicLawNumber: "publicLawNumber", + pmid: "extra[PMID]", + sourceType: "", + session: "session", + shortTitle: "shortTitle", + volume: "volume", + year: "", // handled explicitly + userType: "type", + country: "place[country]", + dateAccessed: "accessDate", + committee: "committee", + counsel: "creator[counsel]", + doi: "DOI", + edition: "edition", + day: "", // handled explicitly + department: "", + citationKey: "citationKey", // put in Extra + city: "place[city]", + chapter: "", + codeSection: "section", + codeVolume: "codeVolume", + code: "code", + codeNumber: "codeNumber", + issue: "issue", + language: "language", + isbn: "ISBN", + issn: "ISSN", + length: "", + medium: "medium", + lastUpdate: "", + legalStatus: "legalStatus", + hideFromMendeleyWebIndex: "", + institution: "publisher", + genre: "genre", + internationalTitle: "", + internationalUserType: "", + internationalAuthor: "", + internationalNumber: "", + deletionPending: "", + favourite: "", // tag? + confirmed: "", // tag? + deduplicated: "", + read: "", // tag? + type: "", // item type handled separately + title: "title", + privacy: "", + applicationNumber: "applicationNumber", + arxivId: "extra[arXiv]", + advisor: "", + articleColumn: "", + modified: "func[fromUnixtime:dateModified]", + abstract: "abstractNote", + added: "func[fromUnixtime:dateAdded]", + note: "func[note]", + importer: "" + }, + creatorTypes: { + DocumentAuthor: "author", + DocumentEditor: "editor", + DocumentTranslator: "translator" + } + } +}; diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -712,7 +712,7 @@ Zotero.Collection.prototype.toJSON = function (options = {}) { obj.name = this.name; obj.parentCollection = this.parentKey ? this.parentKey : false; - obj.relations = {}; // TEMP + obj.relations = this.getRelations(); return this._postToJSON(env); } diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js @@ -285,7 +285,7 @@ Zotero.DataObject.prototype._setParentKey = function(key) { /** * Returns all relations of the object * - * @return {Object} - Object with predicates as keys and arrays of URIs as values + * @return {Object} - Object with predicates as keys and arrays of values */ Zotero.DataObject.prototype.getRelations = function () { this._requireData('relations'); @@ -410,7 +410,7 @@ Zotero.DataObject.prototype.setRelations = function (newRelations) { // Limit predicates to letters and colons for now for (let p in newRelations) { - if (!/[a-z]+:[a-z]+/.test(p)) { + if (!/^[a-z]+:[a-z]+$/i.test(p)) { throw new Error(`Invalid relation predicate '${p}'`); } } diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js @@ -32,7 +32,8 @@ Zotero.Relations = new function () { this._namespaces = { dc: 'http://purl.org/dc/elements/1.1/', - owl: 'http://www.w3.org/2002/07/owl#' + owl: 'http://www.w3.org/2002/07/owl#', + mendeleyDB: 'http://zotero.org/namespaces/mendeleyDB#' }; var _types = ['collection', 'item']; diff --git a/chrome/content/zotero/xpcom/mime.js b/chrome/content/zotero/xpcom/mime.js @@ -47,8 +47,9 @@ Zotero.MIME = new function(){ ["\uFFFDPNG", 'image/png', 0], ["JFIF", 'image/jpeg'], ["FLV", "video/x-flv", 0], - ["\u0000\u0000\u0001\u0000", "image/vnd.microsoft.icon", 0] - + ["\u0000\u0000\u0001\u0000", "image/vnd.microsoft.icon", 0], + ["\u0053\u0051\u004C\u0069\u0074\u0065\u0020\u0066" + + "\u006F\u0072\u006D\u0061\u0074\u0020\u0033\u0000", "application/x-sqlite3", 0] ]; var _extensions = { diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul @@ -49,7 +49,7 @@ <commandset id="mainCommandSet"> <command id="cmd_zotero_reportErrors" oncommand="ZoteroPane_Local.reportErrors();"/> - <command id="cmd_zotero_import" oncommand="Zotero_File_Interface.importFile();"/> + <command id="cmd_zotero_import" oncommand="Zotero_File_Interface.startImport();"/> <command id="cmd_zotero_importFromClipboard" oncommand="Zotero_File_Interface.importFromClipboard();"/> <command id="cmd_zotero_exportLibrary" oncommand="Zotero_File_Interface.exportFile();"/> <command id="cmd_zotero_advancedSearch" oncommand="ZoteroPane_Local.openAdvancedSearchWindow();"/> diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd @@ -202,6 +202,14 @@ <!ENTITY zotero.progress.title "Progress"> +<!ENTITY zotero.import "Import"> +<!ENTITY zotero.import.whereToImportFrom "Where do you want to import from?"> +<!ENTITY zotero.import.source.file "A file (BibTeX, RIS, Zotero RDF, etc.)"> +<!ENTITY zotero.import.importing "Importing…"> +<!ENTITY zotero.import.database "Database"> +<!ENTITY zotero.import.lastModified "Last Modified"> +<!ENTITY zotero.import.size "Size"> + <!ENTITY zotero.exportOptions.title "Export…"> <!ENTITY zotero.exportOptions.format.label "Format:"> <!ENTITY zotero.exportOptions.translatorOptions.label "Translator Options"> diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -69,6 +69,7 @@ general.processing = Processing general.submitted = Submitted general.thanksForHelpingImprove = Thanks for helping to improve %S! general.describeProblem = Briefly describe the problem: +general.nMegabytes = %S MB general.operationInProgress = A Zotero operation is currently in progress. general.operationInProgress.waitUntilFinished = Please wait until it has finished. @@ -688,10 +689,12 @@ fileInterface.importComplete = Import Complete fileInterface.itemsWereImported = %1$S item was imported;%1$S items were imported fileInterface.itemsExported = Exporting items… fileInterface.import = Import +fileInterface.chooseAppDatabaseToImport = Choose the %S database to import fileInterface.export = Export fileInterface.exportedItems = Exported Items fileInterface.imported = Imported fileInterface.unsupportedFormat = The selected file is not in a supported format. +fileInterface.appDatabase = %S Database fileInterface.viewSupportedFormats = View Supported Formats… fileInterface.untitledBibliography = Untitled Bibliography fileInterface.bibliographyHTMLTitle = Bibliography @@ -1075,6 +1078,7 @@ rtfScan.rtf = Rich Text Format (.rtf) rtfScan.saveTitle = Select a location in which to save the formatted file rtfScan.scannedFileSuffix = (Scanned) +extractedAnnotations = Extracted Annotations file.accessError.theFileCannotBeCreated = The file '%S' cannot be created. file.accessError.theFileCannotBeUpdated = The file '%S' cannot be updated. diff --git a/chrome/skin/default/zotero/importWizard.css b/chrome/skin/default/zotero/importWizard.css @@ -0,0 +1,44 @@ +.wizard-header-label { + font-size: 16px; + font-weight: bold; +} + +/* Start */ +wizard[currentpageid="page-start"] .wizard-header-label { + padding-top: 24px; +} + +wizard[currentpageid="page-start"] .wizard-page-box { + margin-top: -2px; + padding-top: 0; +} + +radiogroup { + font-size: 14px; + margin-top: 4px; +} + +radio { + padding-top: 5px; +} + +/* File options */ +wizard[currentpageid="page-file-options"] .wizard-header { + display: none; +} + +#file-options-header { + font-size: 15px; + font-weight: bold; + margin-bottom: 6px; +} + +listbox, #result-description { + font-size: 13px; +} + +#result-report-error { + margin-top: 13px; + margin-left: 0; + font-size: 13px; +}