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>
17 if (window
.parent
.qBittorrent
!== undefined)
18 window
.qBittorrent
= window
.parent
.qBittorrent
;
19 window
.qBittorrent
= window
.parent
.qBittorrent
;
21 const TriState
= window
.qBittorrent
.FileTree
.TriState
;
22 const data
= window
.MUI
.Windows
.instances
["multiRenamePage"].options
.data
;
23 let bulkRenameFilesContextMenu
;
24 if (!bulkRenameFilesContextMenu
) {
25 bulkRenameFilesContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
26 targets
: "#bulkRenameFilesTableDiv tr",
27 menu
: "multiRenameFilesMenu",
29 ToggleSelection: function(element
, ref
) {
30 const rowId
= parseInt(element
.getAttribute("data-row-id"), 10);
31 const row
= bulkRenameFilesTable
.getNode(rowId
);
32 const checkState
= (row
.checked
=== 1) ? 0 : 1;
33 bulkRenameFilesTable
.toggleNodeTreeCheckbox(rowId
, checkState
);
34 bulkRenameFilesTable
.updateGlobalCheckbox();
35 bulkRenameFilesTable
.onRowSelectionChange(bulkRenameFilesTable
.getSelectedRows());
45 // Setup the dynamic table for bulk renaming
46 const bulkRenameFilesTable
= new window
.qBittorrent
.DynamicTable
.BulkRenameTorrentFilesTable();
47 bulkRenameFilesTable
.setup("bulkRenameFilesTableDiv", "bulkRenameFilesTableFixedHeaderDiv", bulkRenameFilesContextMenu
);
49 // Inject checkbox into the first column of the table header
50 const tableHeaders
= $$("#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th");
52 if (tableHeaders
.length
> 0) {
54 checkboxHeader
.remove();
55 checkboxHeader
= new Element("input");
56 checkboxHeader
.type
= "checkbox";
57 checkboxHeader
.id
= "rootMultiRename_cb";
58 checkboxHeader
.addEventListener("click", (e
) => {
59 bulkRenameFilesTable
.toggleGlobalCheckbox();
60 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
64 const checkboxTH
= tableHeaders
[0];
65 checkboxHeader
.injectInside(checkboxTH
);
68 // Register keyboard events to modal window
69 // https://github.com/qbittorrent/qBittorrent/pull/18687#discussion_r1135045726
72 keyboard
= new Keyboard({
73 defaultEventType
: "keydown",
75 "Escape": function(event
) {
76 window
.parent
.qBittorrent
.Client
.closeWindows();
77 event
.preventDefault();
79 "Esc": function(event
) {
80 window
.parent
.qBittorrent
.Client
.closeWindows();
81 event
.preventDefault();
88 const fileRenamer
= new window
.qBittorrent
.MultiRename
.RenameFiles();
89 fileRenamer
.hash
= data
.hash
;
91 // Load Multi Rename Preferences
92 const multiRenamePrefChecked
= LocalPreferences
.get("multirename_rememberPreferences", "true") === "true";
93 $("multirename_rememberprefs_checkbox").checked
= multiRenamePrefChecked
;
95 if (multiRenamePrefChecked
) {
96 const multirename_search
= LocalPreferences
.get("multirename_search", "");
97 fileRenamer
.setSearch(multirename_search
);
98 $("multiRenameSearch").value
= multirename_search
;
100 const multirename_useRegex
= LocalPreferences
.get("multirename_useRegex", false);
101 fileRenamer
.useRegex
= multirename_useRegex
=== "true";
102 $("use_regex_search").checked
= fileRenamer
.useRegex
;
104 const multirename_matchAllOccurrences
= LocalPreferences
.get("multirename_matchAllOccurrences", false);
105 fileRenamer
.matchAllOccurrences
= multirename_matchAllOccurrences
=== "true";
106 $("match_all_occurrences").checked
= fileRenamer
.matchAllOccurrences
;
108 const multirename_caseSensitive
= LocalPreferences
.get("multirename_caseSensitive", false);
109 fileRenamer
.caseSensitive
= multirename_caseSensitive
=== "true";
110 $("case_sensitive").checked
= fileRenamer
.caseSensitive
;
112 const multirename_replace
= LocalPreferences
.get("multirename_replace", "");
113 fileRenamer
.setReplacement(multirename_replace
);
114 $("multiRenameReplace").value
= multirename_replace
;
116 const multirename_appliesTo
= LocalPreferences
.get("multirename_appliesTo", window
.qBittorrent
.MultiRename
.AppliesTo
.FilenameExtension
);
117 fileRenamer
.appliesTo
= window
.qBittorrent
.MultiRename
.AppliesTo
[multirename_appliesTo
];
118 $("applies_to_option").value
= fileRenamer
.appliesTo
;
120 const multirename_includeFiles
= LocalPreferences
.get("multirename_includeFiles", true);
121 fileRenamer
.includeFiles
= multirename_includeFiles
=== "true";
122 $("include_files").checked
= fileRenamer
.includeFiles
;
124 const multirename_includeFolders
= LocalPreferences
.get("multirename_includeFolders", false);
125 fileRenamer
.includeFolders
= multirename_includeFolders
=== "true";
126 $("include_folders").checked
= fileRenamer
.includeFolders
;
128 const multirename_fileEnumerationStart
= LocalPreferences
.get("multirename_fileEnumerationStart", 0);
129 fileRenamer
.fileEnumerationStart
= parseInt(multirename_fileEnumerationStart
, 10);
130 $("file_counter").value
= fileRenamer
.fileEnumerationStart
;
132 const multirename_replaceAll
= LocalPreferences
.get("multirename_replaceAll", false);
133 fileRenamer
.replaceAll
= multirename_replaceAll
=== "true";
134 const renameButtonValue
= fileRenamer
.replaceAll
? "Replace All" : "Replace";
135 $("renameOptions").value
= renameButtonValue
;
136 $("renameButton").value
= renameButtonValue
;
139 // Fires every time a row's selection changes
140 bulkRenameFilesTable
.onRowSelectionChange = function(row
) {
141 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
142 fileRenamer
.update();
145 // Setup Search Events that control renaming
146 $("multiRenameSearch").addEventListener("input", (e
) => {
147 const sanitized
= e
.target
.value
.replace(/\n/g, "");
148 $("multiRenameSearch").value
= sanitized
;
150 // Search input has changed
151 $("multiRenameSearch").style
["border-color"] = "";
152 LocalPreferences
.set("multirename_search", sanitized
);
153 fileRenamer
.setSearch(sanitized
);
155 $("use_regex_search").addEventListener("change", (e
) => {
156 fileRenamer
.useRegex
= e
.target
.checked
;
157 LocalPreferences
.set("multirename_useRegex", e
.target
.checked
);
158 fileRenamer
.update();
160 $("match_all_occurrences").addEventListener("change", (e
) => {
161 fileRenamer
.matchAllOccurrences
= e
.target
.checked
;
162 LocalPreferences
.set("multirename_matchAllOccurrences", e
.target
.checked
);
163 fileRenamer
.update();
165 $("case_sensitive").addEventListener("change", (e
) => {
166 fileRenamer
.caseSensitive
= e
.target
.checked
;
167 LocalPreferences
.set("multirename_caseSensitive", e
.target
.checked
);
168 fileRenamer
.update();
172 * Fires every time the filerenamer gets changed, it will update all the rows in the table
174 fileRenamer
.onChanged = function(matchedRows
) {
175 // Clear renamed column
177 .querySelectorAll("span[id^='filesTablefileRenamed']")
179 span
.textContent
= "";
182 // Update renamed column for matched rows
183 for (let i
= 0; i
< matchedRows
.length
; ++i
) {
184 const row
= matchedRows
[i
];
185 $("filesTablefileRenamed" + row
.rowId
).textContent
= row
.renamed
;
188 fileRenamer
.onInvalidRegex = function(err
) {
189 $("multiRenameSearch").style
["border-color"] = "#CC0033";
192 // Setup Replace Events that control renaming
193 $("multiRenameReplace").addEventListener("input", (e
) => {
194 const sanitized
= e
.target
.value
.replace(/\n/g, "");
195 $("multiRenameReplace").value
= sanitized
;
197 // Replace input has changed
198 $("multiRenameReplace").style
["border-color"] = "";
199 LocalPreferences
.set("multirename_replace", sanitized
);
200 fileRenamer
.setReplacement(sanitized
);
202 $("applies_to_option").addEventListener("change", (e
) => {
203 fileRenamer
.appliesTo
= e
.target
.value
;
204 LocalPreferences
.set("multirename_appliesTo", e
.target
.value
);
205 fileRenamer
.update();
207 $("include_files").addEventListener("change", (e
) => {
208 fileRenamer
.includeFiles
= e
.target
.checked
;
209 LocalPreferences
.set("multirename_includeFiles", e
.target
.checked
);
210 fileRenamer
.update();
212 $("include_folders").addEventListener("change", (e
) => {
213 fileRenamer
.includeFolders
= e
.target
.checked
;
214 LocalPreferences
.set("multirename_includeFolders", e
.target
.checked
);
215 fileRenamer
.update();
217 $("file_counter").addEventListener("input", (e
) => {
218 let value
= e
.target
.valueAsNumber
;
223 if (value
> 99999999)
225 fileRenamer
.fileEnumerationStart
= value
;
226 $("file_counter").value
= value
;
227 LocalPreferences
.set("multirename_fileEnumerationStart", value
);
228 fileRenamer
.update();
231 // Setup Rename Operation Events
232 $("renameButton").addEventListener("click", (e
) => {
233 // Disable Search Options
234 $("multiRenameSearch").disabled
= true;
235 $("use_regex_search").disabled
= true;
236 $("match_all_occurrences").disabled
= true;
237 $("case_sensitive").disabled
= true;
238 // Disable Replace Options
239 $("multiRenameReplace").disabled
= true;
240 $("applies_to_option").disabled
= true;
241 $("include_files").disabled
= true;
242 $("include_folders").disabled
= true;
243 $("file_counter").disabled
= true;
244 // Disable Rename Buttons
245 $("renameButton").disabled
= true;
246 $("renameOptions").disabled
= true;
248 $("rename_error").textContent
= "";
249 fileRenamer
.rename();
251 fileRenamer
.onRenamed = function(rows
) {
252 // Disable Search Options
253 $("multiRenameSearch").disabled
= false;
254 $("use_regex_search").disabled
= false;
255 $("match_all_occurrences").disabled
= false;
256 $("case_sensitive").disabled
= false;
257 // Disable Replace Options
258 $("multiRenameReplace").disabled
= false;
259 $("applies_to_option").disabled
= false;
260 $("include_files").disabled
= false;
261 $("include_folders").disabled
= false;
262 $("file_counter").disabled
= false;
263 // Disable Rename Buttons
264 $("renameButton").disabled
= false;
265 $("renameOptions").disabled
= false;
268 let selectedRows
= bulkRenameFilesTable
.getSelectedRows().map(row
=> row
.rowId
.toString());
269 for (const renamedRow
of rows
)
270 selectedRows
= selectedRows
.filter(selectedRow
=> selectedRow
!== renamedRow
.rowId
.toString());
271 bulkRenameFilesTable
.clear();
273 // Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts
274 if (!fileRenamer
.replaceAll
) {
275 fileRenamer
.fileEnumerationStart
++;
276 $("file_counter").value
= fileRenamer
.fileEnumerationStart
;
278 setupTable(selectedRows
);
280 fileRenamer
.onRenameError = function(err
, row
) {
281 if (err
.xhr
.status
=== 409)
282 $("rename_error").textContent
= `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}
\``;
284 $("renameOptions").addEventListener("change", (e
) => {
285 const combobox
= e
.target
;
286 const replaceOperation
= combobox
.value
;
287 if (replaceOperation
=== "Replace")
288 fileRenamer
.replaceAll
= false;
289 else if (replaceOperation
=== "Replace All")
290 fileRenamer
.replaceAll
= true;
292 fileRenamer
.replaceAll
= false;
293 LocalPreferences
.set("multirename_replaceAll", fileRenamer
.replaceAll
);
294 $("renameButton").value
= replaceOperation
;
296 $("closeButton").addEventListener("click", () => {
297 window
.parent
.qBittorrent
.Client
.closeWindows();
298 event
.preventDefault();
300 // synchronize header scrolling to table body
301 $("bulkRenameFilesTableDiv").onscroll = function() {
302 const length
= $(this).scrollLeft
;
303 $("bulkRenameFilesTableFixedHeaderDiv").scrollLeft
= length
;
306 const handleTorrentFiles = function(files
, selectedRows
) {
307 const rows
= files
.map((file
, index
) => {
311 checked
: 1, // unchecked
313 original
: window
.qBittorrent
.Filesystem
.fileName(file
.name
),
321 addRowsToTable(rows
, selectedRows
);
324 const addRowsToTable = function(rows
, selectedRows
) {
326 const rootNode
= new window
.qBittorrent
.FileTree
.FolderNode();
327 rootNode
.autoCheckFolders
= false;
329 rows
.forEach((row
) => {
330 const pathItems
= row
.path
.split(window
.qBittorrent
.Filesystem
.PathSeparator
);
332 pathItems
.pop(); // remove last item (i.e. file name)
333 let parent
= rootNode
;
334 pathItems
.forEach((folderName
) => {
335 if (folderName
=== ".unwanted")
338 let folderNode
= null;
339 if (parent
.children
!== null) {
340 for (let i
= 0; i
< parent
.children
.length
; ++i
) {
341 const childFolder
= parent
.children
[i
];
342 if (childFolder
.original
=== folderName
) {
343 folderNode
= childFolder
;
349 if (folderNode
=== null) {
350 folderNode
= new window
.qBittorrent
.FileTree
.FolderNode();
351 folderNode
.autoCheckFolders
= false;
352 folderNode
.rowId
= rowId
;
353 folderNode
.path
= (parent
.path
=== "")
355 : [parent
.path
, folderName
].join(window
.qBittorrent
.Filesystem
.PathSeparator
);
356 folderNode
.checked
= selectedRows
.includes(rowId
.toString()) ? 0 : 1;
357 folderNode
.original
= folderName
;
358 folderNode
.renamed
= "";
359 folderNode
.root
= parent
;
360 parent
.addChild(folderNode
);
368 const childNode
= new window
.qBittorrent
.FileTree
.FileNode();
369 childNode
.rowId
= rowId
;
370 childNode
.path
= row
.path
;
371 childNode
.checked
= selectedRows
.includes(rowId
.toString()) ? 0 : 1;
372 childNode
.original
= row
.original
;
373 childNode
.renamed
= "";
374 childNode
.root
= parent
;
375 childNode
.data
= row
;
376 parent
.addChild(childNode
);
381 bulkRenameFilesTable
.populateTable(rootNode
);
382 bulkRenameFilesTable
.updateTable(false);
384 if (selectedRows
!== undefined)
385 bulkRenameFilesTable
.reselectRows(selectedRows
);
387 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
388 fileRenamer
.update();
391 const setupTable = function(selectedRows
) {
393 url
: new URI("api/v2/torrents/files?hash=" + data
.hash
),
396 onSuccess: function(files
) {
397 if (files
.length
=== 0)
398 bulkRenameFilesTable
.clear();
400 handleTorrentFiles(files
, selectedRows
);
404 setupTable(data
.selectedRows
);
408 <body style=
"min-width: 400px; min-height: 300px;">
409 <div style=
"padding: 0px 10px 0px 0px;">
410 <div style=
"float: left; height: 100%; width: 228px;">
411 <div class=
"formRow">
412 <input type=
"checkbox" id=
"multirename_rememberprefs_checkbox" onchange=
"LocalPreferences.set('multirename_rememberPreferences', this.checked);">
413 <label for=
"multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]
</label>
416 <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>
417 <div class=
"formRow">
418 <input type=
"checkbox" id=
"use_regex_search">
419 <label for=
"use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]
</label>
421 <div class=
"formRow">
422 <input type=
"checkbox" id=
"match_all_occurrences">
423 <label for=
"match_all_occurrences">QBT_TR(Match all occurrences)QBT_TR[CONTEXT=PropertiesWidget]
</label>
425 <div class=
"formRow">
426 <input type=
"checkbox" id=
"case_sensitive">
427 <label for=
"case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]
</label>
430 <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>
431 <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;">
432 <option selected
value=
"FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]
</option>
433 <option value=
"Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]
</option>
434 <option value=
"Extension">QBT_TR(Extension)QBT_TR[CONTEXT=PropertiesWidget]
</option>
436 <div class=
"formRow">
437 <input type=
"checkbox" id=
"include_files" checked
>
438 <label for=
"include_files">QBT_TR(Include files)QBT_TR[CONTEXT=PropertiesWidget]
</label>
440 <div class=
"formRow">
441 <input type=
"checkbox" id=
"include_folders">
442 <label for=
"include_folders">QBT_TR(Include folders)QBT_TR[CONTEXT=PropertiesWidget]
</label>
444 <div class=
"formRow">
445 <input type=
"number" min=
"0" max=
"99999999" value=
"0" id=
"file_counter" style=
"width: 80px;">
446 <label for=
"file_counter">QBT_TR(Enumerate Files)QBT_TR[CONTEXT=PropertiesWidget]
</label>
449 <div id=
"operation_btns" style=
"position: absolute; left: 0; bottom: 0; margin: 0px 12px 36px 12px; width: 228px;background: #ffffff;padding: 0px 5px 10px 0px;">
450 <div style=
"overflow: auto;">
451 <span id=
"rename_error" style=
"float: unset; font-size: unset;"></span>
454 <div style=
"width: 60%; float: left;">
455 <input id=
"renameButton" type=
"button" value=
"Replace" style=
"float: left; width: 86px;">
456 <select id=
"renameOptions" name=
"renameOptions" aria-label=
"QBT_TR(Replace option)QBT_TR[CONTEXT=PropertiesWidget]" style=
"width: 22px;">
457 <option selected
value=
"Replace">QBT_TR(Replace)QBT_TR[CONTEXT=PropertiesWidget]
</option>
458 <option value=
"Replace All">QBT_TR(Replace All)QBT_TR[CONTEXT=PropertiesWidget]
</option>
461 <input id=
"closeButton" type=
"button" value=
"Close" style=
"float: right; width: 30%;">
463 <div id=
"torrentFiles" class=
"panel" style=
"position: absolute; top: 0; right: 0; bottom: 0; left: 228px; margin: 35px 10px 45px 20px; border-bottom: 0">
464 <div id=
"bulkRenameFilesTableFixedHeaderDiv" class=
"dynamicTableFixedHeaderDiv">
465 <table class=
"dynamicTable">
467 <tr class=
"dynamicTableHeader"></tr>
471 <div id=
"bulkRenameFilesTableDiv" class=
"dynamicTableDiv">
472 <table class=
"dynamicTable">
474 <tr class=
"dynamicTableHeader"></tr>