6 <title>QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]
</title>
7 <script src=
"scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
8 <script src=
"scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
9 <script src=
"scripts/filesystem.js?v=${CACHEID}"></script>
10 <script src=
"scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
11 <script src=
"scripts/file-tree.js?v=${CACHEID}"></script>
12 <script src=
"scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script>
13 <script src=
"scripts/rename-files.js?v=${CACHEID}"></script>
21 } = window
.MUI
.Windows
.instances
["multiRenamePage"];
23 const bulkRenameFilesContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
24 targets
: "#bulkRenameFilesTableDiv tr",
25 menu
: "multiRenameFilesMenu",
27 ToggleSelection
: (element
, ref
) => {
28 const rowId
= Number(element
.getAttribute("data-row-id"));
29 const row
= bulkRenameFilesTable
.getNode(rowId
);
30 const checkState
= (row
.checked
=== 1) ? 0 : 1;
31 bulkRenameFilesTable
.toggleNodeTreeCheckbox(rowId
, checkState
);
32 bulkRenameFilesTable
.updateGlobalCheckbox();
33 bulkRenameFilesTable
.onRowSelectionChange(bulkRenameFilesTable
.getSelectedRows());
42 // Setup the dynamic table for bulk renaming
43 const bulkRenameFilesTable
= new window
.qBittorrent
.DynamicTable
.BulkRenameTorrentFilesTable();
44 bulkRenameFilesTable
.setup("bulkRenameFilesTableDiv", "bulkRenameFilesTableFixedHeaderDiv", bulkRenameFilesContextMenu
);
46 // Inject checkbox into the first column of the table header
47 const tableHeaders
= $$("#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th");
48 if (tableHeaders
.length
> 0) {
49 const checkboxHeader
= document
.createElement("input");
50 checkboxHeader
.type
= "checkbox";
51 checkboxHeader
.id
= "rootMultiRename_cb";
52 checkboxHeader
.addEventListener("click", (e
) => {
53 bulkRenameFilesTable
.toggleGlobalCheckbox();
54 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
58 const checkboxTH
= tableHeaders
[0];
59 checkboxTH
.append(checkboxHeader
);
62 // Register keyboard events to modal window
63 // https://github.com/qbittorrent/qBittorrent/pull/18687#discussion_r1135045726
65 defaultEventType
: "keydown",
67 "Escape": function(event
) {
68 window
.qBittorrent
.Client
.closeWindow(windowEl
);
69 event
.preventDefault();
71 "Esc": function(event
) {
72 window
.qBittorrent
.Client
.closeWindow(windowEl
);
73 event
.preventDefault();
78 const fileRenamer
= new window
.qBittorrent
.MultiRename
.RenameFiles();
79 fileRenamer
.hash
= data
.hash
;
81 // Load Multi Rename Preferences
82 const multiRenamePrefChecked
= LocalPreferences
.get("multirename_rememberPreferences", "true") === "true";
83 $("multirename_rememberprefs_checkbox").checked
= multiRenamePrefChecked
;
85 if (multiRenamePrefChecked
) {
86 const multirename_search
= LocalPreferences
.get("multirename_search", "");
87 fileRenamer
.setSearch(multirename_search
);
88 $("multiRenameSearch").value
= multirename_search
;
90 const multirename_useRegex
= LocalPreferences
.get("multirename_useRegex", false);
91 fileRenamer
.useRegex
= multirename_useRegex
=== "true";
92 $("use_regex_search").checked
= fileRenamer
.useRegex
;
94 const multirename_matchAllOccurrences
= LocalPreferences
.get("multirename_matchAllOccurrences", false);
95 fileRenamer
.matchAllOccurrences
= multirename_matchAllOccurrences
=== "true";
96 $("match_all_occurrences").checked
= fileRenamer
.matchAllOccurrences
;
98 const multirename_caseSensitive
= LocalPreferences
.get("multirename_caseSensitive", false);
99 fileRenamer
.caseSensitive
= multirename_caseSensitive
=== "true";
100 $("case_sensitive").checked
= fileRenamer
.caseSensitive
;
102 const multirename_replace
= LocalPreferences
.get("multirename_replace", "");
103 fileRenamer
.setReplacement(multirename_replace
);
104 $("multiRenameReplace").value
= multirename_replace
;
106 const multirename_appliesTo
= LocalPreferences
.get("multirename_appliesTo", window
.qBittorrent
.MultiRename
.AppliesTo
.FilenameExtension
);
107 fileRenamer
.appliesTo
= window
.qBittorrent
.MultiRename
.AppliesTo
[multirename_appliesTo
];
108 $("applies_to_option").value
= fileRenamer
.appliesTo
;
110 const multirename_includeFiles
= LocalPreferences
.get("multirename_includeFiles", true);
111 fileRenamer
.includeFiles
= multirename_includeFiles
=== "true";
112 $("include_files").checked
= fileRenamer
.includeFiles
;
114 const multirename_includeFolders
= LocalPreferences
.get("multirename_includeFolders", false);
115 fileRenamer
.includeFolders
= multirename_includeFolders
=== "true";
116 $("include_folders").checked
= fileRenamer
.includeFolders
;
118 const multirename_fileEnumerationStart
= LocalPreferences
.get("multirename_fileEnumerationStart", 0);
119 fileRenamer
.fileEnumerationStart
= Number(multirename_fileEnumerationStart
);
120 $("file_counter").value
= fileRenamer
.fileEnumerationStart
;
122 const multirename_replaceAll
= LocalPreferences
.get("multirename_replaceAll", false);
123 fileRenamer
.replaceAll
= multirename_replaceAll
=== "true";
124 const renameButtonValue
= fileRenamer
.replaceAll
? "Replace All" : "Replace";
125 $("renameOptions").value
= renameButtonValue
;
126 $("renameButton").value
= renameButtonValue
;
129 // Fires every time a row's selection changes
130 bulkRenameFilesTable
.onRowSelectionChange
= (row
) => {
131 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
132 fileRenamer
.update();
135 // Setup Search Events that control renaming
136 $("multiRenameSearch").addEventListener("input", (e
) => {
137 const sanitized
= e
.target
.value
.replace(/\n/g, "");
138 $("multiRenameSearch").value
= sanitized
;
140 // Search input has changed
141 $("multiRenameSearch").style
["border-color"] = "";
142 LocalPreferences
.set("multirename_search", sanitized
);
143 fileRenamer
.setSearch(sanitized
);
145 $("use_regex_search").addEventListener("change", (e
) => {
146 fileRenamer
.useRegex
= e
.target
.checked
;
147 LocalPreferences
.set("multirename_useRegex", e
.target
.checked
);
148 fileRenamer
.update();
150 $("match_all_occurrences").addEventListener("change", (e
) => {
151 fileRenamer
.matchAllOccurrences
= e
.target
.checked
;
152 LocalPreferences
.set("multirename_matchAllOccurrences", e
.target
.checked
);
153 fileRenamer
.update();
155 $("case_sensitive").addEventListener("change", (e
) => {
156 fileRenamer
.caseSensitive
= e
.target
.checked
;
157 LocalPreferences
.set("multirename_caseSensitive", e
.target
.checked
);
158 fileRenamer
.update();
162 * Fires every time the filerenamer gets changed, it will update all the rows in the table
164 fileRenamer
.onChanged
= (matchedRows
) => {
165 // Clear renamed column
167 .querySelectorAll("span[id^='filesTablefileRenamed']")
169 span
.textContent
= "";
172 // Update renamed column for matched rows
173 for (let i
= 0; i
< matchedRows
.length
; ++i
) {
174 const row
= matchedRows
[i
];
175 $("filesTablefileRenamed" + row
.rowId
).textContent
= row
.renamed
;
178 fileRenamer
.onInvalidRegex
= (err
) => {
179 $("multiRenameSearch").style
["border-color"] = "#CC0033";
182 // Setup Replace Events that control renaming
183 $("multiRenameReplace").addEventListener("input", (e
) => {
184 const sanitized
= e
.target
.value
.replace(/\n/g, "");
185 $("multiRenameReplace").value
= sanitized
;
187 // Replace input has changed
188 $("multiRenameReplace").style
["border-color"] = "";
189 LocalPreferences
.set("multirename_replace", sanitized
);
190 fileRenamer
.setReplacement(sanitized
);
192 $("applies_to_option").addEventListener("change", (e
) => {
193 fileRenamer
.appliesTo
= e
.target
.value
;
194 LocalPreferences
.set("multirename_appliesTo", e
.target
.value
);
195 fileRenamer
.update();
197 $("include_files").addEventListener("change", (e
) => {
198 fileRenamer
.includeFiles
= e
.target
.checked
;
199 LocalPreferences
.set("multirename_includeFiles", e
.target
.checked
);
200 fileRenamer
.update();
202 $("include_folders").addEventListener("change", (e
) => {
203 fileRenamer
.includeFolders
= e
.target
.checked
;
204 LocalPreferences
.set("multirename_includeFolders", e
.target
.checked
);
205 fileRenamer
.update();
207 $("file_counter").addEventListener("input", (e
) => {
208 let value
= e
.target
.valueAsNumber
;
213 if (value
> 99999999)
215 fileRenamer
.fileEnumerationStart
= value
;
216 $("file_counter").value
= value
;
217 LocalPreferences
.set("multirename_fileEnumerationStart", value
);
218 fileRenamer
.update();
221 // Setup Rename Operation Events
222 $("renameButton").addEventListener("click", (e
) => {
223 // Disable Search Options
224 $("multiRenameSearch").disabled
= true;
225 $("use_regex_search").disabled
= true;
226 $("match_all_occurrences").disabled
= true;
227 $("case_sensitive").disabled
= true;
228 // Disable Replace Options
229 $("multiRenameReplace").disabled
= true;
230 $("applies_to_option").disabled
= true;
231 $("include_files").disabled
= true;
232 $("include_folders").disabled
= true;
233 $("file_counter").disabled
= true;
234 // Disable Rename Buttons
235 $("renameButton").disabled
= true;
236 $("renameOptions").disabled
= true;
238 $("rename_error").textContent
= "";
239 fileRenamer
.rename();
241 fileRenamer
.onRenamed
= (rows
) => {
242 // Disable Search Options
243 $("multiRenameSearch").disabled
= false;
244 $("use_regex_search").disabled
= false;
245 $("match_all_occurrences").disabled
= false;
246 $("case_sensitive").disabled
= false;
247 // Disable Replace Options
248 $("multiRenameReplace").disabled
= false;
249 $("applies_to_option").disabled
= false;
250 $("include_files").disabled
= false;
251 $("include_folders").disabled
= false;
252 $("file_counter").disabled
= false;
253 // Disable Rename Buttons
254 $("renameButton").disabled
= false;
255 $("renameOptions").disabled
= false;
258 let selectedRows
= bulkRenameFilesTable
.getSelectedRows().map(row
=> row
.rowId
.toString());
259 for (const renamedRow
of rows
)
260 selectedRows
= selectedRows
.filter(selectedRow
=> selectedRow
!== renamedRow
.rowId
.toString());
261 bulkRenameFilesTable
.clear();
263 // Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts
264 if (!fileRenamer
.replaceAll
) {
265 fileRenamer
.fileEnumerationStart
++;
266 $("file_counter").value
= fileRenamer
.fileEnumerationStart
;
268 setupTable(selectedRows
);
270 fileRenamer
.onRenameError
= (err
, row
) => {
271 if (err
.xhr
.status
=== 409)
272 $("rename_error").textContent
= `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}
\``;
274 $("renameOptions").addEventListener("change", (e
) => {
275 const combobox
= e
.target
;
276 const replaceOperation
= combobox
.value
;
277 if (replaceOperation
=== "Replace")
278 fileRenamer
.replaceAll
= false;
279 else if (replaceOperation
=== "Replace All")
280 fileRenamer
.replaceAll
= true;
282 fileRenamer
.replaceAll
= false;
283 LocalPreferences
.set("multirename_replaceAll", fileRenamer
.replaceAll
);
284 $("renameButton").value
= replaceOperation
;
286 $("closeButton").addEventListener("click", (event
) => {
287 event
.preventDefault();
288 window
.qBittorrent
.Client
.closeWindow(windowEl
);
290 // synchronize header scrolling to table body
291 $("bulkRenameFilesTableDiv").onscroll = function() {
292 const length
= $(this).scrollLeft
;
293 $("bulkRenameFilesTableFixedHeaderDiv").scrollLeft
= length
;
296 const handleTorrentFiles
= (files
, selectedRows
) => {
297 const rows
= files
.map((file
, index
) => {
301 checked
: 1, // unchecked
303 original
: window
.qBittorrent
.Filesystem
.fileName(file
.name
),
311 addRowsToTable(rows
, selectedRows
);
314 const addRowsToTable
= (rows
, selectedRows
) => {
316 const rootNode
= new window
.qBittorrent
.FileTree
.FolderNode();
317 rootNode
.autoCheckFolders
= false;
319 rows
.forEach((row
) => {
320 const pathItems
= row
.path
.split(window
.qBittorrent
.Filesystem
.PathSeparator
);
322 pathItems
.pop(); // remove last item (i.e. file name)
323 let parent
= rootNode
;
324 pathItems
.forEach((folderName
) => {
325 if (folderName
=== ".unwanted")
328 let folderNode
= null;
329 if (parent
.children
!== null) {
330 for (let i
= 0; i
< parent
.children
.length
; ++i
) {
331 const childFolder
= parent
.children
[i
];
332 if (childFolder
.original
=== folderName
) {
333 folderNode
= childFolder
;
339 if (folderNode
=== null) {
340 folderNode
= new window
.qBittorrent
.FileTree
.FolderNode();
341 folderNode
.autoCheckFolders
= false;
342 folderNode
.rowId
= rowId
;
343 folderNode
.path
= (parent
.path
=== "")
345 : [parent
.path
, folderName
].join(window
.qBittorrent
.Filesystem
.PathSeparator
);
346 folderNode
.checked
= selectedRows
.includes(rowId
.toString()) ? 0 : 1;
347 folderNode
.original
= folderName
;
348 folderNode
.renamed
= "";
349 folderNode
.root
= parent
;
350 parent
.addChild(folderNode
);
358 const childNode
= new window
.qBittorrent
.FileTree
.FileNode();
359 childNode
.rowId
= rowId
;
360 childNode
.path
= row
.path
;
361 childNode
.checked
= selectedRows
.includes(rowId
.toString()) ? 0 : 1;
362 childNode
.original
= row
.original
;
363 childNode
.renamed
= "";
364 childNode
.root
= parent
;
365 childNode
.data
= row
;
366 parent
.addChild(childNode
);
371 bulkRenameFilesTable
.populateTable(rootNode
);
372 bulkRenameFilesTable
.updateTable(false);
374 if (selectedRows
!== undefined)
375 bulkRenameFilesTable
.reselectRows(selectedRows
);
377 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
378 fileRenamer
.update();
381 const setupTable
= (selectedRows
) => {
382 fetch(new URI("api/v2/torrents/files").setData("hash", data
.hash
), {
386 .then(async (response
) => {
390 const files
= await response
.json();
391 if (files
.length
=== 0)
392 bulkRenameFilesTable
.clear();
394 handleTorrentFiles(files
, selectedRows
);
397 setupTable(data
.selectedRows
);
402 <body style=
"min-width: 400px; min-height: 300px;">
403 <div style=
"padding: 0px 10px 0px 0px;">
404 <div style=
"float: left; height: 100%; width: 228px;">
405 <div class=
"formRow">
406 <input type=
"checkbox" id=
"multirename_rememberprefs_checkbox" onchange=
"LocalPreferences.set('multirename_rememberPreferences', this.checked);">
407 <label for=
"multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]
</label>
410 <textarea id=
"multiRenameSearch" placeholder=
"QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" aria-label=
"QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" style=
"width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea>
411 <div class=
"formRow">
412 <input type=
"checkbox" id=
"use_regex_search">
413 <label for=
"use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]
</label>
415 <div class=
"formRow">
416 <input type=
"checkbox" id=
"match_all_occurrences">
417 <label for=
"match_all_occurrences">QBT_TR(Match all occurrences)QBT_TR[CONTEXT=PropertiesWidget]
</label>
419 <div class=
"formRow">
420 <input type=
"checkbox" id=
"case_sensitive">
421 <label for=
"case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]
</label>
424 <textarea id=
"multiRenameReplace" placeholder=
"QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" aria-label=
"QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" style=
"width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea>
425 <select id=
"applies_to_option" name=
"applies_to_option" aria-label=
"QBT_TR(Apply to which filename part)QBT_TR[CONTEXT=PropertiesWidget]" style=
"width: 100%; margin-bottom: 5px;">
426 <option selected
value=
"FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]
</option>
427 <option value=
"Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]
</option>
428 <option value=
"Extension">QBT_TR(Extension)QBT_TR[CONTEXT=PropertiesWidget]
</option>
430 <div class=
"formRow">
431 <input type=
"checkbox" id=
"include_files" checked
>
432 <label for=
"include_files">QBT_TR(Include files)QBT_TR[CONTEXT=PropertiesWidget]
</label>
434 <div class=
"formRow">
435 <input type=
"checkbox" id=
"include_folders">
436 <label for=
"include_folders">QBT_TR(Include folders)QBT_TR[CONTEXT=PropertiesWidget]
</label>
438 <div class=
"formRow">
439 <input type=
"number" min=
"0" max=
"99999999" value=
"0" id=
"file_counter" style=
"width: 88px;">
440 <label for=
"file_counter">QBT_TR(Enumerate Files)QBT_TR[CONTEXT=PropertiesWidget]
</label>
443 <div id=
"operation_btns" style=
"position: absolute; left: 0; bottom: 0; margin: 0 12px 50px 12px; width: 228px;">
444 <div style=
"overflow: auto;">
445 <span id=
"rename_error" style=
"float: unset; font-size: unset;"></span>
448 <div style=
"width: 60%; float: left;">
449 <input id=
"renameButton" type=
"button" value=
"Replace" style=
"float: left; width: 86px;">
450 <select id=
"renameOptions" name=
"renameOptions" aria-label=
"QBT_TR(Replace option)QBT_TR[CONTEXT=PropertiesWidget]" style=
"width: 22px;">
451 <option selected
value=
"Replace">QBT_TR(Replace)QBT_TR[CONTEXT=PropertiesWidget]
</option>
452 <option value=
"Replace All">QBT_TR(Replace All)QBT_TR[CONTEXT=PropertiesWidget]
</option>
455 <input id=
"closeButton" type=
"button" value=
"Close" style=
"float: right; width: 30%;">
457 <div id=
"torrentFiles" class=
"panel" style=
"position: absolute; top: 0; right: 0; bottom: 0; left: 228px; margin: 35px 10px 45px 20px; border-bottom: 0; height: initial;">
458 <div id=
"bulkRenameFilesTableFixedHeaderDiv" class=
"dynamicTableFixedHeaderDiv">
459 <table class=
"dynamicTable">
461 <tr class=
"dynamicTableHeader"></tr>
465 <div id=
"bulkRenameFilesTableDiv" class=
"dynamicTableDiv">
466 <table class=
"dynamicTable">
468 <tr class=
"dynamicTableHeader"></tr>