Per-group file renaming settings (#5862)

Also:

- Add `isAdmin` property to libraries
 
---------

Co-authored-by: Dan Stillman <dstillman@zotero.org>
This commit is contained in:
Tom Najdek 2026-04-02 21:15:06 +02:00 committed by GitHub
parent 41b5442952
commit 862573eabc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1084 additions and 665 deletions

View file

@ -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);

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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(`
<vbox>
<groupbox id="file-rename-settings-section-main">
<checkbox id="auto-rename-files"
data-l10n-id="file-renaming-auto-rename-files"
native="true"
/>
<vbox class="indented-pref" aria-labelledby="file-renaming-file-types" role="group">
<label id="file-renaming-file-types" data-l10n-id="file-renaming-file-types"/>
<hbox
id="file-renaming-file-types-box"
class="indented-pref"
>
<checkbox
data-l10n-id="file-renaming-file-type-pdf"
data-content-type="application/pdf"
native="true"
/>
<checkbox
data-l10n-id="file-renaming-file-type-epub"
data-content-type="application/epub+zip"
native="true"
/>
<checkbox
data-l10n-id="file-renaming-file-type-image"
data-content-type="image/"
native="true"
/>
<checkbox
data-l10n-id="file-renaming-file-type-audio"
data-content-type="audio/"
native="true"
/>
<checkbox
data-l10n-id="file-renaming-file-type-video"
data-content-type="video/"
native="true"
/>
</hbox>
</vbox>
<checkbox id="rename-linked-files" class="indented-pref"
data-l10n-id="file-renaming-rename-linked"
preference="extensions.zotero.autoRenameFiles.linked"
native="true"
/>
</groupbox>
<groupbox id="file-rename-settings-section-instructions">
<label data-l10n-id="file-renaming-format-instructions" />
<separator class="thin" />
<label data-l10n-id="file-renaming-format-instructions-example"
data-l10n-args='${JSON.stringify({ example: "{{ title truncate=\"50\" }}" })}' />
<separator class="thin" />
<label data-l10n-id="file-renaming-format-instructions-more">
<label
is="zotero-text-link"
href="https://www.zotero.org/support/file_renaming"
data-l10n-name="file-renaming-format-help-link"
/>
</label>
<separator class="thin" />
</groupbox>
<groupbox id="file-rename-settings-section-template">
<html:label
for="file-renaming-format-template"
id="file-renaming-format-template-label"
>
<html:h2 data-l10n-id="file-renaming-format-template" />
</html:label>
<html:textarea
aria-labelledby="file-renaming-format-template-label"
id="file-renaming-format-template"
rows="8"
/>
<html:label id="file-renaming-format-preview-label">
<html:h2
data-l10n-id="file-renaming-format-preview"
/>
</html:label>
<html:label
aria-labelledby="file-renaming-format-preview-label"
id="file-renaming-format-preview"
/>
</groupbox>
</vbox>
`);
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);
}

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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,
};
}
}
};

View file

@ -0,0 +1,68 @@
<?xml version="1.0"?>
<!--
***** 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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://zotero/skin/zotero.css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.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="file-renaming-settings-dialog"
data-l10n-id="file-renaming-settings-window"
width="800"
height="600"
onload="FileRenamingDialog.init()"
onclose="return FileRenamingDialog.handleWindowClose(event)"
>
<groupbox>
<script>
Services.scriptloader.loadSubScript('chrome://zotero/content/include.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/customElements.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/fileRenamingDialog.js', this);
</script>
<linkset>
<html:link rel="localization" href="branding/brand.ftl" />
<html:link rel="localization" href="zotero.ftl" />
</linkset>
<hbox align="center" id="library-picker-row">
<label data-l10n-id="file-renaming-settings-library-label" control="library-picker" />
<menulist id="library-picker" native="true">
<menupopup />
</menulist>
</hbox>
<file-renaming-settings id="file-renaming-settings" />
<hbox id="file-renaming-buttons">
<button id="file-renaming-rename-files-btn" data-l10n-id="file-renaming-rename-now" />
<button id="file-renaming-done-btn" data-l10n-id="file-renaming-done-button" />
</hbox>
</groupbox>
</window>

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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;
},
};

View file

