WebUI: migrate to fetch API
[qBittorrent.git] / src / webui / www / private / rename_files.html
blobd16f75e9bd475cff3b17229225f9afd76753844b
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 (() => {
18 const {
19 options: { data },
20 windowEl
21 } = window.MUI.Windows.instances["multiRenamePage"];
23 const bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
24 targets: "#bulkRenameFilesTableDiv tr",
25 menu: "multiRenameFilesMenu",
26 actions: {
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());
36 offsets: {
37 x: 0,
38 y: 2
40 });
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();
55 fileRenamer.update();
56 });
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
64 new Keyboard({
65 defaultEventType: "keydown",
66 events: {
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();
76 }).activate();
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
166 document
167 .querySelectorAll("span[id^='filesTablefileRenamed']")
168 .forEach((span) => {
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;
209 if (!value)
210 value = 0;
211 if (value < 0)
212 value = 0;
213 if (value > 99999999)
214 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;
237 // Clear error text
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;
257 // Recreate table
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;
281 else
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) => {
299 const row = {
300 fileId: index,
301 checked: 1, // unchecked
302 path: file.name,
303 original: window.qBittorrent.Filesystem.fileName(file.name),
304 renamed: "",
305 size: file.size
308 return row;
311 addRowsToTable(rows, selectedRows);
314 const addRowsToTable = (rows, selectedRows) => {
315 let rowId = 0;
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")
326 return;
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;
334 break;
339 if (folderNode === null) {
340 folderNode = new window.qBittorrent.FileTree.FolderNode();
341 folderNode.autoCheckFolders = false;
342 folderNode.rowId = rowId;
343 folderNode.path = (parent.path === "")
344 ? folderName
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);
352 ++rowId;
355 parent = 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);
368 ++rowId;
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), {
383 method: "GET",
384 cache: "no-store"
386 .then(async (response) => {
387 if (!response.ok)
388 return;
390 const files = await response.json();
391 if (files.length === 0)
392 bulkRenameFilesTable.clear();
393 else
394 handleTorrentFiles(files, selectedRows);
397 setupTable(data.selectedRows);
398 })();
399 </script>
400 </head>
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>
408 </div>
409 <hr>
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>
414 </div>
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>
418 </div>
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>
422 </div>
423 <hr>
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>
429 </select>
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>
433 </div>
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>
437 </div>
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>
441 </div>
442 </div>
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>
446 </div>
447 <hr>
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>
453 </select>
454 </div>
455 <input id="closeButton" type="button" value="Close" style="float: right; width: 30%;">
456 </div>
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">
460 <thead>
461 <tr class="dynamicTableHeader"></tr>
462 </thead>
463 </table>
464 </div>
465 <div id="bulkRenameFilesTableDiv" class="dynamicTableDiv">
466 <table class="dynamicTable">
467 <thead>
468 <tr class="dynamicTableHeader"></tr>
469 </thead>
470 <tbody></tbody>
471 </table>
472 </div>
473 </div>
474 </div>
475 </body>
477 </html>