WebUI: Use Map instead of Mootools Hash in Torrents table
[qBittorrent.git] / src / webui / www / private / rename_files.html
blob290490b2f533fe9a4f1a6627ea381a234371ac30
1 <!DOCTYPE html>
2 <html lang="${LANG}">
4 <head>
5 <meta charset="UTF-8">
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>
14 <script>
15 "use strict";
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",
28 actions: {
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());
38 offsets: {
39 x: -15,
40 y: 2
42 });
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");
51 let checkboxHeader;
52 if (tableHeaders.length > 0) {
53 if (checkboxHeader)
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();
61 fileRenamer.update();
62 });
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
70 let keyboard;
71 if (!keyboard) {
72 keyboard = new Keyboard({
73 defaultEventType: "keydown",
74 events: {
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();
84 });
85 keyboard.activate();
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
176 document
177 .querySelectorAll("span[id^='filesTablefileRenamed']")
178 .forEach((span) => {
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;
219 if (!value)
220 value = 0;
221 if (value < 0)
222 value = 0;
223 if (value > 99999999)
224 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;
247 // Clear error text
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;
267 // Recreate table
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;
291 else
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) => {
309 const row = {
310 fileId: index,
311 checked: 1, // unchecked
312 path: file.name,
313 original: window.qBittorrent.Filesystem.fileName(file.name),
314 renamed: "",
315 size: file.size
318 return row;
321 addRowsToTable(rows, selectedRows);
324 const addRowsToTable = function(rows, selectedRows) {
325 let rowId = 0;
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")
336 return;
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;
344 break;
349 if (folderNode === null) {
350 folderNode = new window.qBittorrent.FileTree.FolderNode();
351 folderNode.autoCheckFolders = false;
352 folderNode.rowId = rowId;
353 folderNode.path = (parent.path === "")
354 ? folderName
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);
362 ++rowId;
365 parent = 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);
378 ++rowId;
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) {
392 new Request.JSON({
393 url: new URI("api/v2/torrents/files?hash=" + data.hash),
394 noCache: true,
395 method: "get",
396 onSuccess: function(files) {
397 if (files.length === 0)
398 bulkRenameFilesTable.clear();
399 else
400 handleTorrentFiles(files, selectedRows);
402 }).send();
404 setupTable(data.selectedRows);
405 </script>
406 </head>
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>
414 </div>
415 <hr>
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>
420 </div>
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>
424 </div>
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>
428 </div>
429 <hr>
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>
435 </select>
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>
439 </div>
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>
443 </div>
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>
447 </div>
448 </div>
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>
452 </div>
453 <hr>
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>
459 </select>
460 </div>
461 <input id="closeButton" type="button" value="Close" style="float: right; width: 30%;">
462 </div>
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">
466 <thead>
467 <tr class="dynamicTableHeader"></tr>
468 </thead>
469 </table>
470 </div>
471 <div id="bulkRenameFilesTableDiv" class="dynamicTableDiv">
472 <table class="dynamicTable">
473 <thead>
474 <tr class="dynamicTableHeader"></tr>
475 </thead>
476 <tbody></tbody>
477 </table>
478 </div>
479 </div>
480 </div>
481 </body>
483 </html>