@ -1,77 +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 <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
-->
<vbox
class="main-section" id="zotero-prefpane-file-renaming-format"
onload="Zotero_Preferences.FileRenaming.init()"
onunload="Zotero_Preferences.FileRenaming.uninit()"
>
<hbox class="header">
<html:h1 data-l10n-id="preferences-file-renaming-format-title" />
</hbox>
<label data-l10n-id="preferences-file-renaming-format-instructions" />
<separator class="thin" />
<label data-l10n-id="preferences-file-renaming-format-instructions-example"
data-l10n-args='{"example": "{{ title truncate=\"50\" }}"}' />
<separator class="thin" />
<label data-l10n-id="preferences-file-renaming-format-instructions-more">
<label
is="zotero-text-link"
href="https://www.zotero.org/support/file_renaming"
data-l10n-name="file-renaming-format-help-link"
/>
</label>
<separator class="thin" />
<groupbox>
<html:label
for="file-renaming-format-template"
id="file-renaming-format-template-label"
>
<html:h2 data-l10n-id="preferences-file-renaming-format-template" />
</html:label>
<html:textarea
aria-labelledby="file-renaming-format-template-label"
id="file-renaming-format-template"
rows="8"
/>
<html:label id="file-renaming-format-preview-label">
<html:h2
data-l10n-id="preferences-file-renaming-format-preview"
/>
</html:label>
<html:label
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>

View file

@ -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();
},
//

View file

@ -107,69 +107,15 @@
<groupbox id="zotero-prefpane-file-renaming-groupbox" aria-labelledby="preferences-file-renaming-title" aria-describedby="preferences-file-renaming-intro">
<label><html:h2 id="preferences-file-renaming-title" data-l10n-id="preferences-file-renaming-title"/></label>
<vbox align="start">
<label id="preferences-file-renaming-intro" data-l10n-id="preferences-file-renaming-intro"/>
<separator class="thin"/>
<checkbox id="auto-rename-files"
data-l10n-id="preferences-file-renaming-auto-rename-files"
preference="extensions.zotero.autoRenameFiles"
oncommand="setTimeout(() => { Zotero_Preferences.General.handleAutoRenameChange(); Zotero_Preferences.General.updateAutoRenameFilesUI(); })" native="true"
<button id="file-renaming-configure-button"
data-l10n-id="preferences-file-renaming-configure-button"
data-search-strings="file-renaming-format-title, file-renaming-format-template, file-renaming-auto-rename-files"
oncommand="Zotero_Preferences.General.openFileRenamingDialog()"
/>
<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"/>
<hbox
id="zotero-prefpane-file-renaming-file-types-box"
class="indented-pref"
oncommand="Zotero_Preferences.General.setAutoRenameFileTypes()"
>
<checkbox
data-l10n-id="preferences-file-renaming-file-type-pdf"
data-content-type="application/pdf"
native="true"
/>
<checkbox
data-l10n-id="preferences-file-renaming-file-type-epub"
data-content-type="application/epub+zip"
native="true"
/>
<checkbox
data-l10n-id="preferences-file-renaming-file-type-image"
data-content-type="image/"
native="true"
/>
<checkbox
data-l10n-id="preferences-file-renaming-file-type-audio"
data-content-type="audio/"
native="true"
/>
<checkbox
data-l10n-id="preferences-file-renaming-file-type-video"
data-content-type="video/"
native="true"
/>
</hbox>
</vbox>
<vbox class="indented-pref">
<checkbox id="rename-linked-files"
label="&zotero.preferences.autoRenameFiles.renameLinked;"
preference="extensions.zotero.autoRenameFiles.linked"
oncommand="Zotero_Preferences.General.updateAutoRenameFilesUI()" native="true"
/>
</vbox>
<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>

View file

