mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Add continuous-renaming functionality for attachment files (#3860)
Resolves #1685
This commit is contained in:
parent
17bcb1e84c
commit
e896fd0137
35 changed files with 1273 additions and 332 deletions
|
|
@ -338,6 +338,9 @@ class VirtualizedTable extends React.Component {
|
|||
|
||||
this.preventScrollKeys = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", " ", "PageUp", "PageDown"]);
|
||||
this.onSelection = oncePerAnimationFrame(this._onSelection);
|
||||
|
||||
// Create a map of custom row heights (if provided) so `this._renderItem` can apply the correct per-row height
|
||||
this._customRowHeightMap = Object.fromEntries(props.customRowHeights ?? []);
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -450,6 +453,8 @@ class VirtualizedTable extends React.Component {
|
|||
onFocus: PropTypes.func,
|
||||
|
||||
onItemContextMenu: PropTypes.func,
|
||||
customRowHeights: PropTypes.array,
|
||||
getRowHeight: PropTypes.func,
|
||||
};
|
||||
|
||||
// ------------------------ Selection Methods ------------------------- //
|
||||
|
|
@ -1099,6 +1104,7 @@ class VirtualizedTable extends React.Component {
|
|||
itemHeight: this._rowHeight,
|
||||
renderItem: this._renderItem,
|
||||
targetElement: document.getElementById(this._jsWindowID),
|
||||
customRowHeights: this.props.customRowHeights ?? []
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1115,7 +1121,7 @@ class VirtualizedTable extends React.Component {
|
|||
node.addEventListener('mouseup', e => this._handleMouseUp(e, index), { passive: true });
|
||||
node.addEventListener('dblclick', e => this._activateNode(e, [index]), { passive: true });
|
||||
}
|
||||
node.style.height = this._rowHeight + 'px';
|
||||
node.style.height = (index in this._customRowHeightMap ? this._customRowHeightMap[index] : this._rowHeight) + 'px';
|
||||
node.id = this.props.id + "-row-" + index;
|
||||
node.classList.toggle('odd', index % 2 == 1);
|
||||
node.classList.toggle('even', index % 2 == 0);
|
||||
|
|
@ -1300,14 +1306,19 @@ class VirtualizedTable extends React.Component {
|
|||
* @param customRowHeights an array of tuples specifying row index and row height: e.g. [[1, 10], [5, 10]]
|
||||
*/
|
||||
updateCustomRowHeights = (customRowHeights=[]) => {
|
||||
this._customRowHeightMap = Object.fromEntries(customRowHeights);
|
||||
return this._jsWindow.update({customRowHeights});
|
||||
};
|
||||
|
||||
_getRowHeight() {
|
||||
if (this.props.getRowHeight) {
|
||||
return this.props.getRowHeight(this);
|
||||
}
|
||||
let rowHeight = this.props.linesPerRow * this._renderedTextHeight;
|
||||
if (!this.props.disableFontSizeScaling) {
|
||||
rowHeight *= Zotero.Prefs.get('fontSize');
|
||||
}
|
||||
|
||||
rowHeight += Zotero.Prefs.get('uiDensity') === 'comfortable' ? 11 : 5;
|
||||
|
||||
// @TODO: Check row height across platforms and remove commented code below
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
|
||||
{
|
||||
let { canRenameFileFromParent, renameFileFromParent } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
class AttachmentBox extends ItemPaneSectionElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<collapsible-section data-l10n-id="section-attachment-info" data-pane="attachment-info">
|
||||
|
|
@ -43,7 +44,10 @@
|
|||
</html:div>
|
||||
<html:div id="fileNameRow" class="meta-row">
|
||||
<html:div class="meta-label"><html:label id="fileName-label" class="key" data-l10n-id="attachment-info-filename"/></html:div>
|
||||
<html:div class="meta-data"><editable-text id="fileName" aria-labelledby="fileName-label" tight="true"/></html:div>
|
||||
<html:div class="meta-data">
|
||||
<editable-text id="fileName" aria-labelledby="fileName-label" tight="true"/>
|
||||
<toolbarbutton id="rename-from-parent" data-l10n-id="attachment-rename-from-parent" tabindex="0" oncommand=""/>
|
||||
</html:div>
|
||||
</html:div>
|
||||
<html:div id="accessedRow" class="meta-row">
|
||||
<html:div class="meta-label"><html:label id="accessed-label" class="key" data-l10n-id="attachment-info-accessed"/></html:div>
|
||||
|
|
@ -257,6 +261,9 @@
|
|||
fileName.addEventListener('focus', this._handleFileNameFocus);
|
||||
fileName.addEventListener('blur', this._handleFileNameBlur);
|
||||
|
||||
let renameFromParent = this._id("rename-from-parent");
|
||||
renameFromParent.addEventListener("command", this._handleRenameFromParent);
|
||||
|
||||
let noteButton = this._id('note-button');
|
||||
noteButton.addEventListener("command", this._handleNoteButtonCommand);
|
||||
|
||||
|
|
@ -510,6 +517,12 @@
|
|||
else {
|
||||
selectButton.hidden = true;
|
||||
}
|
||||
|
||||
|
||||
const isRenamePossible = this._item.isAttachment() && !this._item.isTopLevelItem();
|
||||
|
||||
// Hide the rename button for cases where it's not possible to rename from parent, not editable, the file does not exist, or the file name would not be changed
|
||||
this._id("rename-from-parent").hidden = !isRenamePossible || !this.editable || !fileExists || !(await canRenameFileFromParent(this._item));
|
||||
}
|
||||
|
||||
async updatePreview() {
|
||||
|
|
@ -626,7 +639,7 @@
|
|||
}
|
||||
|
||||
// Force overwrite, but make sure we check that this doesn't fail
|
||||
renamed = await item.renameAttachmentFile(newFilename, true);
|
||||
renamed = await item.renameAttachmentFile(newFilename, { overwrite: true });
|
||||
}
|
||||
|
||||
if (renamed == -2) {
|
||||
|
|
@ -787,6 +800,10 @@
|
|||
}
|
||||
};
|
||||
|
||||
_handleRenameFromParent = async () => {
|
||||
await renameFileFromParent(this.item);
|
||||
};
|
||||
|
||||
_handleMetaLabelMousedown = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2566,7 +2566,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
let parentItem;
|
||||
if (parentItemID
|
||||
&& data.length == 1
|
||||
&& Zotero.Attachments.shouldAutoRenameFile(dropEffect == 'link')) {
|
||||
&& Zotero.Attachments.shouldAutoRenameFile(dropEffect == 'link', targetLibraryID)) {
|
||||
parentItem = Zotero.Items.get(parentItemID);
|
||||
if (!parentItem.numNonHTMLFileAttachments()) {
|
||||
renameIfAllowedType = true;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
https://www.zotero.org
|
||||
Copyright © Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
https://www.zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
|
|
@ -24,24 +24,96 @@
|
|||
*/
|
||||
/* global Zotero_Preferences: false */
|
||||
|
||||
const { DEFAULT_ATTACHMENT_RENAME_TEMPLATE, openRenameFilesPreview,
|
||||
promptAutoRenameFiles } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
|
||||
Zotero_Preferences.FileRenaming = {
|
||||
mockItem: null,
|
||||
defaultExt: 'pdf',
|
||||
prompted: false,
|
||||
|
||||
init: function () {
|
||||
this.inputRef = document.getElementById('file-renaming-format-template');
|
||||
this.lastFormatString = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate') ?? DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
|
||||
this.isTemplateInSync = Zotero.Prefs.get('autoRenameFiles.done');
|
||||
this.inputEl = document.getElementById('file-renaming-format-template');
|
||||
this.backButtonEl = document.getElementById('prefs-subpane-back-button');
|
||||
this.navigationEl = document.getElementById('prefs-navigation');
|
||||
this.renameNowBtnEl = document.getElementById('file-renaming-rename-now');
|
||||
|
||||
this.updatePreview();
|
||||
this.inputRef.addEventListener('input', this.updatePreview.bind(this));
|
||||
this.inputRef.addEventListener('blur', this.handleInputBlur.bind(this));
|
||||
this.inputEl.addEventListener('input', this.handleInputChange.bind(this));
|
||||
this.inputEl.addEventListener('blur', this.handleInputBlur.bind(this));
|
||||
this.renameNowBtnEl.addEventListener('command', this.renameNow.bind(this));
|
||||
this.renameNowBtnEl.setAttribute('disabled', Zotero.Prefs.get('autoRenameFiles.done'));
|
||||
this.inputEl.value = this.lastFormatString;
|
||||
|
||||
this._itemsView = Zotero.getActiveZoteroPane()?.itemsView;
|
||||
this._updatePreview = this.updatePreview.bind(this);
|
||||
this._promptReplace = this.promptReplace.bind(this);
|
||||
this._handleDonePrefChange = this.handleDonePrefChange.bind(this);
|
||||
|
||||
this._renameFilesPrefObserver = Zotero.Prefs.registerObserver('autoRenameFiles.done', this._handleDonePrefChange);
|
||||
|
||||
if (this._itemsView) {
|
||||
this._itemsView.onSelect.addListener(this._updatePreview);
|
||||
}
|
||||
if (this.backButtonEl) {
|
||||
this.backButtonEl.addEventListener('command', this._promptReplace);
|
||||
}
|
||||
if (this.navigationEl) {
|
||||
this.navigationEl.addEventListener('select', this._promptReplace);
|
||||
}
|
||||
},
|
||||
|
||||
uninit: function () {
|
||||
this._itemsView.onSelect.removeListener(this._updatePreview);
|
||||
this.backButtonEl.removeEventListener('command', this._promptReplace);
|
||||
this.navigationEl.removeEventListener('select', this._promptReplace);
|
||||
Zotero.Prefs.unregisterObserver(this._renameFilesPrefObserver);
|
||||
this.promptReplace();
|
||||
},
|
||||
|
||||
async handleInputChange() {
|
||||
const formatString = this.inputEl.value;
|
||||
// Ignore empty value, which we'll reset in handleInputBlur() if necessary
|
||||
if (formatString.replace(/\s/g, '') === '') {
|
||||
return;
|
||||
}
|
||||
this.updatePreview();
|
||||
await Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate', formatString);
|
||||
|
||||
// reset 'done' to enable the rename button, set it to `false` if the
|
||||
// template is out of sync (e.g., the user changed it and declined
|
||||
// renaming) or if the new template has changed
|
||||
Zotero.Prefs.set('autoRenameFiles.done', this.isTemplateInSync ? formatString === this.lastFormatString : false);
|
||||
},
|
||||
|
||||
async handleInputBlur() {
|
||||
const formatString = this.inputEl.value;
|
||||
if (formatString.replace(/\s/g, '') === '') {
|
||||
this.inputEl.value = this.lastFormatString = DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
|
||||
this.updatePreview();
|
||||
await Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate');
|
||||
}
|
||||
},
|
||||
|
||||
handleDonePrefChange(newValue) {
|
||||
this.renameNowBtnEl.setAttribute('disabled', newValue);
|
||||
|
||||
// renaming has finished, store the new value of the template and reset the flags
|
||||
if (newValue) {
|
||||
this.lastFormatString = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate');
|
||||
this.isTemplateInSync = true;
|
||||
this.prompted = false;
|
||||
}
|
||||
},
|
||||
|
||||
promptReplace: function () {
|
||||
if (!this.prompted && !Zotero.Prefs.get('autoRenameFiles.done')) {
|
||||
// Set the flag to avoid repeating the prompt while renaming is in progress or user declined renaming
|
||||
this.prompted = true;
|
||||
promptAutoRenameFiles();
|
||||
}
|
||||
},
|
||||
|
||||
getActiveItem() {
|
||||
|
|
@ -62,22 +134,18 @@ Zotero_Preferences.FileRenaming = {
|
|||
|
||||
updatePreview() {
|
||||
const [item, ext, attachmentTitle] = this.getActiveItem() ?? [this.mockItem ?? this.makeMockItem(), this.defaultExt, ''];
|
||||
const formatString = this.inputRef.value;
|
||||
const formatString = this.inputEl.value;
|
||||
const preview = Zotero.Attachments.getFileBaseNameFromItem(item, { formatString, attachmentTitle });
|
||||
document.getElementById('file-renaming-format-preview').innerText = `${preview}.${ext}`;
|
||||
},
|
||||
|
||||
handleInputBlur() {
|
||||
const formatString = this.inputRef.value;
|
||||
const prefKey = this.inputRef.getAttribute('preference');
|
||||
if (formatString.replace(/\s/g, '') === '') {
|
||||
Zotero.Prefs.clear(prefKey, true);
|
||||
this.updatePreview();
|
||||
}
|
||||
async renameNow() {
|
||||
openRenameFilesPreview();
|
||||
},
|
||||
|
||||
makeMockItem() {
|
||||
this.mockItem = new Zotero.Item('journalArticle');
|
||||
this.mockItem.libraryID = Zotero.Libraries.userLibraryID;
|
||||
this.mockItem.setField('title', 'Example Title: Example Subtitle');
|
||||
this.mockItem.setCreators([
|
||||
{ firstName: 'Jane', lastName: 'Doe', creatorType: 'author' },
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@
|
|||
<html:textarea
|
||||
aria-labelledby="file-renaming-format-template-label"
|
||||
id="file-renaming-format-template"
|
||||
preference="extensions.zotero.attachmentRenameTemplate"
|
||||
rows="8"
|
||||
/>
|
||||
<html:label id="file-renaming-format-preview-label">
|
||||
|
|
@ -68,5 +67,11 @@
|
|||
aria-labelledby="file-renaming-format-preview-label"
|
||||
id="file-renaming-format-preview"
|
||||
/>
|
||||
<hbox id="file-renaming-format-preview-buttons">
|
||||
<button
|
||||
id="file-renaming-rename-now"
|
||||
data-l10n-id="preferences-file-renaming-rename-now"
|
||||
/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
</vbox>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"use strict";
|
||||
|
||||
var { FilePicker } = ChromeUtils.importESModule('chrome://zotero/content/modules/filePicker.mjs');
|
||||
let { openRenameFilesPreview, promptAutoRenameFiles } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
|
||||
Zotero_Preferences.General = {
|
||||
_openURLResolvers: null,
|
||||
|
|
@ -53,6 +54,7 @@ Zotero_Preferences.General = {
|
|||
}
|
||||
document.getElementById('openurl-primary-popup').firstChild.setAttribute('label', resolverName);
|
||||
|
||||
this._renameFilesPrefObserver = Zotero.Prefs.registerObserver('autoRenameFiles.done', this._handleRenameFilesDonePrefChange.bind(this));
|
||||
this.refreshLocale();
|
||||
this._initItemPaneHeaderUI();
|
||||
this.updateAutoRenameFilesUI();
|
||||
|
|
@ -61,6 +63,10 @@ Zotero_Preferences.General = {
|
|||
this._initAutoDisableToolCheckbox();
|
||||
},
|
||||
|
||||
uninit: function () {
|
||||
Zotero.Prefs.unregisterObserver(this._renameFilesPrefObserver);
|
||||
},
|
||||
|
||||
_getAutomaticLocaleMenuLabel: function () {
|
||||
return Zotero.getString(
|
||||
'zotero.preferences.locale.automaticWithLocale',
|
||||
|
|
@ -233,6 +239,7 @@ Zotero_Preferences.General = {
|
|||
}
|
||||
}
|
||||
Zotero.Prefs.set('autoRenameFiles.fileTypes', [...enabledTypes].join(','));
|
||||
Zotero.Prefs.set('autoRenameFiles.done', false);
|
||||
},
|
||||
|
||||
updateAutoRenameFilesUI: function () {
|
||||
|
|
@ -245,8 +252,23 @@ Zotero_Preferences.General = {
|
|||
checkbox.disabled = disabled;
|
||||
}
|
||||
document.getElementById('rename-linked-files').disabled = disabled;
|
||||
document.getElementById('file-renaming-general-rename-now').setAttribute('hidden', Zotero.Prefs.get('autoRenameFiles.done'));
|
||||
},
|
||||
|
||||
handleAutoRenameChange: function () {
|
||||
if (Zotero.Prefs.get('autoRenameFiles')) {
|
||||
promptAutoRenameFiles();
|
||||
}
|
||||
},
|
||||
|
||||
_handleRenameFilesDonePrefChange: function (newValue) {
|
||||
document.getElementById('file-renaming-general-rename-now').setAttribute('hidden', newValue);
|
||||
},
|
||||
|
||||
openRenameFilesPreview: function () {
|
||||
openRenameFilesPreview();
|
||||
},
|
||||
|
||||
//
|
||||
// File handlers
|
||||
//
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@
|
|||
|
||||
***** END LICENSE BLOCK *****
|
||||
-->
|
||||
<vbox id="zotero-prefpane-general" onload="Zotero_Preferences.General.init()">
|
||||
<vbox id="zotero-prefpane-general"
|
||||
onload="Zotero_Preferences.General.init()"
|
||||
onunload="Zotero_Preferences.General.uninit()"
|
||||
>
|
||||
<vbox class="main-section">
|
||||
<groupbox aria-labelledby="preferences-appearance-title">
|
||||
<label><html:h2 id="preferences-appearance-title" data-l10n-id="preferences-appearance-title"/></label>
|
||||
|
|
@ -108,9 +111,10 @@
|
|||
<vbox align="start">
|
||||
<label id="preferences-file-renaming-intro" data-l10n-id="preferences-file-renaming-intro"/>
|
||||
<separator class="thin"/>
|
||||
<checkbox data-l10n-id="preferences-file-renaming-auto-rename-files"
|
||||
<checkbox id="auto-rename-files"
|
||||
data-l10n-id="preferences-file-renaming-auto-rename-files"
|
||||
preference="extensions.zotero.autoRenameFiles"
|
||||
oncommand="setTimeout(() => Zotero_Preferences.General.updateAutoRenameFilesUI())" native="true"
|
||||
oncommand="setTimeout(() => { Zotero_Preferences.General.handleAutoRenameChange(); Zotero_Preferences.General.updateAutoRenameFilesUI(); })" native="true"
|
||||
/>
|
||||
<vbox class="indented-pref" aria-labelledby="preferences-file-renaming-file-types" role="group">
|
||||
<label id="preferences-file-renaming-file-types" data-l10n-id="preferences-file-renaming-file-types"/>
|
||||
|
|
@ -151,12 +155,19 @@
|
|||
preference="extensions.zotero.autoRenameFiles.linked"
|
||||
oncommand="Zotero_Preferences.General.updateAutoRenameFilesUI()" native="true"
|
||||
/>
|
||||
|
||||
<button id="file-renaming-button"
|
||||
data-l10n-id="preferences-file-renaming-customize-button"
|
||||
data-search-strings="preferences-file-renaming-format-title, preferences-file-renaming-format-template"
|
||||
oncommand="Zotero_Preferences.navigateToPane('zotero-subpane-file-renaming')"
|
||||
/>
|
||||
<hbox id="file-renaming-buttons">
|
||||
<button id="file-renaming-customize-button"
|
||||
data-l10n-id="preferences-file-renaming-customize-button"
|
||||
data-search-strings="preferences-file-renaming-format-title, preferences-file-renaming-format-template"
|
||||
oncommand="Zotero_Preferences.navigateToPane('zotero-subpane-file-renaming')"
|
||||
/>
|
||||
<button
|
||||
hidden="true"
|
||||
id="file-renaming-general-rename-now"
|
||||
data-l10n-id="preferences-file-renaming-rename-now"
|
||||
oncommand="Zotero_Preferences.General.openRenameFilesPreview()"
|
||||
/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
|
||||
|
|
|
|||
318
chrome/content/zotero/renameFiles.mjs
Normal file
318
chrome/content/zotero/renameFiles.mjs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2025 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
http://zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
var { Zotero } = ChromeUtils.importESModule("chrome://zotero/content/zotero.mjs");
|
||||
|
||||
const clamp = (val, min = 0, max = 1.0) => Math.min(Math.max(val, min), max);
|
||||
|
||||
export const DEFAULT_ATTACHMENT_RENAME_TEMPLATE = "{{ firstCreator suffix=\" - \" }}{{ year suffix=\" - \" }}{{ title truncate=\"100\" }}";
|
||||
export const DEFAULT_AUTO_RENAME_FILE_TYPES = "application/pdf,application/epub+zip";
|
||||
|
||||
/**
|
||||
* Rename eligible attachment files based on their parent items' metadata.
|
||||
* @async
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.userLibrary=true] - Process "My Library".
|
||||
* @param {boolean} [options.groupLibrary=false] - Process group libraries.
|
||||
* @param {boolean} [options.pretend=false] - If true, perform a dry run (compile a list of files to rename).
|
||||
* @param {(progress:number)=>void} [options.reportProgress] - Callback for progress updates (0..1).
|
||||
* @returns {Promise<Array<{attachmentId:number,parentItemId:number,oldName:string,newName:string,isFilePresent:boolean}>>}
|
||||
* Summary of (performed or proposed) rename operations.
|
||||
*/
|
||||
export async function renameFilesFromParent({ userLibrary = true, groupLibrary = false, pretend = false, reportProgress = () => {} } = {}) {
|
||||
const t1 = Date.now();
|
||||
let summary = [];
|
||||
let progress = 0;
|
||||
|
||||
let adjustProgressBy = (additionalProgress) => {
|
||||
progress = clamp(progress + additionalProgress);
|
||||
reportProgress(progress);
|
||||
};
|
||||
|
||||
let libraries = Zotero.Libraries.getAll();
|
||||
adjustProgressBy(0.01); // move progress bar slightly while we load required data
|
||||
let items = [];
|
||||
let librariesWithAttachmentsToRename = [];
|
||||
|
||||
for (let library of libraries) {
|
||||
let shouldRename = userLibrary && library.libraryType === 'user';
|
||||
|
||||
if (!shouldRename) {
|
||||
// for group libraries, check `autoRenameFiles` synced setting
|
||||
shouldRename = groupLibrary && Zotero.SyncedSettings.get(library.libraryID, 'autoRenameFiles');
|
||||
}
|
||||
|
||||
if (shouldRename) {
|
||||
items.push(...await Zotero.Items.getAll(library.libraryID, false, true));
|
||||
librariesWithAttachmentsToRename.push(library);
|
||||
}
|
||||
}
|
||||
|
||||
adjustProgressBy(0.01);
|
||||
|
||||
await Zotero.Items.loadDataTypes(items, ['itemData']);
|
||||
adjustProgressBy(0.01);
|
||||
|
||||
// use remaining 97% of progress bar for renaming attachments
|
||||
let perItemProgress = 0.97 / items.length;
|
||||
let count = 0;
|
||||
let noFilePresentCount = 0;
|
||||
|
||||
for (let parentItem of items) {
|
||||
adjustProgressBy(perItemProgress);
|
||||
if (!parentItem.isTopLevelItem() || !parentItem.isRegularItem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let attachmentItem = await parentItem.getBestAttachment();
|
||||
if (!attachmentItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Zotero.Attachments.shouldAutoRenameAttachment(attachmentItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = await attachmentItem.getFilePathAsync();
|
||||
const ext = path ? Zotero.File.getExtension(path) : attachmentItem.attachmentFilename.split('.').pop();
|
||||
|
||||
let newName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: attachmentItem.getField('title') });
|
||||
let newNameWithExtension = ext.length ? `${newName}.${ext}` : newName;
|
||||
Zotero.debug(`Renaming attachment ${attachmentItem.id} on parent item ${parentItem.id} to ${newName}`);
|
||||
|
||||
if (newNameWithExtension !== attachmentItem.attachmentFilename) {
|
||||
summary.push({
|
||||
attachmentId: attachmentItem.id,
|
||||
parentItemId: parentItem.id,
|
||||
oldName: attachmentItem.attachmentFilename,
|
||||
newName: newNameWithExtension,
|
||||
isFilePresent: !!path
|
||||
});
|
||||
}
|
||||
|
||||
if (!pretend) {
|
||||
if (path) {
|
||||
let out = {};
|
||||
const renamed = await attachmentItem.renameAttachmentFile(newNameWithExtension, { updateTitle: true, out });
|
||||
if (out.noChange) {
|
||||
continue;
|
||||
}
|
||||
if (renamed === true) {
|
||||
count++;
|
||||
}
|
||||
else {
|
||||
Zotero.debug(`Failed to rename attachment ${attachmentItem.id} on parent item ${parentItem.id}`);
|
||||
}
|
||||
}
|
||||
else if (attachmentItem.attachmentFilename !== newName && attachmentItem.isStoredFileAttachment()) {
|
||||
const oldFileName = attachmentItem.attachmentFilename;
|
||||
const oldBaseName = attachmentItem.attachmentFilename.replace(/\.[^.]+$/, '');
|
||||
|
||||
// file is not present locally but we can still update filename in the database
|
||||
attachmentItem.attachmentFilename = newName;
|
||||
|
||||
// update title if it matches the old filename
|
||||
if (attachmentItem.getField('title') === oldBaseName || attachmentItem.getField('title') === oldFileName) {
|
||||
attachmentItem.setAutoAttachmentTitle({ ignoreAutoRenamePrefs: true });
|
||||
}
|
||||
|
||||
await attachmentItem.saveTx();
|
||||
noFilePresentCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const t2 = Date.now();
|
||||
if (!pretend) {
|
||||
Zotero.debug(`Renaming ${count + noFilePresentCount} attachments (${noFilePresentCount} with no file present) in ${librariesWithAttachmentsToRename.length} `
|
||||
+ `libraries took ${((t2 - t1) / 1000).toFixed(2)} seconds `
|
||||
+ `(user library: ${userLibrary}, group libraries: ${groupLibrary})`);
|
||||
Zotero.Prefs.set('autoRenameFiles.done', true);
|
||||
}
|
||||
return summary;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renames an invidual attachment file based on its parent item's metadata.
|
||||
*
|
||||
* @async
|
||||
* @param {Zotero.Item} attachmentItem - The attachment item to be renamed.
|
||||
* @throws {Error} If the item is not a valid attachment for renaming.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function renameFileFromParent(attachmentItem) {
|
||||
if (!attachmentItem.isAttachment() || attachmentItem.isTopLevelItem() || attachmentItem.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
throw new Error('Item ' + attachmentItem.itemID + ' cannot be renamed based on its parent item');
|
||||
}
|
||||
|
||||
const parentItemID = attachmentItem.parentItemID;
|
||||
let parentItem = await Zotero.Items.getAsync(parentItemID);
|
||||
const oldBaseName = attachmentItem.attachmentFilename.replace(/\.[^.]+$/, '');
|
||||
const fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: attachmentItem.getField('title') });
|
||||
const ext = Zotero.Attachments.getCorrectFileExtension(attachmentItem);
|
||||
const newName = fileBaseName + (ext ? '.' + ext : '');
|
||||
|
||||
const renamed = await attachmentItem.renameAttachmentFile(newName, { updateTitle: false, unique: true });
|
||||
let requiresSave = false;
|
||||
if (!renamed && attachmentItem.isStoredFileAttachment()) {
|
||||
// file is not present locally but we can still update filename in the database
|
||||
attachmentItem.attachmentFilename = newName;
|
||||
requiresSave = true;
|
||||
}
|
||||
|
||||
if (attachmentItem.getField('title') === oldBaseName) {
|
||||
attachmentItem.setAutoAttachmentTitle({ ignoreAutoRenamePrefs: true });
|
||||
requiresSave = true;
|
||||
}
|
||||
|
||||
if (requiresSave) {
|
||||
await attachmentItem.saveTx();
|
||||
}
|
||||
};
|
||||
|
||||
export async function canRenameFileFromParent(attachmentItem) {
|
||||
if (!attachmentItem.isAttachment() || attachmentItem.isTopLevelItem() || attachmentItem.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let path = await attachmentItem.getFilePathAsync();
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentItemID = attachmentItem.parentItemID;
|
||||
let parentItem = await Zotero.Items.getAsync(parentItemID);
|
||||
const origFilename = PathUtils.filename(path);
|
||||
const ext = Zotero.File.getExtension(path);
|
||||
let newName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: attachmentItem.getField('title') });
|
||||
|
||||
newName = ext.length ? `${newName}.${ext}` : newName;
|
||||
return newName !== origFilename;
|
||||
};
|
||||
|
||||
|
||||
export function registerAutoRenameFileFromParent() {
|
||||
Zotero.Notifier.registerObserver({
|
||||
notify: async (event, _type, ids, extraData) => {
|
||||
if (!Zotero.Prefs.get('autoRenameFiles.onMetadataChange')) {
|
||||
return;
|
||||
}
|
||||
if (event !== 'modify') {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let id of ids) {
|
||||
if (extraData[id]?.skipRenameFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentItem = await Zotero.Items.getAsync(id);
|
||||
if (!parentItem.isTopLevelItem() || !parentItem.isRegularItem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let attachmentItem = await parentItem.getBestAttachment();
|
||||
|
||||
if (!attachmentItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Zotero.Attachments.shouldAutoRenameAttachment(attachmentItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!extraData?.[id]?.changed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let changes = Object.entries(extraData[id].changed).filter(([key, _value]) => {
|
||||
if (['tags', 'collections'].includes(key)) {
|
||||
return false; // Don't care about tags or collections
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (changes.length === 0) {
|
||||
continue; // No relevant changes
|
||||
}
|
||||
|
||||
let parentItemBefore = parentItem.clone(null, { skipTags: true, includeCollections: false });
|
||||
let validFields = Zotero.ItemFields.getItemTypeFields(parentItem.itemTypeID).map(fieldID => Zotero.ItemFields.getName(fieldID));
|
||||
for (let [key, value] of changes) {
|
||||
if (key === 'itemType') {
|
||||
parentItemBefore.setType(value);
|
||||
}
|
||||
else if (key === 'creators') {
|
||||
parentItemBefore.setCreators(value);
|
||||
}
|
||||
else if (validFields.includes(key)) {
|
||||
parentItemBefore.setField(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let previousMetadataBaseName = Zotero.Attachments.getFileBaseNameFromItem(
|
||||
parentItemBefore, { attachmentTitle: attachmentItem.getField('title') }
|
||||
);
|
||||
let currentBaseName = attachmentItem.attachmentFilename?.replace(/\.[^.]+$/, '') ?? '';
|
||||
|
||||
if (previousMetadataBaseName === currentBaseName) {
|
||||
// Filename appears to be derived from the metadata, so update it to match the latest metadata.
|
||||
renameFileFromParent(attachmentItem);
|
||||
}
|
||||
else {
|
||||
// Filename has most likely been manually changed, so
|
||||
// don’t rename it. Reset `autoRenameFiles.done` so that
|
||||
// "Rename Files Now" appears in Preferences.
|
||||
Zotero.Prefs.set('autoRenameFiles.done', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, ['item'], 'autoRenameFileFromParent', 150); // lower priority than the other item observers
|
||||
}
|
||||
|
||||
export async function openRenameFilesPreview() {
|
||||
Services.ww.openWindow(null, "chrome://zotero/content/renameFilesPreview.xhtml",
|
||||
"renameFilesPreview", "chrome,dialog=yes,centerscreen,modal", null);
|
||||
}
|
||||
|
||||
export async function promptAutoRenameFiles() {
|
||||
let [title, description, yes, no] = await Zotero.getMainWindow().document.l10n.formatValues([
|
||||
'file-renaming-auto-rename-prompt-title',
|
||||
'file-renaming-auto-rename-prompt-body',
|
||||
'file-renaming-auto-rename-prompt-yes',
|
||||
'file-renaming-auto-rename-prompt-no'
|
||||
]);
|
||||
let index = Zotero.Prompt.confirm({
|
||||
title,
|
||||
text: description,
|
||||
button0: yes,
|
||||
button1: no
|
||||
});
|
||||
if (index == 0) {
|
||||
openRenameFilesPreview();
|
||||
}
|
||||
else {
|
||||
Zotero.Prefs.set('autoRenameFiles.done', false);
|
||||
}
|
||||
}
|
||||
151
chrome/content/zotero/renameFilesPreview.js
Normal file
151
chrome/content/zotero/renameFilesPreview.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2025 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
http://zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
import VirtualizedTable from 'components/virtualized-table';
|
||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
const { renameFilesFromParent } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
|
||||
var RenameFilesPreview = { // eslint-disable-line no-unused-vars
|
||||
rows: [],
|
||||
columns: [],
|
||||
introEl: null,
|
||||
filesListEl: null,
|
||||
treeRef: null,
|
||||
loadingEl: null,
|
||||
progressEl: null,
|
||||
acceptBtnEl: null,
|
||||
cancelBtnEl: null,
|
||||
|
||||
init: function () {
|
||||
this.introEl = document.getElementById('intro');
|
||||
this.filesListEl = document.getElementById('renamed-files-list');
|
||||
this.loadingEl = document.getElementById('loading');
|
||||
this.progressEl = document.getElementById('progress');
|
||||
this.progressEl.classList.add('hidden');
|
||||
this.acceptBtnEl = document.querySelector('dialog').getButton('accept');
|
||||
this.cancelBtnEl = document.querySelector('dialog').getButton('cancel');
|
||||
this.acceptBtnEl.disabled = false;
|
||||
this.rowRenderer = VirtualizedTable.makeRowRenderer(this.getRowData.bind(this));
|
||||
document.addEventListener('dialogaccept', this.handleAcceptClick.bind(this));
|
||||
setTimeout(this.pretendRenameItems.bind(this), 0);
|
||||
},
|
||||
|
||||
handleAcceptClick: async function (ev) {
|
||||
ev.preventDefault();
|
||||
this.filesListEl.remove();
|
||||
this.progressEl.classList.remove('hidden');
|
||||
this.introEl.dataset.l10nId = 'rename-files-preview-renaming';
|
||||
this.acceptBtnEl.disabled = true;
|
||||
this.cancelBtnEl.disabled = true;
|
||||
|
||||
await renameFilesFromParent({ reportProgress: this.updateProgress.bind(this) });
|
||||
setTimeout(() => {
|
||||
// remain open for a moment longer so that user can see 100% complete
|
||||
window.close();
|
||||
}, 500);
|
||||
},
|
||||
|
||||
prepareColumns: async function () {
|
||||
return [
|
||||
{ dataKey: 'name', label: '', primary: true, flex: 1 },
|
||||
];
|
||||
},
|
||||
|
||||
pretendRenameItems: async function () {
|
||||
this.columns = await this.prepareColumns();
|
||||
let rows = await renameFilesFromParent({ pretend: true });
|
||||
this.loadingEl.remove();
|
||||
if (rows.length === 0) {
|
||||
this.introEl.dataset.l10nId = 'rename-files-preview-no-files';
|
||||
this.cancelBtnEl.label = await document.l10n.formatValue('general-done');
|
||||
this.acceptBtnEl.remove();
|
||||
// There is nothing that would be renamed; ensure the “Rename Files” button is hidden in the preferences pane.
|
||||
Zotero.Prefs.set('autoRenameFiles.done', true);
|
||||
}
|
||||
else {
|
||||
this.rows = rows.flatMap(obj => [{ name: obj.oldName }, { name: obj.newName }, { type: 'separator' }]);
|
||||
this.acceptBtnEl.disabled = false;
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
updateProgress: async function (progress) {
|
||||
this.progressEl.value = progress;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
let customRowHeights = [];
|
||||
this.rows.forEach((row, index) => {
|
||||
if (row.type === 'separator') {
|
||||
customRowHeights.push([index, 8]);
|
||||
}
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(this.filesListEl).render((
|
||||
<VirtualizedTable
|
||||
columns={this.columns}
|
||||
containerWidth={this.filesListEl.clientWidth}
|
||||
customRowHeights={customRowHeights}
|
||||
disableFontSizeScaling={true}
|
||||
getRowCount={() => this.rows.length}
|
||||
getRowHeight={this.getRowHeight.bind(this)}
|
||||
id="rename-files-confirm-table"
|
||||
isSelectable={this.getIsSelectable.bind(this)}
|
||||
ref={ref => this.treeRef = ref}
|
||||
renderItem={this.renderItem.bind(this)}
|
||||
showHeader={false}
|
||||
/>
|
||||
));
|
||||
},
|
||||
|
||||
renderItem: function (index, selection, oldDiv, ...args) {
|
||||
if (this.rows[index].type === 'separator') {
|
||||
let div = oldDiv || document.createElement('div');
|
||||
div.innerHTML = '';
|
||||
div.className = 'row separator';
|
||||
return div;
|
||||
}
|
||||
else {
|
||||
let div = this.rowRenderer(index, selection, oldDiv, ...args);
|
||||
div.classList.toggle('old', index % 3 === 0);
|
||||
div.classList.toggle('new', index % 3 === 1);
|
||||
return div;
|
||||
}
|
||||
},
|
||||
|
||||
getIsSelectable: function (index) {
|
||||
return this.rows[index].type !== 'separator';
|
||||
},
|
||||
|
||||
getRowData: function (index) {
|
||||
return this.rows[index];
|
||||
},
|
||||
|
||||
getRowHeight: function ({ _renderedTextHeight }) {
|
||||
return _renderedTextHeight;
|
||||
}
|
||||
};
|
||||
60
chrome/content/zotero/renameFilesPreview.xhtml
Normal file
60
chrome/content/zotero/renameFilesPreview.xhtml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0"?>
|
||||
<!--
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2025 Corporation for Digital Scholarship
|
||||
Vienna, Virginia, USA
|
||||
http://zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Zotero is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***** END LICENSE BLOCK *****
|
||||
-->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/overlay.css"?>
|
||||
<?xml-stylesheet href="chrome://zotero-platform/content/zotero.css"?>
|
||||
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
id="rename-files-preview" class="zotero-dialog" drawintitlebar-platforms="mac" onload="RenameFilesPreview.init()">
|
||||
<dialog buttons="cancel,accept" data-l10n-id="rename-files-preview" data-l10n-attrs="buttonlabelaccept">
|
||||
<script>
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/include.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/titlebar.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/customElements.js', this);
|
||||
Services.scriptloader.loadSubScript('chrome://zotero/content/renameFilesPreview.js', this);
|
||||
</script>
|
||||
<linkset>
|
||||
<html:link rel="localization" href="branding/brand.ftl" />
|
||||
<html:link rel="localization" href="zotero.ftl" />
|
||||
<html:link rel="localization" href="preferences.ftl" />
|
||||
</linkset>
|
||||
<vbox>
|
||||
<html:p id="intro" data-l10n-id="rename-files-preview-intro" />
|
||||
</vbox>
|
||||
|
||||
<vbox class="virtualized-table-container">
|
||||
<html:div id="renamed-files-list" class="virtualized-table-wrapper">
|
||||
<html:div class="virtualized-table-loading" id="loading">
|
||||
<html:span data-l10n-id="rename-files-confirmation-loading" />
|
||||
</html:div>
|
||||
</html:div>
|
||||
<html:progress id="progress" max="1" value="0" />
|
||||
</vbox>
|
||||
</dialog>
|
||||
</window>
|
||||
|
|
@ -810,7 +810,7 @@ const ZoteroStandalone = new function () {
|
|||
ZoteroPane.loadURI(ZOTERO_CONFIG.SUPPORT_URL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks for updates
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -581,7 +581,7 @@ Zotero.Attachments = new function () {
|
|||
// Save using remote web browser persist
|
||||
var externalHandlerImport = async function (contentType) {
|
||||
// Rename attachment
|
||||
if (renameIfAllowedType && !fileBaseName && this.isRenameAllowedForType(contentType)) {
|
||||
if (renameIfAllowedType && !fileBaseName && this.isRenameAllowedForType(contentType, libraryID)) {
|
||||
let parentItem = Zotero.Items.get(parentItemID);
|
||||
fileBaseName = this.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
}
|
||||
|
|
@ -2329,7 +2329,7 @@ Zotero.Attachments = new function () {
|
|||
* based on the metadata of the specified item and a format string
|
||||
*
|
||||
* (Optional) |formatString| specifies the format string -- otherwise
|
||||
* the 'attachmentRenameTemplate' pref is used
|
||||
* the 'attachmentRenameTemplate' synced setting for the user's library is used
|
||||
*
|
||||
* @param {Zotero.Item} item
|
||||
* @param {String} formatString
|
||||
|
|
@ -2338,6 +2338,9 @@ Zotero.Attachments = new function () {
|
|||
if (!(item instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
if (!item.libraryID) {
|
||||
throw new Error("Item must have a libraryID");
|
||||
}
|
||||
if (typeof options === 'string') {
|
||||
Zotero.warn("Zotero.Attachments.getFileBaseNameFromItem(item, formatString) is deprecated -- use Zotero.Attachments.getFileBaseNameFromItem(item, options)");
|
||||
options = { formatString: options };
|
||||
|
|
@ -2346,7 +2349,8 @@ Zotero.Attachments = new function () {
|
|||
let { formatString = null, attachmentTitle = '' } = options;
|
||||
|
||||
if (!formatString) {
|
||||
formatString = Zotero.Prefs.get('attachmentRenameTemplate');
|
||||
const { DEFAULT_ATTACHMENT_RENAME_TEMPLATE } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
formatString = Zotero.SyncedSettings.get(item.libraryID, 'attachmentRenameTemplate') ?? DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
|
||||
}
|
||||
|
||||
let chunks = [];
|
||||
|
|
@ -2615,28 +2619,59 @@ Zotero.Attachments = new function () {
|
|||
return ext;
|
||||
};
|
||||
|
||||
this.shouldAutoRenameFile = function (isLink) {
|
||||
if (!Zotero.Prefs.get('autoRenameFiles')) {
|
||||
this.shouldAutoRenameFile = function (isLink, libraryID = null) {
|
||||
if (libraryID === null) {
|
||||
Zotero.debug('Calling Zotero.Attachments.shouldAutoRenameFile without a libraryID is deprecated. Assuming user library.');
|
||||
libraryID = Zotero.Libraries.userLibraryID;
|
||||
}
|
||||
if (libraryID === Zotero.Libraries.userLibraryID && !Zotero.Prefs.get('autoRenameFiles')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (libraryID !== Zotero.Libraries.userLibraryID && !Zotero.SyncedSettings.get(libraryID, 'autoRenameFiles')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isLink) {
|
||||
return Zotero.Prefs.get('autoRenameFiles.linked');
|
||||
// Linked files may only be renamed in the user library, where it's based on the preference (in group libraries, it's always false)
|
||||
return libraryID === Zotero.Libraries.userLibraryID ? Zotero.Prefs.get('autoRenameFiles.linked') : false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
this.isRenameAllowedForType = function (contentType) {
|
||||
this.isRenameAllowedForType = function (contentType, libraryID = null) {
|
||||
let typePrefixes;
|
||||
try {
|
||||
typePrefixes = Zotero.Prefs.get('autoRenameFiles.fileTypes')
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
}
|
||||
catch (e) {
|
||||
typePrefixes = [];
|
||||
if (libraryID === null) {
|
||||
Zotero.debug('Calling Zotero.Attachments.isRenameAllowedForType without a libraryID is deprecated. Assuming user library.');
|
||||
libraryID = Zotero.Libraries.userLibraryID;
|
||||
}
|
||||
|
||||
|
||||
if (libraryID === Zotero.Libraries.userLibraryID) {
|
||||
try {
|
||||
typePrefixes = Zotero.Prefs.get('autoRenameFiles.fileTypes')
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
}
|
||||
catch (e) { // eslint-disable-line no-unused-vars
|
||||
typePrefixes = [];
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
typePrefixes = Zotero.SyncedSettings.get(libraryID, 'autoRenameFilesFileTypes')
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
}
|
||||
catch (e) { // eslint-disable-line no-unused-vars
|
||||
const { DEFAULT_AUTO_RENAME_FILE_TYPES } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
typePrefixes = DEFAULT_AUTO_RENAME_FILE_TYPES
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
return typePrefixes.some(prefix => contentType.startsWith(prefix));
|
||||
};
|
||||
|
||||
|
|
@ -2654,17 +2689,22 @@ Zotero.Attachments = new function () {
|
|||
|
||||
|
||||
this.shouldAutoRenameAttachment = function (attachment) {
|
||||
return Zotero.Attachments.shouldAutoRenameFile(attachment.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE)
|
||||
&& Zotero.Attachments.isRenameAllowedForType(attachment.attachmentContentType);
|
||||
return Zotero.Attachments.shouldAutoRenameFile(attachment.isLinkedFileAttachment(), attachment.libraryID)
|
||||
&& Zotero.Attachments.isRenameAllowedForType(attachment.attachmentContentType, attachment.libraryID)
|
||||
&& !attachment.isSnapshotAttachment();
|
||||
};
|
||||
|
||||
|
||||
// NOTE: This should only be used during attachment item creation, where the
|
||||
// attachment item does not exist yet, because when generating a new
|
||||
// filename, the current file name is used in place of the
|
||||
// `attachmentTitle`.
|
||||
this.getRenamedFileBaseNameIfAllowedType = async function (parentItem, file) {
|
||||
var contentType = file.endsWith('.pdf')
|
||||
// Don't bother reading file if there's a .pdf extension
|
||||
? 'application/pdf'
|
||||
: await Zotero.MIME.getMIMETypeFromFile(file);
|
||||
if (!this.isRenameAllowedForType(contentType)) {
|
||||
if (!this.isRenameAllowedForType(contentType, parentItem.libraryID)) {
|
||||
return false;
|
||||
}
|
||||
return this.getFileBaseNameFromItem(parentItem, { attachmentTitle: PathUtils.filename(file) });
|
||||
|
|
|
|||
|
|
@ -2889,15 +2889,25 @@ Zotero.Item.prototype.fileExistsCached = function () {
|
|||
/**
|
||||
* Rename file associated with an attachment
|
||||
*
|
||||
* @param {String} newName
|
||||
* @param {Boolean} [overwrite=false] - Overwrite file if one exists
|
||||
* @param {Boolean} [unique=false] - Add suffix to create unique filename if necessary
|
||||
* @return {Number|false} -- true - Rename successful
|
||||
* -1 - Destination file exists; use _force_ to overwrite
|
||||
* -2 - Error renaming
|
||||
* false - Attachment file not found
|
||||
* @param {String} newName - The new name for the file
|
||||
* @param {Object} [options={}] - Options for renaming the file
|
||||
* @param {Boolean} [options.overwrite=false] - Overwrite file if one exists
|
||||
* @param {Boolean} [options.unique=false] - Add suffix to create unique filename if necessary
|
||||
* @param {Boolean} [options.updateTitle=false] - Also update the attachment item title if currently matches filename
|
||||
* @param {Object} [options.out={}] - Output object for additional information about the operation
|
||||
* @return {Number|Boolean} - Returns:
|
||||
* - true: Rename successful
|
||||
* - -1: Destination file exists; use _force_ to overwrite
|
||||
* - -2: Error renaming
|
||||
* - false: Attachment file not found
|
||||
*/
|
||||
Zotero.Item.prototype.renameAttachmentFile = async function (newName, overwrite = false, unique = false) {
|
||||
Zotero.Item.prototype.renameAttachmentFile = async function (newName, options = { overwrite: false, unique: false, updateTitle: false, out: {} }, ...rest) {
|
||||
if (typeof options === 'boolean') {
|
||||
Zotero.debug("Zotero.Item.renameAttachmentFile() now takes an options object as a second argument -- update your code", 2);
|
||||
options = { overwrite: options, unique: rest[0], updateTitle: false, out: {} };
|
||||
}
|
||||
let { overwrite, unique, updateTitle, out = {} } = options;
|
||||
|
||||
var origPath = await this.getFilePathAsync();
|
||||
if (!origPath) {
|
||||
Zotero.debug("Attachment file not found in renameAttachmentFile()", 2);
|
||||
|
|
@ -2905,11 +2915,12 @@ Zotero.Item.prototype.renameAttachmentFile = async function (newName, overwrite
|
|||
}
|
||||
|
||||
try {
|
||||
let origName = PathUtils.filename(origPath);
|
||||
let origFilename = PathUtils.filename(origPath);
|
||||
|
||||
// No change
|
||||
if (origName === newName) {
|
||||
if (origFilename === newName) {
|
||||
Zotero.debug("Filename has not changed");
|
||||
out.noChange = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2928,6 +2939,22 @@ Zotero.Item.prototype.renameAttachmentFile = async function (newName, overwrite
|
|||
|
||||
await this.relinkAttachmentFile(destPath);
|
||||
|
||||
if (updateTitle) {
|
||||
// Update title if it matches the old filename
|
||||
const ext = Zotero.File.getExtension(origPath);
|
||||
let origFilenameNoExt = origFilename;
|
||||
if (ext.length && origFilename.endsWith(ext)) {
|
||||
origFilenameNoExt = origFilename.substring(0, origFilename.length - ext.length - 1);
|
||||
}
|
||||
|
||||
let origTitle = this.getField('title');
|
||||
if (origTitle === origFilename || origTitle === origFilenameNoExt) {
|
||||
this.setField('title', newName);
|
||||
out.titleUpdated = true;
|
||||
await this.saveTx();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (e) {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Zotero.Prefs = new function () {
|
|||
|
||||
// Process pref version updates
|
||||
var fromVersion = this.get('prefVersion');
|
||||
var toVersion = 16;
|
||||
var toVersion = 18;
|
||||
if (!fromVersion) {
|
||||
this.set('prefVersion', toVersion);
|
||||
}
|
||||
|
|
@ -191,11 +191,27 @@ Zotero.Prefs = new function () {
|
|||
case 16:
|
||||
this.clear('reopenPanesOnRestart');
|
||||
break;
|
||||
|
||||
case 17: {
|
||||
if (this.get('autoRenameFiles')) {
|
||||
// If the user has `autoRenameFiles` enabled, show a banner informing that file names are now kept in sync
|
||||
this.set('autoRenameFiles.bannerShown', false);
|
||||
}
|
||||
let attachmentRenameTemplate = this.get('attachmentRenameTemplate');
|
||||
if (this.prefHasUserValue('attachmentRenameTemplate')) {
|
||||
// If the user has a custom template, reset `autoRenameFiles.done` so that the "Rename Files Now" button appears in preferences
|
||||
Zotero.initializationPromise.then(() => {
|
||||
Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate', attachmentRenameTemplate);
|
||||
this.set('autoRenameFiles.done', false);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.set('prefVersion', toVersion);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ Zotero.RecognizeDocument = new function () {
|
|||
let fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: originalTitle });
|
||||
let ext = Zotero.Attachments.getCorrectFileExtension(attachment);
|
||||
let newName = fileBaseName + (ext ? '.' + ext : '');
|
||||
let result = await attachment.renameAttachmentFile(newName, false, true);
|
||||
let result = await attachment.renameAttachmentFile(newName, { overwrite: false, unique: true });
|
||||
if (result !== true) {
|
||||
throw new Error("Error renaming " + path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1494,6 +1494,22 @@ Zotero.Sync.Data.Local = {
|
|||
|
||||
_saveObjectFromJSON: async function (obj, json, options) {
|
||||
var results = {};
|
||||
var saveOptions = {
|
||||
skipEditCheck: true,
|
||||
skipDateModifiedUpdate: true,
|
||||
skipSelect: true,
|
||||
skipCache: options.skipCache || false,
|
||||
notifierQueue: options.notifierQueue,
|
||||
// Errors are logged elsewhere, so skip in DataObject.save()
|
||||
errorHandler: function (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if (obj.objectType == 'item') {
|
||||
saveOptions.notifierData = {
|
||||
skipRenameFile: true
|
||||
};
|
||||
}
|
||||
try {
|
||||
results.key = json.key;
|
||||
json = this._checkCacheJSON(json);
|
||||
|
|
@ -1523,17 +1539,7 @@ Zotero.Sync.Data.Local = {
|
|||
if (!options.saveAsUnsynced) {
|
||||
obj.synced = true;
|
||||
}
|
||||
await obj.save({
|
||||
skipEditCheck: true,
|
||||
skipDateModifiedUpdate: true,
|
||||
skipSelect: true,
|
||||
skipCache: options.skipCache || false,
|
||||
notifierQueue: options.notifierQueue,
|
||||
// Errors are logged elsewhere, so skip in DataObject.save()
|
||||
errorHandler: function (e) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
await obj.save(saveOptions);
|
||||
let cacheJSON = options.cacheObject ? options.cacheObject : json.data;
|
||||
await this.saveCacheObject(obj.objectType, obj.libraryID, cacheJSON);
|
||||
// Delete older versions of the object in the cache
|
||||
|
|
@ -1575,16 +1581,7 @@ Zotero.Sync.Data.Local = {
|
|||
for (let c of options.newParentItemCollections) {
|
||||
parentItem.addToCollection(c);
|
||||
}
|
||||
await parentItem.save({
|
||||
skipEditCheck: true,
|
||||
skipDateModifiedUpdate: true,
|
||||
skipSelect: true,
|
||||
notifierQueue: options.notifierQueue,
|
||||
// Errors are logged elsewhere, so skip in DataObject.save()
|
||||
errorHandler: function (e) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
await parentItem.save(saveOptions);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
|
|
|
|||
|
|
@ -700,6 +700,11 @@ const { CommandLineOptions } = ChromeUtils.importESModule("chrome://zotero/conte
|
|||
|
||||
Zotero.Notifier.registerObserver(Zotero.Tags, 'setting', 'tags');
|
||||
|
||||
const { registerAutoRenameFileFromParent } = ChromeUtils.importESModule(
|
||||
"chrome://zotero/content/renameFiles.mjs"
|
||||
);
|
||||
registerAutoRenameFileFromParent();
|
||||
|
||||
await Zotero.Sync.Data.Local.init();
|
||||
await Zotero.Sync.Data.Utilities.init();
|
||||
Zotero.Sync.Storage.Local.init();
|
||||
|
|
|
|||
|
|
@ -638,6 +638,7 @@ var ZoteroPane = new function () {
|
|||
ZoteroPane.showPostUpgradeBanner();
|
||||
ZoteroPane.showRetractionBanner();
|
||||
ZoteroPane.showArchitectureWarning();
|
||||
ZoteroPane.showFileRenamingBanner();
|
||||
ZoteroPane.initSyncReminders(true);
|
||||
});
|
||||
|
||||
|
|
@ -2942,7 +2943,6 @@ var ZoteroPane = new function () {
|
|||
'syncReminder');
|
||||
};
|
||||
|
||||
|
||||
this.showSetUpSyncReminder = function () {
|
||||
const sevenDays = 60 * 60 * 24 * 7;
|
||||
|
||||
|
|
@ -3698,7 +3698,6 @@ var ZoteroPane = new function () {
|
|||
'recognizePDF',
|
||||
'unrecognize',
|
||||
'createParent',
|
||||
'renameAttachments',
|
||||
'reindexItem',
|
||||
];
|
||||
|
||||
|
|
@ -3755,8 +3754,7 @@ var ZoteroPane = new function () {
|
|||
showRelate = true, canRelate = true,
|
||||
canIndex = true,
|
||||
canRecognize = true,
|
||||
canUnrecognize = true,
|
||||
canRename = true;
|
||||
canUnrecognize = true;
|
||||
var canMarkRead = collectionTreeRow.isFeedsOrFeed();
|
||||
var markUnread = true;
|
||||
|
||||
|
|
@ -3786,12 +3784,7 @@ var ZoteroPane = new function () {
|
|||
canUnrecognize = false;
|
||||
}
|
||||
|
||||
// Show rename option only if all items are child attachments
|
||||
if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
|
||||
canRename = false;
|
||||
}
|
||||
|
||||
if(canMarkRead && markUnread && !item.isRead) {
|
||||
if (canMarkRead && markUnread && !item.isRead) {
|
||||
markUnread = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -3872,13 +3865,10 @@ var ZoteroPane = new function () {
|
|||
if (canCreateParent) {
|
||||
show.add(m.createParent);
|
||||
}
|
||||
|
||||
if (canRename) {
|
||||
show.add(m.renameAttachments);
|
||||
}
|
||||
|
||||
|
||||
// Add in attachment separator
|
||||
if (canCreateParent || canRecognize || canUnrecognize || canRename || canIndex) {
|
||||
if (canCreateParent || canRecognize || canUnrecognize || canIndex) {
|
||||
show.add(m.sep5);
|
||||
}
|
||||
|
||||
|
|
@ -3889,7 +3879,6 @@ var ZoteroPane = new function () {
|
|||
if (item.isFileAttachment()) {
|
||||
disable.add(m.moveToTrash);
|
||||
disable.add(m.createParent);
|
||||
disable.add(m.renameAttachments);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -3971,12 +3960,6 @@ var ZoteroPane = new function () {
|
|||
showSep5 = true;
|
||||
}
|
||||
|
||||
// Attachment rename option
|
||||
if (!item.isTopLevelItem() && item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
show.add(m.renameAttachments);
|
||||
showSep5 = true;
|
||||
}
|
||||
|
||||
// If not linked URL, show reindex line
|
||||
if (await Zotero.Fulltext.canReindex(item)) {
|
||||
show.add(m.reindexItem);
|
||||
|
|
@ -4026,7 +4009,7 @@ var ZoteroPane = new function () {
|
|||
|
||||
// Block certain actions on files if no access
|
||||
if (item.isFileAttachment() && !collectionTreeRow.filesEditable) {
|
||||
[m.moveToTrash, m.createParent, m.renameAttachments]
|
||||
[m.moveToTrash, m.createParent]
|
||||
.forEach(function (x) {
|
||||
disable.add(x);
|
||||
});
|
||||
|
|
@ -4167,7 +4150,6 @@ var ZoteroPane = new function () {
|
|||
menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.items.menu.generateReport' + multiple));
|
||||
menu.childNodes[m.createParent].setAttribute('label', Zotero.getString('pane.items.menu.createParent' + multiple));
|
||||
menu.childNodes[m.recognizePDF].setAttribute('label', Zotero.getString('pane.items.menu.recognizeDocument'));
|
||||
menu.childNodes[m.renameAttachments].setAttribute('label', Zotero.getString('pane.items.menu.renameAttachments' + multiple));
|
||||
menu.childNodes[m.reindexItem].setAttribute('label', Zotero.getString('pane.items.menu.reindexItem' + multiple));
|
||||
|
||||
// Hide and enable all actions by default (so if they're shown they're enabled)
|
||||
|
|
@ -4749,7 +4731,7 @@ var ZoteroPane = new function () {
|
|||
// If only one item is being added, automatic renaming is enabled, and the parent item
|
||||
// doesn't have any other non-HTML file attachments, rename the file.
|
||||
// This should be kept in sync with itemTreeView::drop().
|
||||
if (files.length == 1 && Zotero.Attachments.shouldAutoRenameFile(link)) {
|
||||
if (files.length == 1 && Zotero.Attachments.shouldAutoRenameFile(link, libraryID)) {
|
||||
let parentItem = Zotero.Items.get(parentItemID);
|
||||
if (!parentItem.numNonHTMLFileAttachments()) {
|
||||
fileBaseName = await Zotero.Attachments.getRenamedFileBaseNameIfAllowedType(
|
||||
|
|
@ -5874,7 +5856,7 @@ var ZoteroPane = new function () {
|
|||
let fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(item.parentItem, { attachmentTitle: item.getField('title') });
|
||||
let ext = Zotero.Attachments.getCorrectFileExtension(item);
|
||||
let newName = fileBaseName + (ext ? '.' + ext : '');
|
||||
let result = await item.renameAttachmentFile(newName, false, true);
|
||||
let result = await item.renameAttachmentFile(newName, { overwrite: false, unique: true });
|
||||
if (result !== true) {
|
||||
throw new Error('Error renaming ' + path);
|
||||
}
|
||||
|
|
@ -6185,59 +6167,6 @@ var ZoteroPane = new function () {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
this.renameSelectedAttachmentsFromParents = async function () {
|
||||
// TEMP: fix
|
||||
|
||||
if (!this.canEdit()) {
|
||||
this.displayCannotEditLibraryMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
var items = this.getSelectedItems();
|
||||
if (!items.length) return;
|
||||
|
||||
var progressWin = new Zotero.ProgressWindow();
|
||||
|
||||
for (var i=0; i<items.length; i++) {
|
||||
var item = items[i];
|
||||
|
||||
if (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
throw('Item ' + itemID + ' is not a child file attachment in ZoteroPane_Local.renameAttachmentFromParent()');
|
||||
}
|
||||
|
||||
var file = await item.getFilePathAsync();
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parentItemID = item.parentItemID;
|
||||
let parentItem = await Zotero.Items.getAsync(parentItemID);
|
||||
var oldBaseName = item.attachmentFilename.replace(/\.[^.]+$/, '');
|
||||
var fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: item.getField('title') });
|
||||
let ext = Zotero.Attachments.getCorrectFileExtension(item);
|
||||
let newName = fileBaseName + (ext ? '.' + ext : '');
|
||||
|
||||
var renamed = await item.renameAttachmentFile(newName, false, true);
|
||||
if (renamed !== true) {
|
||||
Zotero.debug("Could not rename file (" + renamed + ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.getField('title') === oldBaseName) {
|
||||
item.setAutoAttachmentTitle({ ignoreAutoRenamePrefs: true });
|
||||
await item.saveTx();
|
||||
}
|
||||
|
||||
let str = await document.l10n.formatValue('file-renaming-file-renamed-to', { filename: newName });
|
||||
progressWin.addLines(str, item.getItemTypeIconName());
|
||||
progressWin.show();
|
||||
}
|
||||
|
||||
progressWin.startCloseTimer(4000);
|
||||
};
|
||||
|
||||
|
||||
this.convertLinkedFilesToStoredFiles = async function () {
|
||||
if (!this.canEdit() || !this.canEditFiles()) {
|
||||
this.displayCannotEditLibraryMessage();
|
||||
|
|
@ -6777,6 +6706,32 @@ var ZoteroPane = new function () {
|
|||
}
|
||||
};
|
||||
|
||||
this.showFileRenamingBanner = function () {
|
||||
if (Zotero.Prefs.get('autoRenameFiles.bannerShown')) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('file-renaming-documentation-link').onclick = () => {
|
||||
Zotero.launchURL("https://www.zotero.org/support/file_renaming");
|
||||
};
|
||||
|
||||
document.getElementById('file-renaming-settings-link').onclick = () => {
|
||||
Zotero.Utilities.Internal.openPreferences('zotero-prefpane-general', {
|
||||
scrollTo: '#zotero-prefpane-file-renaming-groupbox'
|
||||
});
|
||||
};
|
||||
|
||||
this.document.getElementById('file-renaming-banner-close').onclick = () => {
|
||||
this.hideFileRenamingBanner();
|
||||
};
|
||||
|
||||
this.document.getElementById('file-renaming-banner-container').removeAttribute('collapsed');
|
||||
};
|
||||
|
||||
this.hideFileRenamingBanner = function () {
|
||||
document.getElementById('file-renaming-banner-container').setAttribute('collapsed', true);
|
||||
Zotero.Prefs.set('autoRenameFiles.bannerShown', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the layout to either a three-vertical-pane layout and a layout where itemsPane is above itemPane
|
||||
|
|
|
|||
|
|
@ -991,7 +991,6 @@
|
|||
<menuitem class="menuitem-iconic zotero-menuitem-retrieve-metadata" oncommand="ZoteroPane.recognizeSelected();"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-unrecognize" label="&zotero.items.menu.unrecognize;" oncommand="ZoteroPane.unrecognizeSelected()"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-create-parent" oncommand="ZoteroPane_Local.createParentItemsFromSelected();"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-rename-from-parent" oncommand="ZoteroPane_Local.renameSelectedAttachmentsFromParents()"/>
|
||||
<menuitem class="menuitem-iconic zotero-menuitem-reindex" oncommand="ZoteroPane_Local.reindexItem();"/>
|
||||
</menupopup>
|
||||
|
||||
|
|
@ -1158,6 +1157,15 @@
|
|||
</html:div>
|
||||
</vbox>
|
||||
|
||||
<vbox id="file-renaming-banner-container" class="banner-container" collapsed="true" role="status">
|
||||
<html:div id="file-renaming-banner" class="banner">
|
||||
<html:div id="file-renaming-message" class="message" data-l10n-id="file-renaming-banner-message" />
|
||||
<html:a id="file-renaming-documentation-link" data-l10n-id="file-renaming-banner-documentation-link" class="link" />
|
||||
<html:a id="file-renaming-settings-link" data-l10n-id="file-renaming-banner-settings-link" class="link" />
|
||||
<html:div class="spacer" />
|
||||
<label is="text-link" id="file-renaming-banner-close" class="close-link">×</label>
|
||||
</html:div>
|
||||
</vbox>
|
||||
|
||||
<hbox id="zotero-trees" flex="1">
|
||||
<vbox id="zotero-collections-pane" zotero-persist="width">
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ preferences-auto-recognize-files =
|
|||
.label = Automatically retrieve metadata for PDFs and ebooks
|
||||
|
||||
preferences-file-renaming-title = File Renaming
|
||||
preferences-file-renaming-intro = { -app-name } automatically renames downloaded files based on the details of the parent item (title, author, etc.). You can choose to rename files added from your computer as well.
|
||||
preferences-file-renaming-intro =
|
||||
{ -app-name } can automatically rename files based on the details of the parent item (title, author, etc.) and keep the filenames in sync as you make changes. Downloaded files are always initially named based on the parent item.
|
||||
preferences-file-renaming-auto-rename-files =
|
||||
.label = Automatically rename locally added files
|
||||
.label = Automatically rename files
|
||||
preferences-file-renaming-file-types = Rename files of these types:
|
||||
preferences-file-renaming-file-type-pdf =
|
||||
.label = { file-type-pdf }
|
||||
|
|
@ -23,6 +24,8 @@ preferences-file-renaming-file-type-video =
|
|||
.label = { file-type-video }
|
||||
preferences-file-renaming-customize-button =
|
||||
.label = Customize Filename Format…
|
||||
preferences-file-renaming-rename-now =
|
||||
.label = Rename Files…
|
||||
|
||||
preferences-file-renaming-format-title = Filename Format
|
||||
preferences-file-renaming-format-instructions = You can customize the filename pattern { -app-name } uses to rename attachment files from parent metadata.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ general-remind-me-later = Remind Me Later
|
|||
general-dont-ask-again = Don’t Ask Again
|
||||
general-choose-file = Choose File…
|
||||
general-open-settings = Open Settings
|
||||
general-settings = Settings…
|
||||
general-help = Help
|
||||
general-tag = Tag
|
||||
general-done = Done
|
||||
|
|
@ -56,6 +57,7 @@ general-and = and
|
|||
general-et-al = et al.
|
||||
general-previous = Previous
|
||||
general-next = Next
|
||||
general-learn-more = Learn More
|
||||
|
||||
general-red = Red
|
||||
general-orange = Orange
|
||||
|
|
@ -637,6 +639,21 @@ attachment-info-convert-note =
|
|||
.tooltiptext = Adding notes to attachments is no longer supported, but you can edit this note by migrating it to a separate note.
|
||||
|
||||
attachment-preview-placeholder = No attachment to preview
|
||||
attachment-rename-from-parent =
|
||||
.tooltiptext = Rename File to Match Parent Item
|
||||
|
||||
|
||||
file-renaming-auto-rename-prompt-title = Renaming Settings Changed
|
||||
file-renaming-auto-rename-prompt-body = Would you like to rename existing files in your library to match the new settings?
|
||||
file-renaming-auto-rename-prompt-yes = Preview Changes…
|
||||
file-renaming-auto-rename-prompt-no = Keep Existing Filenames
|
||||
|
||||
rename-files-preview =
|
||||
.buttonlabelaccept = Rename Files
|
||||
rename-files-preview-loading = Loading…
|
||||
rename-files-preview-intro = { -app-name } will rename the following files in your library to match their parent items:
|
||||
rename-files-preview-renaming = Renaming…
|
||||
rename-files-preview-no-files = All filenames already match parent items. No changes are required.
|
||||
|
||||
toggle-preview =
|
||||
.label = {
|
||||
|
|
@ -776,6 +793,10 @@ mac-word-plugin-install-remind-later-button =
|
|||
mac-word-plugin-install-dont-ask-again-button =
|
||||
.label = { general-dont-ask-again }
|
||||
|
||||
file-renaming-banner-message = { -app-name } now automatically keeps attachment filenames in sync as you make changes to items.
|
||||
file-renaming-banner-documentation-link = { general-learn-more }
|
||||
file-renaming-banner-settings-link = { general-settings }
|
||||
|
||||
connector-version-warning = The { -app-name } Connector must be updated to work with this version of { -app-name }.
|
||||
|
||||
userjs-pref-warning = Some { -app-name } settings have been overridden using an unsupported method. { -app-name } will revert them and restart.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1346_124)">
|
||||
<path d="M15.9719 12.0001C15.7231 14.2499 13.8164 16 11.5002 16.0001C10.0851 16.0001 8.82487 15.3449 8.00024 14.3234V16.0001H7.00024V13.0001H10.0002V14.0001H9.05493C9.68563 14.6178 10.5476 15.0001 11.5002 15.0001C13.2633 15 14.7214 13.6961 14.9641 12.0001H15.9719ZM16.0002 10.0001H13.0002V9.00012H13.9456C13.3148 8.38239 12.4529 8.00018 11.5002 8.00012C9.73704 8.00012 8.27903 9.30402 8.03638 11.0001H7.02856C7.27733 8.75019 9.184 7.00012 11.5002 7.00012C12.9155 7.00019 14.1756 7.65524 15.0002 8.67688V7.00012H16.0002V10.0001ZM14.0002 6.60071C13.6835 6.43875 13.3489 6.30676 13.0002 6.20813V4.00012H1.00024V8.00012H7.25806C7.00454 8.30705 6.78467 8.64257 6.60181 9.00012H0.000244141V3.00012H14.0002V6.60071ZM3.00024 7.00012H2.00024V5.00012H3.00024V7.00012Z" fill="context-fill"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1346_124">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1 KiB |
|
|
@ -35,7 +35,9 @@ pref("extensions.zotero.autoRecognizeFiles", true);
|
|||
pref("extensions.zotero.autoRenameFiles", true);
|
||||
pref("extensions.zotero.autoRenameFiles.linked", false);
|
||||
pref("extensions.zotero.autoRenameFiles.fileTypes", "application/pdf,application/epub+zip");
|
||||
pref("extensions.zotero.attachmentRenameTemplate", "{{ firstCreator suffix=\" - \" }}{{ year suffix=\" - \" }}{{ title truncate=\"100\" }}");
|
||||
pref("extensions.zotero.autoRenameFiles.onMetadataChange", true);
|
||||
pref("extensions.zotero.autoRenameFiles.done", true);
|
||||
pref("extensions.zotero.autoRenameFiles.bannerShown", true);
|
||||
pref("extensions.zotero.capitalizeTitles", false);
|
||||
pref("extensions.zotero.launchNonNativeFiles", false);
|
||||
pref("extensions.zotero.naturalSorting", true);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
@import "components/notesList";
|
||||
@import "components/progressMeter";
|
||||
@import "components/publications-dialog";
|
||||
@import "components/renameFilesPreview";
|
||||
@import "components/richlistbox";
|
||||
@import "components/rtfScan";
|
||||
@import "components/runJS";
|
||||
|
|
|
|||
|
|
@ -219,7 +219,6 @@
|
|||
// needed to have the outline appear on all platforms
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
align-self: center;
|
||||
// Make all buttons tigher to not stretch the rows
|
||||
height: auto;
|
||||
width: auto;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
.banner-container > .banner {
|
||||
@media (prefers-color-scheme: light) {
|
||||
--banner-link-active: #4b4b4b;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--banner-link-active: #b4b4b4;
|
||||
}
|
||||
|
||||
@include macOS-normalize-controls;
|
||||
|
||||
display: flex;
|
||||
|
|
@ -42,7 +50,7 @@
|
|||
|
||||
.link {
|
||||
&:active {
|
||||
color: #4b4b4b;
|
||||
color: var(--banner-link-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +130,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
#mac-word-plugin-install-banner {
|
||||
#mac-word-plugin-install-banner,
|
||||
#file-renaming-banner-container {
|
||||
background: var(--accent-blue30);
|
||||
color: var(--fill-primary);
|
||||
font-weight: normal;
|
||||
|
|
|
|||
47
scss/components/_renameFilesPreview.scss
Normal file
47
scss/components/_renameFilesPreview.scss
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#rename-files-preview {
|
||||
min-width: 800px;
|
||||
min-height: 500px;
|
||||
|
||||
dialog {
|
||||
background-color: var(--color-sidepane);
|
||||
padding: 14px 24px;
|
||||
}
|
||||
|
||||
.virtualized-table-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: var(--material-panedivider);
|
||||
|
||||
.virtualized-table-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtualized-table-loading {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.virtualized-table {
|
||||
overflow: hidden;
|
||||
|
||||
.row {
|
||||
&.new:not(.selected) {
|
||||
color: var(--fill-primary);
|
||||
}
|
||||
|
||||
&.old:not(.selected) {
|
||||
color: var(--fill-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,17 @@ attachment-box {
|
|||
color: var(--fill-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
#rename-from-parent {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
@include svgicon-menu("rename-from-parent", "universal", "16");
|
||||
&:not([disabled='true']) {
|
||||
color: var(--fill-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#url
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
#file-renaming-customize-button {
|
||||
#file-renaming-buttons {
|
||||
margin-top: .6em;
|
||||
|
||||
> button + button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
#zotero-prefpane-file-renaming-format label:not([is=zotero-text-link]) {
|
||||
|
|
@ -17,3 +21,13 @@
|
|||
#file-renaming-format-preview {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
#file-renaming-format-preview-buttons {
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.5em;
|
||||
margin-inline: 6px 5px;
|
||||
}
|
||||
|
||||
#file-renaming-rename-now {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ async function waitForDOMAttributes(target, attributes, callback) {
|
|||
for (let mutation of mutations) {
|
||||
if (mutation.type === 'attributes') {
|
||||
let oldValue = mutation.oldValue;
|
||||
let newValue = mutation.target.value;
|
||||
let newValue = mutation.target.getAttribute(mutation.attributeName);
|
||||
if (callback(newValue, oldValue)) {
|
||||
observer.disconnect();
|
||||
deferred.resolve();
|
||||
|
|
@ -360,7 +360,7 @@ function waitForItemEvent(event) {
|
|||
/**
|
||||
* Wait for a single notifier event and return a promise for the data
|
||||
*
|
||||
* Tests run after all other handlers (priority 101, since handlers are 100 by default)
|
||||
* Tests run after all other handlers (priority 200, since handlers are 100 by default, file renaming is 150)
|
||||
*/
|
||||
function waitForNotifierEvent(event, type) {
|
||||
if (!event) throw new Error("event not provided");
|
||||
|
|
@ -374,7 +374,7 @@ function waitForNotifierEvent(event, type) {
|
|||
extraData: extraData
|
||||
});
|
||||
}
|
||||
}}, [type], 'test', 101);
|
||||
}}, [type], 'test', 200);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1431,7 +1431,7 @@ describe("Zotero.Attachments", function () {
|
|||
itemNoRepeatedHyphens, itemNoRepeatedUnderscores, itemLowerCase, itemMixedCase, itemUnicode;
|
||||
|
||||
before(() => {
|
||||
item = createUnsavedDataObject('item', { title: 'Lorem Ipsum', itemType: 'journalArticle' });
|
||||
item = createUnsavedDataObject('item', { title: 'Lorem Ipsum', itemType: 'journalArticle', libraryID: 1 });
|
||||
item.setCreators([
|
||||
{ firstName: 'Foocius', lastName: 'Barius', creatorType: 'author' },
|
||||
{ firstName: 'Bazius', lastName: 'Pixelus', creatorType: 'author' }
|
||||
|
|
@ -1442,10 +1442,10 @@ describe("Zotero.Attachments", function () {
|
|||
item.setField('issue', '42');
|
||||
item.setField('pages', '321');
|
||||
|
||||
itemBookSection = createUnsavedDataObject('item', { title: 'Book Section', itemType: 'bookSection' });
|
||||
itemBookSection = createUnsavedDataObject('item', { title: 'Book Section', itemType: 'bookSection', libraryID: 1 });
|
||||
itemBookSection.setField('bookTitle', 'Book Title');
|
||||
|
||||
itemManyAuthors = createUnsavedDataObject('item', { title: 'Has Many Authors', itemType: 'book' });
|
||||
itemManyAuthors = createUnsavedDataObject('item', { title: 'Has Many Authors', itemType: 'book', libraryID: 1 });
|
||||
itemManyAuthors.setCreators([
|
||||
{ firstName: 'First', lastName: 'Author', creatorType: 'author' },
|
||||
{ firstName: 'Second', lastName: 'Creator', creatorType: 'author' },
|
||||
|
|
@ -1459,7 +1459,7 @@ describe("Zotero.Attachments", function () {
|
|||
itemManyAuthors.setField('publisher', 'Awesome House');
|
||||
itemManyAuthors.setField('volume', '3');
|
||||
|
||||
itemPatent = createUnsavedDataObject('item', { title: 'Retroencabulator', itemType: 'patent' });
|
||||
itemPatent = createUnsavedDataObject('item', { title: 'Retroencabulator', itemType: 'patent', libraryID: 1 });
|
||||
itemPatent.setCreators([
|
||||
{ name: 'AcmeCorp', creatorType: 'inventor' },
|
||||
{ firstName: 'Wile', lastName: 'E', creatorType: 'contributor' },
|
||||
|
|
@ -1468,26 +1468,27 @@ describe("Zotero.Attachments", function () {
|
|||
itemPatent.setField('date', '1952-05-10');
|
||||
itemPatent.setField('number', 'HBK-8539b');
|
||||
itemPatent.setField('assignee', 'Fast FooBar');
|
||||
itemIncomplete = createUnsavedDataObject('item', { title: 'Incomplete', itemType: 'preprint' });
|
||||
itemIncomplete = createUnsavedDataObject('item', { title: 'Incomplete', itemType: 'preprint', libraryID: 1 });
|
||||
|
||||
itemSpaces = createUnsavedDataObject('item', { title: ' Spaces! ', itemType: 'book' });
|
||||
itemSuffixes = createUnsavedDataObject('item', { title: '-Suffixes-', itemType: 'book' });
|
||||
itemSpaces = createUnsavedDataObject('item', { title: ' Spaces! ', itemType: 'book', libraryID: 1 });
|
||||
itemSuffixes = createUnsavedDataObject('item', { title: '-Suffixes-', itemType: 'book', libraryID: 1 });
|
||||
itemSuffixes.setField('date', "1999-07-15");
|
||||
itemKeepHyphens = createUnsavedDataObject('item', { title: 'keep--hyphens', itemType: 'journalArticle' });
|
||||
itemKeepHyphens = createUnsavedDataObject('item', { title: 'keep--hyphens', itemType: 'journalArticle', libraryID: 1 });
|
||||
itemKeepHyphens.setField('publicationTitle', "keep");
|
||||
itemKeepHyphens.setField('issue', 'hyphens');
|
||||
itemKeepHyphens.setField('date', "1999-07-15");
|
||||
itemNoRepeatedHyphens = createUnsavedDataObject('item', { title: 'no - repeated - hyphens', itemType: 'journalArticle' });
|
||||
itemNoRepeatedHyphens = createUnsavedDataObject('item', { title: 'no - repeated - hyphens', itemType: 'journalArticle', libraryID: 1 });
|
||||
itemNoRepeatedHyphens.setField('publicationTitle', "no- repeated- hyphens");
|
||||
itemNoRepeatedUnderscores = createUnsavedDataObject('item', { title: 'no _ repeated _ underscores', itemType: 'journalArticle' });
|
||||
itemNoRepeatedUnderscores = createUnsavedDataObject('item', { title: 'no _ repeated _ underscores', itemType: 'journalArticle', libraryID: 1 });
|
||||
itemNoRepeatedUnderscores.setField('publicationTitle', "no_ repeated_ underscores");
|
||||
itemLowerCase = createUnsavedDataObject('item', { title: 'lower case title', itemType: 'journalArticle' });
|
||||
itemMixedCase = createUnsavedDataObject('item', { title: 'Old MacDonald Had a Farm', itemType: 'journalArticle' });
|
||||
itemUnicode = createUnsavedDataObject('item', { title: '金毛猎犬 - Golden Retriever', itemType: 'journalArticle' });
|
||||
itemLowerCase = createUnsavedDataObject('item', { title: 'lower case title', itemType: 'journalArticle', libraryID: 1 });
|
||||
itemMixedCase = createUnsavedDataObject('item', { title: 'Old MacDonald Had a Farm', itemType: 'journalArticle', libraryID: 1 });
|
||||
itemUnicode = createUnsavedDataObject('item', { title: '金毛猎犬 - Golden Retriever', itemType: 'journalArticle', libraryID: 1 });
|
||||
});
|
||||
|
||||
it('should strip HTML tags from title', function () {
|
||||
var htmlItem = createUnsavedDataObject('item', { title: 'Foo <i>Bar</i> Foo<br><br/><br />Bar' });
|
||||
htmlItem.libraryID = 1;
|
||||
var str = Zotero.Attachments.getFileBaseNameFromItem(htmlItem, { formatString: '{{ title }}' });
|
||||
assert.equal(str, 'Foo Bar Foo Bar');
|
||||
});
|
||||
|
|
@ -1978,7 +1979,7 @@ describe("Zotero.Attachments", function () {
|
|||
|
||||
it("should strip bidi isolates from firstCreator", async function () {
|
||||
var item = createUnsavedDataObject('item',
|
||||
{ creators: [{ name: 'Foo', creatorType: 'author' }, { name: 'Bar', creatorType: 'author' }] });
|
||||
{ creators: [{ name: 'Foo', creatorType: 'author' }, { name: 'Bar', creatorType: 'author' }], libraryID: 1 });
|
||||
var str = Zotero.Attachments.getFileBaseNameFromItem(item);
|
||||
assert.equal(str, Zotero.getString('general.andJoiner', ['Foo', 'Bar']) + ' - ');
|
||||
});
|
||||
|
|
@ -2178,4 +2179,145 @@ describe("Zotero.Attachments", function () {
|
|||
assert.isTrue(await newAttachment.fileExists());
|
||||
});
|
||||
});
|
||||
|
||||
describe("#renameFile()", function () {
|
||||
let { renameFileFromParent } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
|
||||
|
||||
it("should rename a linked file", async function () {
|
||||
var oldFilename = 'old.png';
|
||||
var newFilename = 'Test.png';
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var tmpDir = await getTempDirectory();
|
||||
var oldFile = OS.Path.join(tmpDir, oldFilename);
|
||||
await OS.File.copy(file.path, oldFile);
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Test');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: oldFile,
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
await renameFileFromParent(attachment);
|
||||
|
||||
assert.equal(attachment.attachmentFilename, newFilename);
|
||||
var path = await attachment.getFilePathAsync();
|
||||
assert.equal(OS.Path.basename(path), newFilename);
|
||||
await OS.File.exists(path);
|
||||
});
|
||||
|
||||
it("should use unique name for linked file if target name is taken", async function () {
|
||||
var oldFilename = 'old.png';
|
||||
var newFilename = 'Test.png';
|
||||
var uniqueFilename = 'Test 2.png';
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var tmpDir = await getTempDirectory();
|
||||
var oldFile = OS.Path.join(tmpDir, oldFilename);
|
||||
await OS.File.copy(file.path, oldFile);
|
||||
// Create file with target filename
|
||||
await Zotero.File.putContentsAsync(OS.Path.join(tmpDir, newFilename), '');
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Test');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: oldFile,
|
||||
parentItemID: item.id
|
||||
});
|
||||
await renameFileFromParent(attachment);
|
||||
|
||||
assert.equal(attachment.attachmentFilename, uniqueFilename);
|
||||
var path = await attachment.getFilePathAsync();
|
||||
assert.equal(OS.Path.basename(path), uniqueFilename)
|
||||
await OS.File.exists(path);
|
||||
});
|
||||
|
||||
it("should use unique name for linked file without extension if target name is taken", async function () {
|
||||
var oldFilename = 'old';
|
||||
var newFilename = 'Test.png';
|
||||
var uniqueFilename = 'Test 2.png';
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var tmpDir = await getTempDirectory();
|
||||
var oldFile = OS.Path.join(tmpDir, oldFilename);
|
||||
await OS.File.copy(file.path, oldFile);
|
||||
// Create file with target filename
|
||||
await Zotero.File.putContentsAsync(OS.Path.join(tmpDir, newFilename), '');
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Test');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: oldFile,
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
await renameFileFromParent(attachment);
|
||||
|
||||
assert.equal(attachment.attachmentFilename, uniqueFilename);
|
||||
var path = await attachment.getFilePathAsync();
|
||||
assert.equal(OS.Path.basename(path), uniqueFilename);
|
||||
await OS.File.exists(path);
|
||||
});
|
||||
|
||||
it("shouldn't change attachment title if different from filename", async function () {
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Title');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await importFileAttachment('test.png', { parentItemID: item.id });
|
||||
attachment.setField('title', 'Title');
|
||||
|
||||
await attachment.saveTx();
|
||||
await renameFileFromParent(attachment);
|
||||
|
||||
assert.equal(attachment.attachmentFilename, 'Title.png');
|
||||
assert.equal(attachment.getField('title'), 'Title');
|
||||
});
|
||||
|
||||
it("should change attachment title if previously set to the file basename by setAutoAttachmentTitle()", async function () {
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Title');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await importFileAttachment('test.png', {
|
||||
parentItemID: item.id,
|
||||
// Use default setAutoAttachmentTitle() behavior -- the file isn't going to be
|
||||
// renamed because autoRenameFiles.fileTypes doesn't match image/, so the title
|
||||
// becomes the filename minus extension, i.e., "test"
|
||||
title: null
|
||||
});
|
||||
assert.equal(attachment.getField('title'), 'test');
|
||||
await renameFileFromParent(attachment);
|
||||
assert.equal(attachment.attachmentFilename, 'Title.png');
|
||||
// After a manual rename, the title becomes the default for this type
|
||||
assert.equal(attachment.getField('title'), Zotero.getString('file-type-image'));
|
||||
});
|
||||
|
||||
it("should restore an extension when renaming a misnamed file", async function () {
|
||||
let pdfFile = getTestDataDirectory();
|
||||
pdfFile.append('test.pdf');
|
||||
let tmpDir = await getTempDirectory();
|
||||
let tmpFileToImport = OS.Path.join(tmpDir, 'bad name . not an extension');
|
||||
await OS.File.copy(pdfFile.path, tmpFileToImport);
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Title');
|
||||
await item.saveTx();
|
||||
|
||||
let attachment = await Zotero.Attachments.importFromFile({
|
||||
file: tmpFileToImport,
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
await renameFileFromParent(attachment);
|
||||
assert.equal(attachment.attachmentFilename, 'Title.pdf');
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1714,6 +1714,82 @@ describe("Item pane", function () {
|
|||
attachmentBox._discardPreviewTimeout = currentDiscardTimeout;
|
||||
});
|
||||
|
||||
it("should hide the rename from parent button if already renamed", async function () {
|
||||
let item = await createDataObject('item', { title: 'Lorem Ipsum' });
|
||||
let file = getTestDataDirectory();
|
||||
file.append('test.pdf');
|
||||
let attachment = await Zotero.Attachments.importFromFile({
|
||||
file: file,
|
||||
fileBaseName: "Lorem Ipsum", // Simulate auto-renaming, normally code would call getRenamedFileBaseNameIfAllowedType to generate fileBaseName
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
let zp = win.ZoteroPane;
|
||||
await zp.selectItems([attachment.id]);
|
||||
|
||||
let itemBox = doc.getElementById('zotero-attachment-box');
|
||||
let itemDetails = ZoteroPane.itemPane._itemDetails;
|
||||
await zp.selectItems([attachment.id]);
|
||||
await itemDetails._renderPromise;
|
||||
|
||||
let label = itemBox._id('fileName');
|
||||
let button = itemBox._id('rename-from-parent');
|
||||
|
||||
// File is auto-renamed during import, button should be hidden
|
||||
assert.isTrue(button.hidden);
|
||||
assert.equal(label.value, "Lorem Ipsum.pdf");
|
||||
|
||||
await attachment.eraseTx();
|
||||
await item.eraseTx();
|
||||
});
|
||||
|
||||
it("should hide the rename from parent button, after file was renamed", async function () {
|
||||
let item = await createDataObject('item', { title: 'Lorem Ipsum' });
|
||||
let file = getTestDataDirectory();
|
||||
file.append('test.txt');
|
||||
let attachment = await Zotero.Attachments.importFromFile({
|
||||
file: file,
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
let zp = win.ZoteroPane;
|
||||
let itemBox = doc.getElementById('zotero-attachment-box');
|
||||
let label = itemBox._id('fileName');
|
||||
let button = itemBox._id('rename-from-parent');
|
||||
let itemDetails = ZoteroPane.itemPane._itemDetails;
|
||||
await zp.selectItems([attachment.id]);
|
||||
await itemDetails._renderPromise;
|
||||
assert.isFalse(button.hidden);
|
||||
|
||||
button.click();
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
assert.isTrue(button.hidden);
|
||||
assert.equal(label.value, "Lorem Ipsum.txt");
|
||||
|
||||
await attachment.eraseTx();
|
||||
await item.eraseTx();
|
||||
});
|
||||
|
||||
it("should hide the rename from parent button for top-level items", async function () {
|
||||
let file = getTestDataDirectory();
|
||||
file.append('test.pdf');
|
||||
let topLevelAttachment = await Zotero.Attachments.importFromFile({
|
||||
file: file,
|
||||
});
|
||||
|
||||
let zp = win.ZoteroPane;
|
||||
let itemBox = doc.getElementById('zotero-attachment-box');
|
||||
let button = itemBox._id('rename-from-parent');
|
||||
let itemDetails = ZoteroPane.itemPane._itemDetails;
|
||||
await zp.selectItems([topLevelAttachment.id]);
|
||||
await itemDetails._renderPromise;
|
||||
|
||||
assert.isTrue(button.hidden);
|
||||
await topLevelAttachment.eraseTx();
|
||||
});
|
||||
|
||||
it("should not transfer focused title while switching between items", async function () {
|
||||
let item = new Zotero.Item('book');
|
||||
let attachmentOne = await importFileAttachment('test.pdf', { title: 'PDF_one', parentItemID: item.id });
|
||||
|
|
|
|||
|
|
@ -1332,7 +1332,7 @@ describe("Zotero.Item", function () {
|
|||
await parentItem.getBestAttachment();
|
||||
assert.deepEqual(
|
||||
parentItem.getBestAttachmentStateCached(),
|
||||
{ key: childItem.key }
|
||||
{ exists: true, key: childItem.key }
|
||||
);
|
||||
await childItem._updateAttachmentStates(false);
|
||||
assert.deepEqual(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ describe("Document Recognition", function () {
|
|||
Zotero.Prefs.set('fulltext.textMaxLength', 0);
|
||||
|
||||
this.timeout(60000);
|
||||
Zotero.Prefs.set('autoRenameFiles.onMetadataChange', false); // Prevent auto-rename triggering during recognition
|
||||
// Load Zotero pane and install PDF tools
|
||||
yield Promise.all([
|
||||
loadZoteroPane().then(w => win = w)
|
||||
|
|
@ -47,6 +48,7 @@ describe("Document Recognition", function () {
|
|||
if (win) {
|
||||
win.close();
|
||||
}
|
||||
Zotero.Prefs.clear('autoRenameFiles.onMetadataChange');
|
||||
});
|
||||
|
||||
describe("PDFs", function () {
|
||||
|
|
@ -67,6 +69,12 @@ describe("Document Recognition", function () {
|
|||
assert.equal(item.getField("title"), "Shaping the Research Agenda");
|
||||
assert.equal(item.getField("libraryCatalog"), "DOI.org (Crossref)");
|
||||
assert.lengthOf(modifiedIDs, 2);
|
||||
|
||||
// after item has been recognized, attachment item will be modified
|
||||
// two more times (by `attachment.renameAttachmentFile` and then by
|
||||
// `attachment.setAutoAttachmentTitle` and in recognizeDocument.js):
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
await waitForProgressWindow();
|
||||
|
||||
|
|
@ -228,13 +236,13 @@ describe("Document Recognition", function () {
|
|||
});
|
||||
|
||||
// Link to the PDF
|
||||
var tempDir = await getTempDirectory();
|
||||
var tempFile = OS.Path.join(tempDir, 'test.pdf');
|
||||
let tempDir = await getTempDirectory();
|
||||
let tempFile = OS.Path.join(tempDir, 'test.pdf');
|
||||
await OS.File.copy(OS.Path.join(getTestDataDirectory().path, 'test.pdf'), tempFile);
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: tempFile
|
||||
});
|
||||
|
||||
|
||||
win.ZoteroPane.recognizeSelected();
|
||||
|
||||
var addedIDs = await waitForItemEvent("add");
|
||||
|
|
@ -243,6 +251,12 @@ describe("Document Recognition", function () {
|
|||
var item = Zotero.Items.get(addedIDs[0]);
|
||||
assert.equal(item.getField("title"), itemTitle);
|
||||
assert.lengthOf(modifiedIDs, 2);
|
||||
|
||||
// after item has been recognized, attachment item will be modified
|
||||
// two more times (by `attachment.renameAttachmentFile` and then by
|
||||
// `attachment.setAutoAttachmentTitle` and in recognizeDocument.js):
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
await waitForProgressWindow();
|
||||
|
||||
|
|
@ -286,6 +300,7 @@ describe("Document Recognition", function () {
|
|||
|
||||
// The title should not have changed
|
||||
assert.equal(attachment.getField('title'), 'test');
|
||||
Zotero.Prefs.clear('autoRenameFiles.fileTypes');
|
||||
});
|
||||
|
||||
it("shouldn't rename a linked file attachment using parent metadata if pref disabled", async function () {
|
||||
|
|
@ -344,6 +359,12 @@ describe("Document Recognition", function () {
|
|||
var item = Zotero.Items.get(addedIDs[0]);
|
||||
assert.lengthOf(modifiedIDs, 2);
|
||||
|
||||
// after item has been recognized, attachment item will be modified
|
||||
// two more times (by `attachment.renameAttachmentFile` and then by
|
||||
// `attachment.setAutoAttachmentTitle` and in recognizeDocument.js):
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
await waitForProgressWindow();
|
||||
|
||||
// The file should have been renamed
|
||||
|
|
@ -386,6 +407,12 @@ describe("Document Recognition", function () {
|
|||
assert.equal(Zotero.Utilities.cleanISBN(item.getField('ISBN')), isbn);
|
||||
assert.lengthOf(modifiedIDs, 2);
|
||||
|
||||
// after item has been recognized, attachment item will be modified
|
||||
// two more times (by `attachment.renameAttachmentFile` and then by
|
||||
// `attachment.setAutoAttachmentTitle` and in recognizeDocument.js):
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
await waitForProgressWindow();
|
||||
|
||||
// The file should have been renamed
|
||||
|
|
@ -452,6 +479,12 @@ describe("Document Recognition", function () {
|
|||
assert.equal(Zotero.Utilities.cleanDOI(item.getField('DOI')), doi);
|
||||
assert.lengthOf(modifiedIDs, 2);
|
||||
|
||||
// after item has been recognized, attachment item will be modified
|
||||
// two more times (by `attachment.renameAttachmentFile` and then by
|
||||
// `attachment.setAutoAttachmentTitle` and in recognizeDocument.js):
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
await waitForProgressWindow();
|
||||
|
||||
// The file should have been renamed
|
||||
|
|
@ -485,6 +518,12 @@ describe("Document Recognition", function () {
|
|||
assert.equal(item.getField('ISBN'), '');
|
||||
assert.lengthOf(modifiedIDs, 2);
|
||||
|
||||
// after item has been recognized, attachment item will be modified
|
||||
// two more times (by `attachment.renameAttachmentFile` and then by
|
||||
// `attachment.setAutoAttachmentTitle` and in recognizeDocument.js):
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
assert.equal(await waitForItemEvent('modify'), attachment.id);
|
||||
|
||||
await waitForProgressWindow();
|
||||
|
||||
// The file should have been renamed
|
||||
|
|
|
|||
|
|
@ -590,150 +590,6 @@ describe("ZoteroPane", function () {
|
|||
});
|
||||
|
||||
|
||||
describe("#renameSelectedAttachmentsFromParents()", function () {
|
||||
it("should rename a linked file", async function () {
|
||||
var oldFilename = 'old.png';
|
||||
var newFilename = 'Test.png';
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var tmpDir = await getTempDirectory();
|
||||
var oldFile = OS.Path.join(tmpDir, oldFilename);
|
||||
await OS.File.copy(file.path, oldFile);
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Test');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: oldFile,
|
||||
parentItemID: item.id
|
||||
});
|
||||
await zp.selectItem(attachment.id);
|
||||
|
||||
await zp.renameSelectedAttachmentsFromParents();
|
||||
assert.equal(attachment.attachmentFilename, newFilename);
|
||||
var path = await attachment.getFilePathAsync();
|
||||
assert.equal(OS.Path.basename(path), newFilename)
|
||||
await OS.File.exists(path);
|
||||
});
|
||||
|
||||
it("should use unique name for linked file if target name is taken", async function () {
|
||||
var oldFilename = 'old.png';
|
||||
var newFilename = 'Test.png';
|
||||
var uniqueFilename = 'Test 2.png';
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var tmpDir = await getTempDirectory();
|
||||
var oldFile = OS.Path.join(tmpDir, oldFilename);
|
||||
await OS.File.copy(file.path, oldFile);
|
||||
// Create file with target filename
|
||||
await Zotero.File.putContentsAsync(OS.Path.join(tmpDir, newFilename), '');
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Test');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: oldFile,
|
||||
parentItemID: item.id
|
||||
});
|
||||
await zp.selectItem(attachment.id);
|
||||
|
||||
await zp.renameSelectedAttachmentsFromParents();
|
||||
assert.equal(attachment.attachmentFilename, uniqueFilename);
|
||||
var path = await attachment.getFilePathAsync();
|
||||
assert.equal(OS.Path.basename(path), uniqueFilename)
|
||||
await OS.File.exists(path);
|
||||
});
|
||||
|
||||
it("should use unique name for linked file without extension if target name is taken", async function () {
|
||||
var oldFilename = 'old';
|
||||
var newFilename = 'Test.png';
|
||||
var uniqueFilename = 'Test 2.png';
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var tmpDir = await getTempDirectory();
|
||||
var oldFile = OS.Path.join(tmpDir, oldFilename);
|
||||
await OS.File.copy(file.path, oldFile);
|
||||
// Create file with target filename
|
||||
await Zotero.File.putContentsAsync(OS.Path.join(tmpDir, newFilename), '');
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Test');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await Zotero.Attachments.linkFromFile({
|
||||
file: oldFile,
|
||||
parentItemID: item.id
|
||||
});
|
||||
await zp.selectItem(attachment.id);
|
||||
|
||||
await zp.renameSelectedAttachmentsFromParents();
|
||||
assert.equal(attachment.attachmentFilename, uniqueFilename);
|
||||
var path = await attachment.getFilePathAsync();
|
||||
assert.equal(OS.Path.basename(path), uniqueFilename);
|
||||
await OS.File.exists(path);
|
||||
});
|
||||
|
||||
it("shouldn't change attachment title if different from filename", async function () {
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Title');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await importFileAttachment('test.png', { parentItemID: item.id });
|
||||
attachment.setField('title', 'Title');
|
||||
await attachment.saveTx();
|
||||
await zp.selectItem(attachment.id);
|
||||
|
||||
await zp.renameSelectedAttachmentsFromParents();
|
||||
assert.equal(attachment.attachmentFilename, 'Title.png');
|
||||
assert.equal(attachment.getField('title'), 'Title');
|
||||
});
|
||||
|
||||
it("should change attachment title if previously set to the file basename by setAutoAttachmentTitle()", async function () {
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Title');
|
||||
await item.saveTx();
|
||||
|
||||
var attachment = await importFileAttachment('test.png', {
|
||||
parentItemID: item.id,
|
||||
// Use default setAutoAttachmentTitle() behavior -- the file isn't going to be
|
||||
// renamed because autoRenameFiles.fileTypes doesn't match image/, so the title
|
||||
// becomes the filename minus extension, i.e., "test"
|
||||
title: null
|
||||
});
|
||||
assert.equal(attachment.getField('title'), 'test');
|
||||
await zp.selectItem(attachment.id);
|
||||
|
||||
await zp.renameSelectedAttachmentsFromParents();
|
||||
assert.equal(attachment.attachmentFilename, 'Title.png');
|
||||
// After a manual rename, the title becomes the default for this type
|
||||
assert.equal(attachment.getField('title'), Zotero.getString('file-type-image'));
|
||||
});
|
||||
|
||||
it("should restore an extension when renaming a misnamed file", async function () {
|
||||
let pdfFile = getTestDataDirectory();
|
||||
pdfFile.append('test.pdf');
|
||||
let tmpDir = await getTempDirectory();
|
||||
let tmpFileToImport = OS.Path.join(tmpDir, 'bad name . not an extension');
|
||||
await OS.File.copy(pdfFile.path, tmpFileToImport);
|
||||
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setField('title', 'Title');
|
||||
await item.saveTx();
|
||||
|
||||
let attachment = await Zotero.Attachments.importFromFile({
|
||||
file: tmpFileToImport,
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
await zp.selectItem(attachment.id);
|
||||
await zp.renameSelectedAttachmentsFromParents();
|
||||
assert.equal(attachment.attachmentFilename, 'Title.pdf');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("#duplicateSelectedItem()", function () {
|
||||
it("should add reverse relations", async function () {
|
||||
await selectLibrary(win);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue