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>
18 if (window
.parent
.qBittorrent
!== undefined)
19 window
.qBittorrent
= window
.parent
.qBittorrent
;
20 window
.qBittorrent
= window
.parent
.qBittorrent
;
22 const data
= window
.MUI
.Windows
.instances
["multiRenamePage"].options
.data
;
23 const bulkRenameFilesContextMenu
= new window
.qBittorrent
.ContextMenu
.ContextMenu({
24 targets
: "#bulkRenameFilesTableDiv tr",
25 menu
: "multiRenameFilesMenu",
27 ToggleSelection
: (element
, ref
) => {
28 const rowId
= parseInt(element
.getAttribute("data-row-id"), 10);
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
= new Element("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 checkboxHeader
.injectInside(checkboxTH
);
62 // Register keyboard events to modal window
63 // https://github.com/qbittorrent/qBittorrent/pull/18687#discussion_r1135045726
64 const keyboard
= new Keyboard({
65 defaultEventType
: "keydown",
67 "Escape": function(event
) {
68 window
.parent
.qBittorrent
.Client
.closeWindows();
69 event
.preventDefault();
71 "Esc": function(event
) {
72 window
.parent
.qBittorrent
.Client
.closeWindows();
73 event
.preventDefault();
79 const fileRenamer
= new window
.qBittorrent
.MultiRename
.RenameFiles();
80 fileRenamer
.hash
= data
.hash
;
82 // Load Multi Rename Preferences
83 const multiRenamePrefChecked
= LocalPreferences
.get("multirename_rememberPreferences", "true") === "true";
84 $("multirename_rememberprefs_checkbox").checked
= multiRenamePrefChecked
;
86 if (multiRenamePrefChecked
) {
87 const multirename_search
= LocalPreferences
.get("multirename_search", "");
88 fileRenamer
.setSearch(multirename_search
);
89 $("multiRenameSearch").value
= multirename_search
;
91 const multirename_useRegex
= LocalPreferences
.get("multirename_useRegex", false);
92 fileRenamer
.useRegex
= multirename_useRegex
=== "true";
93 $("use_regex_search").checked
= fileRenamer
.useRegex
;
95 const multirename_matchAllOccurrences
= LocalPreferences
.get("multirename_matchAllOccurrences", false);
96 fileRenamer
.matchAllOccurrences
= multirename_matchAllOccurrences
=== "true";
97 $("match_all_occurrences").checked
= fileRenamer
.matchAllOccurrences
;
99 const multirename_caseSensitive
= LocalPreferences
.get("multirename_caseSensitive", false);
100 fileRenamer
.caseSensitive
= multirename_caseSensitive
=== "true";
101 $("case_sensitive").checked
= fileRenamer
.caseSensitive
;
103 const multirename_replace
= LocalPreferences
.get("multirename_replace", "");
104 fileRenamer
.setReplacement(multirename_replace
);
105 $("multiRenameReplace").value
= multirename_replace
;
107 const multirename_appliesTo
= LocalPreferences
.get("multirename_appliesTo", window
.qBittorrent
.MultiRename
.AppliesTo
.FilenameExtension
);
108 fileRenamer
.appliesTo
= window
.qBittorrent
.MultiRename
.AppliesTo
[multirename_appliesTo
];
109 $("applies_to_option").value
= fileRenamer
.appliesTo
;
111 const multirename_includeFiles
= LocalPreferences
.get("multirename_includeFiles", true);
112 fileRenamer
.includeFiles
= multirename_includeFiles
=== "true";
113 $("include_files").checked
= fileRenamer
.includeFiles
;
115 const multirename_includeFolders
= LocalPreferences
.get("multirename_includeFolders", false);
116 fileRenamer
.includeFolders
= multirename_includeFolders
=== "true";
117 $("include_folders").checked
= fileRenamer
.includeFolders
;
119 const multirename_fileEnumerationStart
= LocalPreferences
.get("multirename_fileEnumerationStart", 0);
120 fileRenamer
.fileEnumerationStart
= parseInt(multirename_fileEnumerationStart
, 10);
121 $("file_counter").value
= fileRenamer
.fileEnumerationStart
;
123 const multirename_replaceAll
= LocalPreferences
.get("multirename_replaceAll", false);
124 fileRenamer
.replaceAll
= multirename_replaceAll
=== "true";
125 const renameButtonValue
= fileRenamer
.replaceAll
? "Replace All" : "Replace";
126 $("renameOptions").value
= renameButtonValue
;
127 $("renameButton").value
= renameButtonValue
;
130 // Fires every time a row's selection changes
131 bulkRenameFilesTable
.onRowSelectionChange
= (row
) => {
132 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
133 fileRenamer
.update();
136 // Setup Search Events that control renaming
137 $("multiRenameSearch").addEventListener("input", (e
) => {
138 const sanitized
= e
.target
.value
.replace(/\n/g, "");
139 $("multiRenameSearch").value
= sanitized
;
141 // Search input has changed
142 $("multiRenameSearch").style
["border-color"] = "";
143 LocalPreferences
.set("multirename_search", sanitized
);
144 fileRenamer
.setSearch(sanitized
);
146 $("use_regex_search").addEventListener("change", (e
) => {
147 fileRenamer
.useRegex
= e
.target
.checked
;
148 LocalPreferences
.set("multirename_useRegex", e
.target
.checked
);
149 fileRenamer
.update();
151 $("match_all_occurrences").addEventListener("change", (e
) => {
152 fileRenamer
.matchAllOccurrences
= e
.target
.checked
;
153 LocalPreferences
.set("multirename_matchAllOccurrences", e
.target
.checked
);
154 fileRenamer
.update();
156 $("case_sensitive").addEventListener("change", (e
) => {
157 fileRenamer
.caseSensitive
= e
.target
.checked
;
158 LocalPreferences
.set("multirename_caseSensitive", e
.target
.checked
);
159 fileRenamer
.update();
163 * Fires every time the filerenamer gets changed, it will update all the rows in the table
165 fileRenamer
.onChanged
= (matchedRows
) => {
166 // Clear renamed column
168 .querySelectorAll("span[id^='filesTablefileRenamed']")
170 span
.textContent
= "";
173 // Update renamed column for matched rows
174 for (let i
= 0; i
< matchedRows
.length
; ++i
) {
175 const row
= matchedRows
[i
];
176 $("filesTablefileRenamed" + row
.rowId
).textContent
= row
.renamed
;
179 fileRenamer
.onInvalidRegex
= (err
) => {
180 $("multiRenameSearch").style
["border-color"] = "#CC0033";
183 // Setup Replace Events that control renaming
184 $("multiRenameReplace").addEventListener("input", (e
) => {
185 const sanitized
= e
.target
.value
.replace(/\n/g, "");
186 $("multiRenameReplace").value
= sanitized
;
188 // Replace input has changed
189 $("multiRenameReplace").style
["border-color"] = "";
190 LocalPreferences
.set("multirename_replace", sanitized
);
191 fileRenamer
.setReplacement(sanitized
);
193 $("applies_to_option").addEventListener("change", (e
) => {
194 fileRenamer
.appliesTo
= e
.target
.value
;
195 LocalPreferences
.set("multirename_appliesTo", e
.target
.value
);
196 fileRenamer
.update();
198 $("include_files").addEventListener("change", (e
) => {
199 fileRenamer
.includeFiles
= e
.target
.checked
;
200 LocalPreferences
.set("multirename_includeFiles", e
.target
.checked
);
201 fileRenamer
.update();
203 $("include_folders").addEventListener("change", (e
) => {
204 fileRenamer
.includeFolders
= e
.target
.checked
;
205 LocalPreferences
.set("multirename_includeFolders", e
.target
.checked
);
206 fileRenamer
.update();
208 $("file_counter").addEventListener("input", (e
) => {
209 let value
= e
.target
.valueAsNumber
;
214 if (value
> 99999999)
216 fileRenamer
.fileEnumerationStart
= value
;
217 $("file_counter").value
= value
;
218 LocalPreferences
.set("multirename_fileEnumerationStart", value
);
219 fileRenamer
.update();
222 // Setup Rename Operation Events
223 $("renameButton").addEventListener("click", (e
) => {
224 // Disable Search Options
225 $("multiRenameSearch").disabled
= true;
226 $("use_regex_search").disabled
= true;
227 $("match_all_occurrences").disabled
= true;
228 $("case_sensitive").disabled
= true;
229 // Disable Replace Options
230 $("multiRenameReplace").disabled
= true;
231 $("applies_to_option").disabled
= true;
232 $("include_files").disabled
= true;
233 $("include_folders").disabled
= true;
234 $("file_counter").disabled
= true;
235 // Disable Rename Buttons
236 $("renameButton").disabled
= true;
237 $("renameOptions").disabled
= true;
239 $("rename_error").textContent
= "";
240 fileRenamer
.rename();
242 fileRenamer
.onRenamed
= (rows
) => {
243 // Disable Search Options
244 $("multiRenameSearch").disabled
= false;
245 $("use_regex_search").disabled
= false;
246 $("match_all_occurrences").disabled
= false;
247 $("case_sensitive").disabled
= false;
248 // Disable Replace Options
249 $("multiRenameReplace").disabled
= false;
250 $("applies_to_option").disabled
= false;
251 $("include_files").disabled
= false;
252 $("include_folders").disabled
= false;
253 $("file_counter").disabled
= false;
254 // Disable Rename Buttons
255 $("renameButton").disabled
= false;
256 $("renameOptions").disabled
= false;
259 let selectedRows
= bulkRenameFilesTable
.getSelectedRows().map(row
=> row
.rowId
.toString());
260 for (const renamedRow
of rows
)
261 selectedRows
= selectedRows
.filter(selectedRow
=> selectedRow
!== renamedRow
.rowId
.toString());
262 bulkRenameFilesTable
.clear();
264 // Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts
265 if (!fileRenamer
.replaceAll
) {
266 fileRenamer
.fileEnumerationStart
++;
267 $("file_counter").value
= fileRenamer
.fileEnumerationStart
;
269 setupTable(selectedRows
);
271 fileRenamer
.onRenameError
= (err
, row
) => {
272 if (err
.xhr
.status
=== 409)
273 $("rename_error").textContent
= `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}
\``;
275 $("renameOptions").addEventListener("change", (e
) => {
276 const combobox
= e
.target
;
277 const replaceOperation
= combobox
.value
;
278 if (replaceOperation
=== "Replace")
279 fileRenamer
.replaceAll
= false;
280 else if (replaceOperation
=== "Replace All")
281 fileRenamer
.replaceAll
= true;
283 fileRenamer
.replaceAll
= false;
284 LocalPreferences
.set("multirename_replaceAll", fileRenamer
.replaceAll
);
285 $("renameButton").value
= replaceOperation
;
287 $("closeButton").addEventListener("click", () => {
288 window
.parent
.qBittorrent
.Client
.closeWindows();
289 event
.preventDefault();
291 // synchronize header scrolling to table body
292 $("bulkRenameFilesTableDiv").onscroll = function() {
293 const length
= $(this).scrollLeft
;
294 $("bulkRenameFilesTableFixedHeaderDiv").scrollLeft
= length
;
297 const handleTorrentFiles
= (files
, selectedRows
) => {
298 const rows
= files
.map((file
, index
) => {
302 checked
: 1, // unchecked
304 original
: window
.qBittorrent
.Filesystem
.fileName(file
.name
),
312 addRowsToTable(rows
, selectedRows
);
315 const addRowsToTable
= (rows
, selectedRows
) => {
317 const rootNode
= new window
.qBittorrent
.FileTree
.FolderNode();
318 rootNode
.autoCheckFolders
= false;
320 rows
.forEach((row
) => {
321 const pathItems
= row
.path
.split(window
.qBittorrent
.Filesystem
.PathSeparator
);
323 pathItems
.pop(); // remove last item (i.e. file name)
324 let parent
= rootNode
;
325 pathItems
.forEach((folderName
) => {
326 if (folderName
=== ".unwanted")
329 let folderNode
= null;
330 if (parent
.children
!== null) {
331 for (let i
= 0; i
< parent
.children
.length
; ++i
) {
332 const childFolder
= parent
.children
[i
];
333 if (childFolder
.original
=== folderName
) {
334 folderNode
= childFolder
;
340 if (folderNode
=== null) {
341 folderNode
= new window
.qBittorrent
.FileTree
.FolderNode();
342 folderNode
.autoCheckFolders
= false;
343 folderNode
.rowId
= rowId
;
344 folderNode
.path
= (parent
.path
=== "")
346 : [parent
.path
, folderName
].join(window
.qBittorrent
.Filesystem
.PathSeparator
);
347 folderNode
.checked
= selectedRows
.includes(rowId
.toString()) ? 0 : 1;
348 folderNode
.original
= folderName
;
349 folderNode
.renamed
= "";
350 folderNode
.root
= parent
;
351 parent
.addChild(folderNode
);
359 const childNode
= new window
.qBittorrent
.FileTree
.FileNode();
360 childNode
.rowId
= rowId
;
361 childNode
.path
= row
.path
;
362 childNode
.checked
= selectedRows
.includes(rowId
.toString()) ? 0 : 1;
363 childNode
.original
= row
.original
;
364 childNode
.renamed
= "";
365 childNode
.root
= parent
;
366 childNode
.data
= row
;
367 parent
.addChild(childNode
);
372 bulkRenameFilesTable
.populateTable(rootNode
);
373 bulkRenameFilesTable
.updateTable(false);
375 if (selectedRows
!== undefined)
376 bulkRenameFilesTable
.reselectRows(selectedRows
);
378 fileRenamer
.selectedFiles
= bulkRenameFilesTable
.getSelectedRows();
379 fileRenamer
.update();
382 const setupTable
= (selectedRows
) => {
384 url
: new URI("api/v2/torrents/files?hash=" + data
.hash
),
387 onSuccess
: (files
) => {
388 if (files
.length
=== 0)
389 bulkRenameFilesTable
.clear();
391 handleTorrentFiles(files
, selectedRows
);
395 setupTable(data
.selectedRows
);
400 <body style=
"min-width: 400px; min-height: 300px;">
401 <div style=
"padding: 0px 10px 0px 0px;">
402 <div style=
"float: left; height: 100%; width: 228px;">
403 <div class=
"formRow">
404 <input type=
"checkbox" id=
"multirename_rememberprefs_checkbox" onchange=
"LocalPreferences.set('multirename_rememberPreferences', this.checked);">
405 <label for=
"multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]
</label>
408 <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>
409 <div class=
"formRow">
410 <input type=
"checkbox" id=
"use_regex_search">
411 <label for=
"use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]
</label>
413 <div class=
"formRow">
414 <input type=
"checkbox" id=
"match_all_occurrences">
415 <label for=
"match_all_occurrences">QBT_TR(Match all occurrences)QBT_TR[CONTEXT=PropertiesWidget]
</label>
417 <div class=
"formRow">
418 <input type=
"checkbox" id=
"case_sensitive">
419 <label for=
"case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]
</label>
422 <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>
423 <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;">
424 <option selected
value=
"FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]
</option>
425 <option value=
"Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]
</option>
426 <option value=
"Extension">QBT_TR(Extension)QBT_TR[CONTEXT=PropertiesWidget]
</option>
428 <div class=
"formRow">
429 <input type=
"checkbox" id=
"include_files" checked
>
430 <label for=
"include_files">QBT_TR(Include files)QBT_TR[CONTEXT=PropertiesWidget]
</label>
432 <div class=
"formRow">
433 <input type=
"checkbox" id=
"include_folders">
434 <label for=
"include_folders">QBT_TR(Include folders)QBT_TR[CONTEXT=PropertiesWidget]
</label>
436 <div class=
"formRow">
437 <input type=
"number" min=
"0" max=
"99999999" value=
"0" id=
"file_counter" style=
"width: 88px;">
438 <label for=
"file_counter">QBT_TR(Enumerate Files)QBT_TR[CONTEXT=PropertiesWidget]
</label>
441 <div id=
"operation_btns" style=
"position: absolute; left: 0; bottom: 0; margin: 0 12px 50px 12px; width: 228px;">
442 <div style=
"overflow: auto;">
443 <span id=
"rename_error" style=
"float: unset; font-size: unset;"></span>
446 <div style=
"width: 60%; float: left;">
447 <input id=
"renameButton" type=
"button" value=
"Replace" style=
"float: left; width: 86px;">
448 <select id=
"renameOptions" name=
"renameOptions" aria-label=
"QBT_TR(Replace option)QBT_TR[CONTEXT=PropertiesWidget]" style=
"width: 22px;">
449 <option selected
value=
"Replace">QBT_TR(Replace)QBT_TR[CONTEXT=PropertiesWidget]
</option>
450 <option value=
"Replace All">QBT_TR(Replace All)QBT_TR[CONTEXT=PropertiesWidget]
</option>
453 <input id=
"closeButton" type=
"button" value=
"Close" style=
"float: right; width: 30%;">
455 <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;">
456 <div id=
"bulkRenameFilesTableFixedHeaderDiv" class=
"dynamicTableFixedHeaderDiv">
457 <table class=
"dynamicTable">
459 <tr class=
"dynamicTableHeader"></tr>
463 <div id=
"bulkRenameFilesTableDiv" class=
"dynamicTableDiv">
464 <table class=
"dynamicTable">
466 <tr class=
"dynamicTableHeader"></tr>