@ -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<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 = () => {} } = {}) {
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
// dont 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);
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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((
<VirtualizedTable
columns={this.columns}
columns={this._columns}
containerWidth={this.filesListEl.clientWidth}
customRowHeights={customRowHeights}
disableFontSizeScaling={true}
getRowCount={() => 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 = [];
}
};

View file

@ -1,18 +1,18 @@
<?xml version="1.0"?>
<!--
***** 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
@ -20,41 +20,49 @@
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/skin/zotero.css"?>
<?xml-stylesheet href="chrome://zotero/skin/overlay.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>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
id="rename-files-preview"
data-l10n-id="rename-files-preview-window"
width="800"
height="500"
onload="RenameFilesPreview.init()"
>
<vbox id="rename-files-preview-content">
<script>
Services.scriptloader.loadSubScript('chrome://zotero/content/include.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" />
</linkset>
<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>
<vbox>
<html:p id="preview-intro" />
</vbox>
<vbox class="virtualized-table-container">
<html:div id="preview-files-list" class="virtualized-table-wrapper">
<html:div class="virtualized-table-loading" id="preview-loading">
<html:span data-l10n-id="rename-files-preview-loading" />
</html:div>
</html:div>
<html:progress id="preview-progress" max="1" value="0" />
</vbox>
<hbox id="preview-buttons">
<button id="preview-cancel-btn" data-l10n-id="file-renaming-cancel-button" />
<button id="preview-accept-btn" data-l10n-id="file-renaming-rename-files" disabled="true" />
</hbox>
</vbox>
</window>

View file

@ -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) {

View file

@ -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 };
};
}

View file

@ -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; i<accessors.length; i++) {
let prop = Zotero.Library._colToProp(accessors[i]);
Zotero.defineProperty(Zotero.Library.prototype, accessors[i], {
@ -353,7 +353,8 @@ Zotero.Library.prototype._loadDataFromRow = function (row) {
this._libraryStorageVersion = row._libraryStorageVersion;
this._libraryLastSync = row._libraryLastSync !== 0 ? new Date(row._libraryLastSync * 1000) : false;
this._libraryArchived = !!row._libraryArchived;
this._libraryIsAdmin = !!row._libraryIsAdmin;
this._hasCollections = !!row.hasCollections;
this._hasSearches = !!row.hasSearches;

View file

@ -89,15 +89,6 @@ Zotero.PreferencePanes = {
scripts: ['chrome://zotero/content/preferences/preferences_sync.js'],
defaultXUL: true,
helpURL: 'https://www.zotero.org/support/preferences/sync#reset',
},
{
id: 'zotero-subpane-file-renaming',
parent: 'zotero-prefpane-general',
label: '',
src: 'chrome://zotero/content/preferences/preferences_file_renaming.xhtml',
scripts: ['chrome://zotero/content/preferences/preferences_file_renaming.js'],
defaultXUL: true,
helpURL: null,
}
]),

View file

@ -3521,7 +3521,13 @@ Zotero.Schema = new function () {
await Zotero.DB.queryAsync("ALTER TABLE itemAttachments ADD COLUMN lastRead INT");
await Zotero.DB.queryAsync("CREATE INDEX itemAttachments_lastRead ON itemAttachments(lastRead)");
}
else if (i == 125) {
await Zotero.DB.queryAsync("ALTER TABLE libraries ADD COLUMN isAdmin INT NOT NULL DEFAULT 0");
// Force all groups to resync so isAdmin is populated from the API
await Zotero.DB.queryAsync("UPDATE groups SET version = 0");
}
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom
}

View file

@ -7,32 +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 } 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 files
preferences-file-renaming-file-types = Rename files of these types:
preferences-file-renaming-file-type-pdf =
.label = { file-type-pdf }
preferences-file-renaming-file-type-epub =
.label = { file-type-ebook }
preferences-file-renaming-file-type-image =
.label = { file-type-image }
preferences-file-renaming-file-type-audio =
.label = { file-type-audio }
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.
preferences-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.
preferences-file-renaming-format-instructions-more = See the <label data-l10n-name="file-renaming-format-help-link">documentation</label> 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 <label data-l10n-name="wiki-link">different from filenames</label>. To support some workflows, { -app-name } can show filenames instead of attachment titles in the items list.

View file

@ -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 <label data-l10n-name="file-renaming-format-help-link">documentation</label> 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 }.

View file

@ -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 (

View file

@ -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";

View file

@ -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;
}
}

View file

@ -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;
}
}
}
#preview-intro {
margin-block: 0 0.5em;
}
progress.hidden {
display: none;
}
#preview-buttons {
justify-content: flex-end;
gap: 0.5em;
margin-top: 0.5em;
}
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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;
}

View file

@ -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;
}
}