diff --git a/chrome/content/zotero/customElements.js b/chrome/content/zotero/customElements.js
index b1c55de54b..736d3eebb8 100644
--- a/chrome/content/zotero/customElements.js
+++ b/chrome/content/zotero/customElements.js
@@ -78,6 +78,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemTreeMe
['libraries-collections-box', 'chrome://zotero/content/elements/librariesCollectionsBox.js'],
['autocomplete-textarea', 'chrome://zotero/content/elements/autocompleteTextArea.js'],
['bubble-input', 'chrome://zotero/content/elements/bubbleInput.js'],
+ ['file-renaming-settings', 'chrome://zotero/content/elements/fileRenamingSettings.js']
]) {
customElements.setElementCreationCallback(tag, () => {
Services.scriptloader.loadSubScript(script, window);
diff --git a/chrome/content/zotero/elements/fileRenamingSettings.js b/chrome/content/zotero/elements/fileRenamingSettings.js
new file mode 100644
index 0000000000..649aa09923
--- /dev/null
+++ b/chrome/content/zotero/elements/fileRenamingSettings.js
@@ -0,0 +1,338 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2026 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+{
+ const { DEFAULT_ATTACHMENT_RENAME_TEMPLATE } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
+ const DEFAULT_EXT = 'pdf';
+ class FileRenameSettings extends XULElementBase {
+ content = MozXULElement.parseXULToFragment(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+
+ static get observedAttributes() {
+ return [
+ 'auto-rename-enabled',
+ 'file-types',
+ 'format-template',
+ 'rename-linked-enabled',
+ 'rename-linked-hidden',
+ 'readonly',
+ ];
+ }
+
+ get autoRenameEnabled() {
+ return this.autoRenameToggleCheckbox.checked;
+ }
+
+ set autoRenameEnabled(val) {
+ this.autoRenameToggleCheckbox.checked = val;
+ this.updateDisabled();
+ }
+
+ get renameLinkedEnabled() {
+ return this.renameLinkedCheckbox.checked;
+ }
+
+ set renameLinkedEnabled(val) {
+ this.renameLinkedCheckbox.checked = val;
+ }
+
+ get enabledFileTypes() {
+ let enabledTypes = new Set(
+ (this._enabledFileTypes).split(',').filter(Boolean)
+ );
+ for (let checkbox of this.fileTypesCheckboxes.querySelectorAll('checkbox')) {
+ if (checkbox.checked) {
+ enabledTypes.add(checkbox.dataset.contentType);
+ }
+ else {
+ enabledTypes.delete(checkbox.dataset.contentType);
+ }
+ }
+ return [...enabledTypes].join(',');
+ }
+
+ set enabledFileTypes(types) {
+ this._enabledFileTypes = types;
+ let enabledTypes = new Set(
+ (this._enabledFileTypes).split(',').filter(Boolean)
+ );
+ for (let checkbox of this.fileTypesCheckboxes.querySelectorAll('checkbox')) {
+ checkbox.checked = enabledTypes.has(checkbox.dataset.contentType);
+ }
+ }
+
+ get formatTemplate() {
+ return this.formatTemplateTextarea.value;
+ }
+
+ set formatTemplate(val) {
+ this.formatTemplateTextarea.value = val;
+ }
+
+ handleChange = () => {
+ let autoRenameEnabled = this.autoRenameEnabled;
+ let enabledFileTypes = this.enabledFileTypes;
+ let renameLinkedEnabled = this.renameLinkedEnabled;
+ let formatTemplate = this.formatTemplate;
+ this.dispatchEvent(new CustomEvent("change", {
+ detail: {
+ autoRenameEnabled,
+ enabledFileTypes,
+ renameLinkedEnabled,
+ formatTemplate
+ },
+ bubbles: true,
+ cancelable: true
+ }));
+ };
+
+ handleTemplateInput = () => {
+ let formatString = this.formatTemplateTextarea.value;
+ // Ignore the empty value, which we'll reset in handleInputBlur() if necessary
+ if (formatString.replace(/\s/g, '') === '') {
+ return;
+ }
+ this.updatePreview();
+ this.handleChange();
+ };
+
+ handleTemplateBlur = () => {
+ let formatString = this.formatTemplateTextarea.value;
+ if (formatString.replace(/\s/g, '') === '') {
+ this.formatTemplateTextarea.value = DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
+ this.updatePreview();
+ this.handleChange();
+ }
+ };
+
+ handleRenameToggle = () => {
+ this.autoRenameEnabled = this.autoRenameToggleCheckbox.checked;
+ this.handleChange();
+ };
+
+ updateDisabled = () => {
+ let readonly = this.getAttribute('readonly') === 'true';
+ for (let checkbox of this.fileTypesCheckboxes.querySelectorAll('checkbox')) {
+ checkbox.disabled = readonly || !this.autoRenameEnabled;
+ }
+ this.autoRenameToggleCheckbox.disabled = readonly;
+ this.renameLinkedCheckbox.disabled = readonly || !this.autoRenameEnabled;
+ this.formatTemplateTextarea.readOnly = readonly;
+ };
+
+ updatePreview = () => {
+ let [item, ext, attachmentTitle] = this.getActiveItem() ?? [this.mockItem ?? this.makeMockItem(), DEFAULT_EXT, ''];
+ let formatString = this.formatTemplate;
+ let preview = Zotero.Attachments.getFileBaseNameFromItem(item, { formatString, attachmentTitle });
+ this.querySelector('#file-renaming-format-preview').innerText = `${preview}.${ext}`;
+ };
+
+ async init() {
+ this.sectionMain = this.querySelector('#file-rename-settings-section-main');
+ this.sectionInstructions = this.querySelector('#file-rename-settings-section-instructions');
+ this.sectionTemplate = this.querySelector('#file-rename-settings-section-template');
+ this.autoRenameToggleCheckbox = this.querySelector('#auto-rename-files');
+ this.fileTypesCheckboxes = this.querySelector('#file-renaming-file-types-box');
+ this.renameLinkedCheckbox = this.querySelector('#rename-linked-files');
+ this.formatTemplateTextarea = this.querySelector('#file-renaming-format-template');
+ this.enabledFileTypes = this.getAttribute('file-types') ?? '';
+ this.autoRenameEnabled = this.getAttribute('auto-rename-enabled') === 'true';
+ this.renameLinkedCheckbox.checked = this.getAttribute('rename-linked-enabled') === 'true';
+ this.formatTemplate = this.getAttribute('format-template') ?? '';
+ this.renameLinkedCheckbox.hidden = this.getAttribute('rename-linked-hidden') === 'true';
+
+ this.autoRenameToggleCheckbox.addEventListener("command", this.handleRenameToggle);
+ this.fileTypesCheckboxes.addEventListener("command", this.handleChange);
+ this.renameLinkedCheckbox.addEventListener("command", this.handleChange);
+ this.formatTemplateTextarea.addEventListener("input", this.handleTemplateInput);
+ this.formatTemplateTextarea.addEventListener("blur", this.handleTemplateBlur);
+
+ this._itemsView = Zotero.getActiveZoteroPane()?.itemsView;
+ if (this._itemsView) {
+ this._itemsView.onSelect.addListener(this.updatePreview);
+ }
+
+ this.updatePreview();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this._itemsView?.onSelect.removeListener(this.updatePreview);
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (!this.sectionMain) return;
+
+ switch (name) {
+ case 'auto-rename-enabled':
+ this.autoRenameEnabled = newValue === 'true';
+ break;
+ case 'file-types':
+ this.enabledFileTypes = newValue ?? '';
+ break;
+ case 'format-template':
+ this.formatTemplate = newValue ?? '';
+ this.updatePreview();
+ break;
+ case 'rename-linked-enabled':
+ this.renameLinkedCheckbox.checked = newValue === 'true';
+ break;
+ case 'rename-linked-hidden':
+ this.renameLinkedCheckbox.hidden = newValue === 'true';
+ break;
+ case 'readonly':
+ this.updateDisabled();
+ break;
+ }
+ }
+
+ getActiveItem() {
+ let selectedItem = Zotero.getActiveZoteroPane()?.getSelectedItems()?.[0];
+ if (selectedItem) {
+ if (selectedItem.isRegularItem() && !selectedItem.parentKey) {
+ return [selectedItem, DEFAULT_EXT, ''];
+ }
+ if (selectedItem.isFileAttachment() && selectedItem.parentKey) {
+ let ext = Zotero.Attachments.getCorrectFileExtension(selectedItem);
+ let parentItem = Zotero.Items.getByLibraryAndKey(selectedItem.libraryID, selectedItem.parentKey);
+ return [parentItem, ext ?? DEFAULT_EXT, selectedItem.getField('title')];
+ }
+ }
+
+ return null;
+ }
+
+ 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' },
+ { firstName: 'John', lastName: 'Smith', creatorType: 'author' }
+ ]);
+ this.mockItem.setField('shortTitle', 'Example Title');
+ this.mockItem.setField('publicationTitle', 'Advances in Example Engineering');
+ this.mockItem.setField('volume', '9');
+ this.mockItem.setField('issue', '1');
+ this.mockItem.setField('pages', '34-55');
+ this.mockItem.setField('date', '2018');
+ this.mockItem.setField('DOI', '10.1016/1234-example');
+ this.mockItem.setField('ISSN', '1234-5678');
+ this.mockItem.setField('abstractNote', 'This is an example abstract.');
+ this.mockItem.setField('extra', 'This is an example Extra field.');
+ this.mockItem.setField('accessDate', '2020-01-01');
+ this.mockItem.setField('url', 'https://example.com');
+ this.mockItem.setField('libraryCatalog', 'Example Library Catalog');
+ return this.mockItem;
+ }
+ }
+
+ customElements.define('file-renaming-settings', FileRenameSettings);
+}
diff --git a/chrome/content/zotero/fileRenamingDialog.js b/chrome/content/zotero/fileRenamingDialog.js
new file mode 100644
index 0000000000..ec5dac071c
--- /dev/null
+++ b/chrome/content/zotero/fileRenamingDialog.js
@@ -0,0 +1,313 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2026 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+var FileRenamingDialog = { // eslint-disable-line no-unused-vars
+ _settingsChanged: false,
+ _currentLibraryID: null,
+ _forceClose: false,
+
+ init: function () {
+ const { DEFAULT_ATTACHMENT_RENAME_TEMPLATE, DEFAULT_AUTO_RENAME_FILE_TYPES } = ChromeUtils.importESModule("chrome://zotero/content/renameFiles.mjs");
+ this.DEFAULT_ATTACHMENT_RENAME_TEMPLATE = DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
+ this.DEFAULT_AUTO_RENAME_FILE_TYPES = DEFAULT_AUTO_RENAME_FILE_TYPES;
+
+ this.settingsEl = document.getElementById('file-renaming-settings');
+ this.libraryPicker = document.getElementById('library-picker');
+ this.renameFilesBtn = document.getElementById('file-renaming-rename-files-btn');
+ this.doneBtn = document.getElementById('file-renaming-done-btn');
+
+ // Populate library picker
+ let libraries = Zotero.Libraries.getAll().filter(lib => !(lib instanceof Zotero.Feed));
+ let menupopup = this.libraryPicker.querySelector('menupopup');
+ for (let lib of libraries) {
+ let menuitem = document.createXULElement('menuitem');
+ menuitem.setAttribute('label', lib.name);
+ menuitem.setAttribute('value', lib.libraryID);
+ menupopup.appendChild(menuitem);
+ }
+
+ // Default to user library, or use passed-in libraryID
+ let initialLibraryID = window.arguments?.[0]?.wrappedJSObject?.libraryID ?? Zotero.Libraries.userLibraryID;
+ this.libraryPicker.value = String(initialLibraryID);
+
+ this.libraryPicker.addEventListener('command', this.handleLibraryChange.bind(this));
+ this.settingsEl.addEventListener('change', this.handleSettingsChange.bind(this));
+ this.renameFilesBtn.addEventListener('command', this._handleRenameFilesClick.bind(this));
+ this.doneBtn.addEventListener('command', this._handleDoneClick.bind(this));
+
+ this._handleDonePrefChange = this._handleDonePrefChange.bind(this);
+ this._renameFilesPrefObserver = Zotero.Prefs.registerObserver('autoRenameFiles.done', this._handleDonePrefChange);
+
+ this.loadSettingsForLibrary(initialLibraryID);
+ this._currentLibraryID = initialLibraryID;
+ this._updateButtons();
+
+ window.addEventListener('unload', () => {
+ Zotero.Prefs.unregisterObserver(this._renameFilesPrefObserver);
+ });
+ },
+
+ get libraryID() {
+ return parseInt(this.libraryPicker.value);
+ },
+
+ get isUserLibrary() {
+ return this.libraryID === Zotero.Libraries.userLibraryID;
+ },
+
+ _updateButtons: function () {
+ let autoRenameEnabled = this.settingsEl.autoRenameEnabled;
+ let library = Zotero.Libraries.get(this._currentLibraryID);
+ let isAdmin = this.isUserLibrary || library.isAdmin;
+ this.renameFilesBtn.hidden = !isAdmin;
+ this.renameFilesBtn.disabled = !autoRenameEnabled || this._isRenameFilesBtnDisabled();
+ },
+
+ _isRenameFilesBtnDisabled: function () {
+ if (this.isUserLibrary) {
+ return Zotero.Prefs.get('autoRenameFiles.done');
+ }
+ return false;
+ },
+
+ _openRenameFilesPreview: function (libraryID) {
+ let args = { libraryID };
+ Services.ww.openWindow(null, "chrome://zotero/content/renameFilesPreview.xhtml",
+ "renameFilesPreview", "chrome,dialog=yes,centerscreen,modal", args);
+ if (!args.cancelled) {
+ this._resetBaseline(libraryID);
+ }
+ return !args.cancelled;
+ },
+
+ _handleDoneClick: async function () {
+ // "Done" button -- prompt if dirty, then close
+ if (this._shouldPromptRename(this._currentLibraryID)) {
+ let wantsRename = await this._promptRename(this._currentLibraryID);
+ if (wantsRename) {
+ this._openRenameFilesPreview(this._currentLibraryID);
+ }
+ }
+ window.close(); // bypasses the onclose handler
+ },
+
+ _handleRenameFilesClick: function () {
+ // "Rename Files..." button
+ this._openRenameFilesPreview(this._currentLibraryID);
+ this._updateButtons();
+ },
+
+ loadSettingsForLibrary: function (libraryID) {
+ this._settingsChanged = false;
+ let isUserLib = libraryID === Zotero.Libraries.userLibraryID;
+
+ let autoRenameEnabled;
+ let fileTypes;
+ let formatTemplate;
+ let renameLinked;
+
+ if (isUserLib) {
+ autoRenameEnabled = Zotero.Prefs.get('autoRenameFiles');
+ fileTypes = Zotero.Prefs.get('autoRenameFiles.fileTypes');
+ renameLinked = Zotero.Prefs.get('autoRenameFiles.linked');
+ formatTemplate = Zotero.SyncedSettings.get(libraryID, 'attachmentRenameTemplate')
+ ?? this.DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
+
+ let library = Zotero.Libraries.get(libraryID);
+ this.settingsEl.setAttribute('readonly', String(!library.editable));
+ this.settingsEl.setAttribute('rename-linked-hidden', 'false');
+ this.settingsEl.setAttribute('rename-linked-enabled', String(renameLinked));
+
+ this._baselineDone = Zotero.Prefs.get('autoRenameFiles.done');
+ }
+ else {
+ autoRenameEnabled = Zotero.Attachments.isAutoRenameFilesEnabledForLibrary(libraryID);
+ fileTypes = Zotero.SyncedSettings.get(libraryID, 'autoRenameFilesFileTypes')
+ ?? this.DEFAULT_AUTO_RENAME_FILE_TYPES;
+ formatTemplate = Zotero.SyncedSettings.get(libraryID, 'attachmentRenameTemplate')
+ ?? this.DEFAULT_ATTACHMENT_RENAME_TEMPLATE;
+
+ let isAdmin = Zotero.Libraries.get(libraryID).isAdmin;
+ this.settingsEl.setAttribute('readonly', String(!isAdmin));
+ this.settingsEl.setAttribute('rename-linked-hidden', 'true');
+ }
+
+ // Store baseline for comparing against future changes
+ this._baselineSettings = isUserLib
+ ? { autoRenameEnabled, fileTypes, formatTemplate, renameLinked }
+ : { autoRenameEnabled, fileTypes, formatTemplate };
+
+ this.settingsEl.setAttribute('auto-rename-enabled', String(autoRenameEnabled));
+ this.settingsEl.setAttribute('file-types', fileTypes);
+ this.settingsEl.setAttribute('format-template', formatTemplate);
+ },
+
+ handleLibraryChange: async function () {
+ let previousLibraryID = this._currentLibraryID;
+ let newLibraryID = this.libraryID;
+ if (newLibraryID === previousLibraryID) {
+ return;
+ }
+ if (this._shouldPromptRename(previousLibraryID)) {
+ // Revert picker so the previous library is visible behind the prompt
+ this.libraryPicker.value = String(previousLibraryID);
+ let wantsRename = await this._promptRename(previousLibraryID);
+ if (wantsRename && !this._openRenameFilesPreview(previousLibraryID)) {
+ // User cancelled the preview -- stay on the current library
+ return;
+ }
+ this.libraryPicker.value = String(newLibraryID);
+ }
+ this._currentLibraryID = newLibraryID;
+ this.loadSettingsForLibrary(newLibraryID);
+ this._updateButtons();
+ },
+
+ handleSettingsChange: function (event) {
+ if (!event.detail) {
+ return;
+ }
+
+ let { autoRenameEnabled, enabledFileTypes, renameLinkedEnabled, formatTemplate } = event.detail;
+ let base = this._baselineSettings;
+
+ if (this.isUserLibrary) {
+ Zotero.Prefs.set('autoRenameFiles', autoRenameEnabled);
+ Zotero.Prefs.set('autoRenameFiles.fileTypes', enabledFileTypes);
+ Zotero.Prefs.set('autoRenameFiles.linked', renameLinkedEnabled);
+
+ // Handle template changes
+ if (formatTemplate.replace(/\s/g, '') === '') {
+ Zotero.SyncedSettings.clear(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate');
+ }
+ else {
+ Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate', formatTemplate);
+ }
+
+ let settingsMatch = autoRenameEnabled === base.autoRenameEnabled
+ && enabledFileTypes === base.fileTypes
+ && formatTemplate === base.formatTemplate
+ && renameLinkedEnabled === base.renameLinked;
+ Zotero.Prefs.set('autoRenameFiles.done', settingsMatch && this._baselineDone);
+ this._settingsChanged = !settingsMatch;
+ }
+ else {
+ Zotero.SyncedSettings.set(this.libraryID, 'autoRenameFiles', autoRenameEnabled);
+ Zotero.SyncedSettings.set(this.libraryID, 'autoRenameFilesFileTypes', enabledFileTypes);
+ Zotero.SyncedSettings.set(this.libraryID, 'attachmentRenameTemplate', formatTemplate);
+
+ this._settingsChanged = autoRenameEnabled !== base.autoRenameEnabled
+ || enabledFileTypes !== base.fileTypes
+ || formatTemplate !== base.formatTemplate;
+ }
+
+ this._updateButtons();
+ },
+
+ handleWindowClose: async function (event) {
+ if (this._forceClose) {
+ return true;
+ }
+ if (this._shouldPromptRename(this._currentLibraryID)) {
+ event.preventDefault();
+ let wantsRename = await this._promptRename(this._currentLibraryID);
+ if (wantsRename) {
+ this._openRenameFilesPreview(this._currentLibraryID);
+ }
+ this._forceClose = true;
+ window.close();
+ return false;
+ }
+ return true;
+ },
+
+ _handleDonePrefChange: function () {
+ if (this._currentLibraryID !== Zotero.Libraries.userLibraryID) {
+ return;
+ }
+
+ this._updateButtons();
+ },
+
+ _shouldPromptRename: function (libraryID) {
+ if (!this._settingsChanged) {
+ return false;
+ }
+ if (libraryID === Zotero.Libraries.userLibraryID) {
+ return Zotero.Prefs.get('autoRenameFiles') && !Zotero.Prefs.get('autoRenameFiles.done');
+ }
+ return Zotero.Attachments.isAutoRenameFilesEnabledForLibrary(libraryID);
+ },
+
+ _promptRename: async function (libraryID) {
+ let isUserLib = libraryID === Zotero.Libraries.userLibraryID;
+ let bodyID = isUserLib
+ ? { id: 'file-renaming-auto-rename-prompt-body' }
+ : { id: 'file-renaming-auto-rename-prompt-body-library', args: { library: Zotero.Libraries.get(libraryID).name } };
+ let [title, description, yes, no] = await document.l10n.formatValues([
+ 'file-renaming-auto-rename-prompt-title',
+ bodyID,
+ '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) {
+ return true;
+ }
+ if (isUserLib) {
+ Zotero.Prefs.set('autoRenameFiles.done', false);
+ }
+ return false;
+ },
+
+ _resetBaseline: function (libraryID) {
+ this._settingsChanged = false;
+ if (libraryID === Zotero.Libraries.userLibraryID) {
+ this._baselineDone = true;
+ this._baselineSettings = {
+ autoRenameEnabled: Zotero.Prefs.get('autoRenameFiles'),
+ fileTypes: Zotero.Prefs.get('autoRenameFiles.fileTypes'),
+ formatTemplate: Zotero.SyncedSettings.get(
+ Zotero.Libraries.userLibraryID, 'attachmentRenameTemplate'
+ ) ?? this.DEFAULT_ATTACHMENT_RENAME_TEMPLATE,
+ renameLinked: Zotero.Prefs.get('autoRenameFiles.linked'),
+ };
+ }
+ else {
+ this._baselineSettings = {
+ autoRenameEnabled: Zotero.Attachments.isAutoRenameFilesEnabledForLibrary(libraryID),
+ fileTypes: Zotero.SyncedSettings.get(libraryID, 'autoRenameFilesFileTypes')
+ ?? this.DEFAULT_AUTO_RENAME_FILE_TYPES,
+ formatTemplate: Zotero.SyncedSettings.get(libraryID, 'attachmentRenameTemplate')
+ ?? this.DEFAULT_ATTACHMENT_RENAME_TEMPLATE,
+ };
+ }
+ }
+};
diff --git a/chrome/content/zotero/fileRenamingDialog.xhtml b/chrome/content/zotero/fileRenamingDialog.xhtml
new file mode 100644
index 0000000000..9d5b3103a8
--- /dev/null
+++ b/chrome/content/zotero/fileRenamingDialog.xhtml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/preferences/preferences_file_renaming.js b/chrome/content/zotero/preferences/preferences_file_renaming.js
deleted file mode 100644
index 73c8f19e07..0000000000
--- a/chrome/content/zotero/preferences/preferences_file_renaming.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © Corporation for Digital Scholarship
- Vienna, Virginia, USA
- https://www.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 .
-
- ***** END LICENSE BLOCK *****
-*/
-/* 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.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.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() {
- let selectedItem = Zotero.getActiveZoteroPane()?.getSelectedItems()?.[0];
- if (selectedItem) {
- if (selectedItem.isRegularItem() && !selectedItem.parentKey) {
- return [selectedItem, this.defaultExt, ''];
- }
- if (selectedItem.isFileAttachment() && selectedItem.parentKey) {
- let ext = Zotero.Attachments.getCorrectFileExtension(selectedItem);
- let parentItem = Zotero.Items.getByLibraryAndKey(selectedItem.libraryID, selectedItem.parentKey);
- return [parentItem, ext ?? this.defaultExt, selectedItem.getField('title')];
- }
- }
-
- return null;
- },
-
- updatePreview() {
- const [item, ext, attachmentTitle] = this.getActiveItem() ?? [this.mockItem ?? this.makeMockItem(), this.defaultExt, ''];
- const formatString = this.inputEl.value;
- const preview = Zotero.Attachments.getFileBaseNameFromItem(item, { formatString, attachmentTitle });
- document.getElementById('file-renaming-format-preview').innerText = `${preview}.${ext}`;
- },
-
- 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' },
- { firstName: 'John', lastName: 'Smith', creatorType: 'author' }
- ]);
- this.mockItem.setField('shortTitle', 'Example Title');
- this.mockItem.setField('publicationTitle', 'Advances in Example Engineering');
- this.mockItem.setField('volume', '9');
- this.mockItem.setField('issue', '1');
- this.mockItem.setField('pages', '34-55');
- this.mockItem.setField('date', '2018');
- this.mockItem.setField('DOI', '10.1016/1234-example');
- this.mockItem.setField('ISSN', '1234-5678');
- this.mockItem.setField('abstractNote', 'This is an example abstract.');
- this.mockItem.setField('extra', 'This is an example Extra field.');
- this.mockItem.setField('accessDate', '2020-01-01');
- this.mockItem.setField('url', 'https://example.com');
- this.mockItem.setField('libraryCatalog', 'Example Library Catalog');
- return this.mockItem;
- },
-};
diff --git a/chrome/content/zotero/preferences/preferences_file_renaming.xhtml b/chrome/content/zotero/preferences/preferences_file_renaming.xhtml
deleted file mode 100644
index 64df0c1afe..0000000000
--- a/chrome/content/zotero/preferences/preferences_file_renaming.xhtml
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/chrome/content/zotero/preferences/preferences_general.js b/chrome/content/zotero/preferences/preferences_general.js
index 80aceafadc..8a5ed51084 100644
--- a/chrome/content/zotero/preferences/preferences_general.js
+++ b/chrome/content/zotero/preferences/preferences_general.js
@@ -26,7 +26,6 @@
"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,
@@ -54,17 +53,14 @@ 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();
this._updateFileHandlerUI();
this._initEbookFontFamilyMenu();
this._initAutoDisableToolCheckbox();
},
uninit: function () {
- Zotero.Prefs.unregisterObserver(this._renameFilesPrefObserver);
},
_getAutomaticLocaleMenuLabel: function () {
@@ -223,50 +219,10 @@ Zotero_Preferences.General = {
}
}),
- setAutoRenameFileTypes: function () {
- let typesBox = document.getElementById('zotero-prefpane-file-renaming-file-types-box');
- let enabledTypes = new Set(
- Zotero.Prefs.get('autoRenameFiles.fileTypes')
- .split(',')
- .filter(Boolean)
+ openFileRenamingDialog: function () {
+ Services.ww.openWindow(null, 'chrome://zotero/content/fileRenamingDialog.xhtml',
+ 'zotero-file-renaming-dialog', 'chrome,dialog=no,titlebar,centerscreen,resizable=yes', null
);
- for (let checkbox of typesBox.querySelectorAll('checkbox')) {
- if (checkbox.checked) {
- enabledTypes.add(checkbox.dataset.contentType);
- }
- else {
- enabledTypes.delete(checkbox.dataset.contentType);
- }
- }
- Zotero.Prefs.set('autoRenameFiles.fileTypes', [...enabledTypes].join(','));
- Zotero.Prefs.set('autoRenameFiles.done', false);
- },
-
- updateAutoRenameFilesUI: function () {
- let disabled = !Zotero.Prefs.get('autoRenameFiles');
-
- let typesBox = document.getElementById('zotero-prefpane-file-renaming-file-types-box');
- let enabledTypes = Zotero.Prefs.get('autoRenameFiles.fileTypes').split(',');
- for (let checkbox of typesBox.querySelectorAll('checkbox')) {
- checkbox.checked = enabledTypes.includes(checkbox.dataset.contentType);
- 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();
},
//
diff --git a/chrome/content/zotero/preferences/preferences_general.xhtml b/chrome/content/zotero/preferences/preferences_general.xhtml
index 1d0659fd6a..8f9abc4e30 100644
--- a/chrome/content/zotero/preferences/preferences_general.xhtml
+++ b/chrome/content/zotero/preferences/preferences_general.xhtml
@@ -107,69 +107,15 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/chrome/content/zotero/renameFiles.mjs b/chrome/content/zotero/renameFiles.mjs
index f5cbd737d1..5da8ebfbe2 100644
--- a/chrome/content/zotero/renameFiles.mjs
+++ b/chrome/content/zotero/renameFiles.mjs
@@ -50,14 +50,13 @@ const getNewFileNameData = async (attachmentItem, parentItem) => {
* 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 {number} [options.libraryID=null] - The ID of the library to process. If null, the user library is used.
* @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>}
* Summary of (performed or proposed) rename operations.
*/
-export async function renameFilesFromParent({ userLibrary = true, groupLibrary = false, pretend = false, reportProgress = () => {} } = {}) {
+export async function renameFilesFromParent({ libraryID = null, pretend = false, reportProgress = () => {} } = {}) {
const t1 = Date.now();
let summary = [];
let progress = 0;
@@ -66,33 +65,16 @@ export async function renameFilesFromParent({ userLibrary = true, groupLibrary =
progress = clamp(progress + additionalProgress);
reportProgress(progress);
};
+
+ libraryID = libraryID ?? Zotero.Libraries.userLibraryID;
+ let items = await Zotero.Items.getAll(libraryID, false, true);
+ adjustProgressBy(0.01); // move the progress bar slightly while we load required data
- 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, this checks `autoRenameFiles` synced setting
- shouldRename = groupLibrary && Zotero.Attachments.isAutoRenameFilesEnabledForLibrary(library.libraryID);
- }
-
- if (shouldRename) {
- items.push(...await Zotero.Items.getAll(library.libraryID, false, true));
- librariesWithAttachmentsToRename.push(library);
- }
- }
-
+ await Zotero.Items.loadDataTypes(items, ['itemData', 'childItems']);
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;
+ // use remaining 98% of progress bar for renaming attachments
+ let perItemProgress = 0.98 / items.length;
let count = 0;
let noFilePresentCount = 0;
@@ -156,13 +138,13 @@ export async function renameFilesFromParent({ userLibrary = true, groupLibrary =
}
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);
+ Zotero.debug(`Renaming ${count + noFilePresentCount} attachments (${noFilePresentCount} with no file present) took ${((t2 - t1) / 1000).toFixed(2)} seconds (Processed ${items.length} items in library: ${libraryID}`);
+ if (libraryID === Zotero.Libraries.userLibraryID) {
+ Zotero.Prefs.set('autoRenameFiles.done', true);
+ }
}
return summary;
-};
+}
/**
* Renames an individual attachment file based on its parent item's metadata.
@@ -261,10 +243,7 @@ export function registerAutoRenameFileFromParent() {
}
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;
+ return !['tags', 'collections'].includes(key); // Only consider metadata fields that affect file naming
});
if (changes.length === 0) {
@@ -293,12 +272,17 @@ export function registerAutoRenameFileFromParent() {
if (previousMetadataBaseName === currentBaseName) {
// Filename appears to be derived from the metadata, so update it to match the latest metadata.
+ // Not awaited: we're inside the parent item's modify notification handler.
+ // Renaming the attachment triggers a child item modify notification.
+ // If we await here, that child notification fires (and is fully processed
+ // by all observers) before the parent notification is released -- reversing
+ // the expected parent-then-child order.
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.
+ // "Rename Files" is enabled in the file renaming settings dialog.
Zotero.Prefs.set('autoRenameFiles.done', false);
}
}
@@ -306,28 +290,3 @@ export function registerAutoRenameFileFromParent() {
}, ['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);
- }
-}
diff --git a/chrome/content/zotero/renameFilesPreview.js b/chrome/content/zotero/renameFilesPreview.js
index 7d23fc336b..46d5f228cc 100644
--- a/chrome/content/zotero/renameFilesPreview.js
+++ b/chrome/content/zotero/renameFilesPreview.js
@@ -1,17 +1,17 @@
/*
***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2025 Corporation for Digital Scholarship
+
+ Copyright © 2026 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
@@ -19,7 +19,7 @@
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see .
-
+
***** END LICENSE BLOCK *****
*/
@@ -30,122 +30,146 @@ 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,
+ _rows: [],
+ _columns: [{ dataKey: 'name', label: '', primary: true, flex: 1 }],
+ _treeRef: null,
+ _root: 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._args = window.arguments[0].wrappedJSObject;
+ this._args.cancelled = true;
+ this.libraryID = this._args.libraryID;
+
+ this.introEl = document.getElementById('preview-intro');
+ this.filesListEl = document.getElementById('preview-files-list');
+ this.loadingEl = document.getElementById('preview-loading');
+ this.progressEl = document.getElementById('preview-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);
- },
+ this.acceptBtn = document.getElementById('preview-accept-btn');
+ this.cancelBtn = document.getElementById('preview-cancel-btn');
- 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);
- },
+ this.cancelBtn.addEventListener('command', () => window.close());
+ this.acceptBtn.addEventListener('command', this.handleAccept.bind(this));
- 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);
+ if (this.libraryID === Zotero.Libraries.userLibraryID) {
+ document.l10n.setAttributes(this.introEl, 'rename-files-preview-intro');
}
else {
- this.rows = rows.flatMap(obj => [{ name: obj.oldName }, { name: obj.newName }, { type: 'separator' }]);
- this.acceptBtnEl.disabled = false;
- this.render();
+ let libraryName = Zotero.Libraries.get(this.libraryID).name;
+ document.l10n.setAttributes(this.introEl, 'rename-files-preview-intro-library', { library: libraryName });
+ }
+
+ this._rowRenderer = VirtualizedTable.makeRowRenderer(this._getRowData.bind(this));
+ setTimeout(this._loadPreview.bind(this), 0);
+ },
+
+ _loadPreview: async function () {
+ this._rows = [];
+ let results = await renameFilesFromParent({ libraryID: this.libraryID, pretend: true });
+ this.loadingEl.hidden = true;
+ this.acceptBtn.disabled = false;
+
+ if (results.length === 0) {
+ this._noFiles = true;
+ this.introEl.dataset.l10nId = 'rename-files-preview-no-files';
+ this.cancelBtn.hidden = true;
+ document.l10n.setAttributes(this.acceptBtn, 'file-renaming-done-button');
+
+ // Mark as done for user library
+ if (this.libraryID === Zotero.Libraries.userLibraryID) {
+ Zotero.Prefs.set('autoRenameFiles.done', true);
+ }
+ }
+ else {
+ this._rows = results.flatMap(obj => [
+ { name: obj.oldName },
+ { name: obj.newName },
+ { type: 'separator' }
+ ]);
+ this._render();
}
},
- updateProgress: async function (progress) {
- this.progressEl.value = progress;
+ handleAccept: async function () {
+ this._args.cancelled = false;
+ if (this._noFiles) {
+ window.close();
+ return;
+ }
+
+ this.filesListEl.hidden = true;
+ this.progressEl.classList.remove('hidden');
+ this.introEl.dataset.l10nId = 'rename-files-preview-renaming';
+ this.cancelBtn.hidden = true;
+ this.acceptBtn.hidden = true;
+
+ await renameFilesFromParent({
+ libraryID: this.libraryID,
+ reportProgress: (progress) => {
+ this.progressEl.value = progress;
+ }
+ });
+
+ this._unmount();
+ window.close();
},
- render: function () {
+ _render: function () {
let customRowHeights = [];
- this.rows.forEach((row, index) => {
+ this._rows.forEach((row, index) => {
if (row.type === 'separator') {
customRowHeights.push([index, 8]);
}
});
- ReactDOM.createRoot(this.filesListEl).render((
+ this._root = ReactDOM.createRoot(this.filesListEl);
+ this._root.render((
this.rows.length}
- getRowHeight={this.getRowHeight.bind(this)}
+ 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)}
+ 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') {
+ _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;
- }
+ 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';
+ _getIsSelectable: function (index) {
+ return this._rows[index].type !== 'separator';
},
- getRowData: function (index) {
- return this.rows[index];
+ _getRowData: function (index) {
+ return this._rows[index];
},
- getRowHeight: function ({ _renderedTextHeight }) {
+ _getRowHeight: function ({ _renderedTextHeight }) {
return _renderedTextHeight;
+ },
+
+ _unmount: function () {
+ if (this._root) {
+ this._root.unmount();
+ this._root = null;
+ }
+ this._rows = [];
}
};
diff --git a/chrome/content/zotero/renameFilesPreview.xhtml b/chrome/content/zotero/renameFilesPreview.xhtml
index e07c21c13c..e46150e440 100644
--- a/chrome/content/zotero/renameFilesPreview.xhtml
+++ b/chrome/content/zotero/renameFilesPreview.xhtml
@@ -1,18 +1,18 @@
-
-
+
+
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/xpcom/data/group.js b/chrome/content/zotero/xpcom/data/group.js
index a2c0cef12f..c5d824030a 100644
--- a/chrome/content/zotero/xpcom/data/group.js
+++ b/chrome/content/zotero/xpcom/data/group.js
@@ -287,11 +287,13 @@ Zotero.Group.prototype.fromJSON = function (json, userID) {
var editable = false;
var filesEditable = false;
+ var isAdmin = false;
if (userID) {
- ({ editable, filesEditable } = Zotero.Groups.getPermissionsFromJSON(json, userID));
+ ({ editable, filesEditable, isAdmin } = Zotero.Groups.getPermissionsFromJSON(json, userID));
}
this.editable = editable;
this.filesEditable = filesEditable;
+ this.isAdmin = isAdmin;
}
Zotero.Group.prototype._prepFieldChange = function (field) {
diff --git a/chrome/content/zotero/xpcom/data/groups.js b/chrome/content/zotero/xpcom/data/groups.js
index fe5f0dd0cd..67d51ca459 100644
--- a/chrome/content/zotero/xpcom/data/groups.js
+++ b/chrome/content/zotero/xpcom/data/groups.js
@@ -124,9 +124,11 @@ Zotero.Groups = new function () {
var editable = false;
var filesEditable = false;
+ var isAdmin = false;
// If user is owner or admin, make library editable, and make files editable unless they're
// disabled altogether
if (json.owner == userID || (json.admins && json.admins.indexOf(userID) != -1)) {
+ isAdmin = true;
editable = true;
if (json.fileEditing != 'none') {
filesEditable = true;
@@ -141,6 +143,6 @@ Zotero.Groups = new function () {
}
}
}
- return { editable, filesEditable };
+ return { editable, filesEditable, isAdmin };
};
}
diff --git a/chrome/content/zotero/xpcom/data/library.js b/chrome/content/zotero/xpcom/data/library.js
index 22fefa57c5..70f9ae0e7f 100644
--- a/chrome/content/zotero/xpcom/data/library.js
+++ b/chrome/content/zotero/xpcom/data/library.js
@@ -71,7 +71,7 @@ Zotero.Library = function (params = {}) {
// DB columns
Zotero.defineProperty(Zotero.Library, '_dbColumns', {
value: Object.freeze([
- 'type', 'editable', 'filesEditable', 'version', 'storageVersion', 'lastSync', 'archived'
+ 'type', 'editable', 'filesEditable', 'version', 'storageVersion', 'lastSync', 'archived', 'isAdmin'
])
});
@@ -209,7 +209,7 @@ Zotero.defineProperty(Zotero.Library.prototype, 'allowsLinkedFiles', {
// Create other accessors
(function () {
- let accessors = ['editable', 'filesEditable', 'storageVersion', 'archived'];
+ let accessors = ['editable', 'filesEditable', 'storageVersion', 'archived', 'isAdmin'];
for (let i=0; idocumentation for more information.
-preferences-file-renaming-format-template = Filename Template:
-preferences-file-renaming-format-preview = Preview:
+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-configure-button =
+ .label = Configure File Renaming…
preferences-attachment-titles-title = Attachment Titles
preferences-attachment-titles-intro = Attachment titles are different from filenames. To support some workflows, { -app-name } can show filenames instead of attachment titles in the items list.
diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl
index d828200f4b..44f1af3659 100644
--- a/chrome/locale/en-US/zotero/zotero.ftl
+++ b/chrome/locale/en-US/zotero/zotero.ftl
@@ -654,6 +654,52 @@ new-collection-create-in = Create in:
show-publications-menuitem =
.label = Show My Publications
+file-renaming-settings-window =
+ .title = Configure File Renaming
+file-renaming-settings-library-label = Library:
+file-renaming-auto-rename-files =
+ .label = Automatically rename files
+file-renaming-file-types = Rename files of these types:
+file-renaming-file-type-pdf =
+ .label = { file-type-pdf }
+file-renaming-file-type-epub =
+ .label = { file-type-ebook }
+file-renaming-file-type-image =
+ .label = { file-type-image }
+file-renaming-file-type-audio =
+ .label = { file-type-audio }
+file-renaming-file-type-video =
+ .label = { file-type-video }
+file-renaming-rename-now =
+ .label = Rename Files…
+file-renaming-rename-linked =
+ .label = Rename linked files
+file-renaming-format-title = Filename Format
+file-renaming-format-instructions = You can customize the filename pattern { -app-name } uses to rename attachment files from parent metadata.
+file-renaming-format-instructions-example = For example, "{ $example }" in this template will be replaced with the title of the parent item, truncated at 50 characters.
+file-renaming-format-instructions-more = See the documentation for more information.
+file-renaming-format-template = Filename Template:
+file-renaming-format-preview = Preview:
+file-renaming-preview-changes = Preview Changes…
+file-renaming-rename-files =
+ .label = Rename Files
+file-renaming-done-button =
+ .label = { general-done }
+file-renaming-cancel-button =
+ .label = { general-cancel }
+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-body-library = Would you like to rename existing files in library "{ $library }" to match the new settings?
+file-renaming-auto-rename-prompt-yes = { file-renaming-preview-changes }
+file-renaming-auto-rename-prompt-no = Keep Existing Filenames
+rename-files-preview-window =
+ .title = 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-intro-library = { -app-name } will rename the following files in "{ $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.
+
attachment-info-title = Title
attachment-info-filename = Filename
attachment-info-accessed = Accessed
@@ -700,17 +746,6 @@ account-log-in = Log In
account-not-logged-in-text = Log in to your Zotero account to sync your data.
account-error-login-session-expired = Your login session has expired. Please try again.
-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 = {
@@ -908,3 +943,4 @@ banner-close-button =
plugins-blocked-plugin =
.message = This plugin has been disabled by { -app-name }.
+
diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql
index 715cca5786..78ff6d3566 100644
--- a/resource/schema/userdata.sql
+++ b/resource/schema/userdata.sql
@@ -1,4 +1,4 @@
--- 124
+-- 125
-- Copyright (c) 2009 Center for History and New Media
-- George Mason University, Fairfax, Virginia, USA
@@ -397,7 +397,8 @@ CREATE TABLE libraries (
version INT NOT NULL DEFAULT 0,
storageVersion INT NOT NULL DEFAULT 0,
lastSync INT NOT NULL DEFAULT 0,
- archived INT NOT NULL DEFAULT 0
+ archived INT NOT NULL DEFAULT 0,
+ isAdmin INT NOT NULL DEFAULT 0
);
CREATE TABLE users (
diff --git a/scss/_zotero.scss b/scss/_zotero.scss
index de4f1af786..e027b26b82 100644
--- a/scss/_zotero.scss
+++ b/scss/_zotero.scss
@@ -53,7 +53,6 @@
@import "components/publications-dialog";
@import "components/readAloudFirstRunDialog";
@import "components/readAloudVoicesDialog";
-@import "components/renameFilesPreview";
@import "components/richlistbox";
@import "components/rtfScan";
@import "components/runJS";
@@ -73,6 +72,8 @@
@import "components/window";
@import "components/newCollectionDialog";
@import "components/reader";
+@import "components/fileRenamingDialog";
+@import "components/renameFilesPreview";
// Elements
@@ -114,3 +115,4 @@
@import "elements/itemPane";
@import "elements/itemPaneCustomSection";
@import "elements/contextPane";
+@import "elements/fileRenamingSettings";
\ No newline at end of file
diff --git a/scss/components/_fileRenamingDialog.scss b/scss/components/_fileRenamingDialog.scss
new file mode 100644
index 0000000000..6e2d4425b5
--- /dev/null
+++ b/scss/components/_fileRenamingDialog.scss
@@ -0,0 +1,33 @@
+#file-renaming-settings-dialog {
+ padding: 14px 24px 24px;
+
+ > groupbox {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ }
+
+ #library-picker-row {
+ margin-bottom: 1em;
+ gap: 0.5em;
+ }
+
+ #library-picker-row > label {
+ font-size: 15px;
+ font-weight: 600;
+ }
+
+ #library-picker {
+ width: 14em;
+ margin-inline-start: .25em;
+ font-size: 15px;
+ height: 1.6em;
+ }
+
+ #file-renaming-buttons {
+ justify-content: flex-end;
+ gap: 0.5em;
+ margin-top: auto;
+ padding-top: 0.5em;
+ }
+}
diff --git a/scss/components/_renameFilesPreview.scss b/scss/components/_renameFilesPreview.scss
index ec92cfa407..c05e3f8b88 100644
--- a/scss/components/_renameFilesPreview.scss
+++ b/scss/components/_renameFilesPreview.scss
@@ -1,47 +1,54 @@
-#rename-files-preview {
- min-width: 800px;
- min-height: 500px;
+#rename-files-preview-content {
+ padding: 14px 24px 24px;
+ flex: 1;
- 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-container {
- flex: 1 1 auto;
- display: flex;
- overflow: hidden;
- border: var(--material-panedivider);
+ .virtualized-table-wrapper {
+ width: 100%;
+ }
- .virtualized-table-wrapper {
- width: 100%;
- }
+ .virtualized-table-loading {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
- .virtualized-table-loading {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
+ &[hidden] {
+ display: none;
+ }
+ }
- .virtualized-table {
- overflow: hidden;
+ .virtualized-table {
+ overflow: hidden;
- .row {
- &.new:not(.selected) {
- color: var(--fill-primary);
- }
+ .row {
+ &.new:not(.selected) {
+ color: var(--fill-primary);
+ }
- &.old:not(.selected) {
- color: var(--fill-secondary);
- }
- }
- }
- }
+ &.old:not(.selected) {
+ color: var(--fill-secondary);
+ }
+ }
+ }
+ }
- progress {
- &.hidden {
- display: none;
- }
- }
-}
\ No newline at end of file
+ #preview-intro {
+ margin-block: 0 0.5em;
+ }
+
+ progress.hidden {
+ display: none;
+ }
+
+ #preview-buttons {
+ justify-content: flex-end;
+ gap: 0.5em;
+ margin-top: 0.5em;
+ }
+}
diff --git a/scss/elements/_fileRenamingSettings.scss b/scss/elements/_fileRenamingSettings.scss
new file mode 100644
index 0000000000..2f3574a3ed
--- /dev/null
+++ b/scss/elements/_fileRenamingSettings.scss
@@ -0,0 +1,29 @@
+file-renaming-settings {
+ #file-renaming-format-template {
+ min-height: 3.25em;
+ padding: 0.4em;
+ /* Match label margins */
+ margin-block: 1px 2px;
+ margin-inline: 6px 5px;
+
+ &:read-only {
+ opacity: 0.5;
+ }
+ }
+
+ #file-renaming-format-preview {
+ padding: 0.4em;
+ }
+
+ #file-rename-settings-section-instructions {
+ margin-top: .5em;
+ }
+
+ #file-renaming-format-template-label h2 {
+ margin-top: 0;
+ }
+
+ .indented-pref {
+ margin-inline-start: 2em;
+ }
+}
diff --git a/scss/preferences.scss b/scss/preferences.scss
index 187bc07caa..24f953e325 100644
--- a/scss/preferences.scss
+++ b/scss/preferences.scss
@@ -39,13 +39,14 @@
// --------------------------------------------------
@import "preferences/general";
-@import "preferences/file_renaming";
@import "preferences/sync";
@import "preferences/sync_reset";
@import "preferences/export";
@import "preferences/cite";
@import "preferences/advanced";
+@import "elements/fileRenamingSettings";
+
@include macOS-normalize-controls;
#zotero-prefs {
diff --git a/scss/preferences/_file_renaming.scss b/scss/preferences/_file_renaming.scss
deleted file mode 100644
index a039a2d74f..0000000000
--- a/scss/preferences/_file_renaming.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-#file-renaming-buttons {
- margin-top: .6em;
-
- > button + button {
- margin-left: 0.5em;
- }
-}
-
-#zotero-prefpane-file-renaming-format label:not([is=zotero-text-link]) {
- display: block;
-}
-
-#file-renaming-format-template {
- min-height: 3.25em;
- padding: 0.4em;
- /* Match label margins */
- margin-block: 1px 2px;
- margin-inline: 6px 5px;
-}
-
-#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;
-}
\ No newline at end of file
diff --git a/scss/preferences/_general.scss b/scss/preferences/_general.scss
index 013ef418ab..a2d57a85c6 100644
--- a/scss/preferences/_general.scss
+++ b/scss/preferences/_general.scss
@@ -22,11 +22,6 @@
min-width: 12em;
}
-@media (-moz-platform: macos) {
- #zotero-prefpane-file-renaming-file-types-box {
- gap: 8px;
- }
-}
@media (-moz-platform: windows) {
button, menulist, radio, checkbox, input {
@@ -42,3 +37,4 @@
visibility: hidden;
}
}
+