Display External IP Address in status bar
[qBittorrent.git] / src / webui / www / private / rename_files.html
blob0e3de25b16e3ae78ec518789899133951adb7d83
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 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",
26 actions: {
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());
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 = 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();
55 fileRenamer.update();
56 });
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",
66 events: {
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();
76 });
77 keyboard.activate();
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
167 document
168 .querySelectorAll("span[id^='filesTablefileRenamed']")
169 .forEach((span) => {
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;
210 if (!value)
211 value = 0;
212 if (value < 0)
213 value = 0;
214 if (value > 99999999)
215 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;
238 // Clear error text
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;
258 // Recreate table
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;
282 else
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) => {
300 const row = {
301 fileId: index,
302 checked: 1, // unchecked
303 path: file.name,
304 original: window.qBittorrent.Filesystem.fileName(file.name),
305 renamed: "",
306 size: file.size
309 return row;
312 addRowsToTable(rows, selectedRows);
315 const addRowsToTable = (rows, selectedRows) => {
316 let rowId = 0;
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")
327 return;
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;
335 break;
340 if (folderNode === null) {
341 folderNode = new window.qBittorrent.FileTree.FolderNode();
342 folderNode.autoCheckFolders = false;
343 folderNode.rowId = rowId;
344 folderNode.path = (parent.path === "")
345 ? folderName
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);
353 ++rowId;
356 parent = 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);
369 ++rowId;
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) => {
383 new Request.JSON({
384 url: new URI("api/v2/torrents/files?hash=" + data.hash),
385 noCache: true,
386 method: "get",
387 onSuccess: (files) => {
388 if (files.length === 0)
389 bulkRenameFilesTable.clear();
390 else
391 handleTorrentFiles(files, selectedRows);
393 }).send();
395 setupTable(data.selectedRows);
396 })();
397 </script>
398 </head>
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>
406 </div>
407 <hr>
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>
412 </div>
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>
416 </div>
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>
420 </div>
421 <hr>
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>
427 </select>
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>
431 </div>
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>
435 </div>
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>
439 </div>
440 </div>
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>
444 </div>
445 <hr>
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>
451 </select>
452 </div>
453 <input id="closeButton" type="button" value="Close" style="float: right; width: 30%;">
454 </div>
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">
458 <thead>
459 <tr class="dynamicTableHeader"></tr>
460 </thead>
461 </table>
462 </div>
463 <div id="bulkRenameFilesTableDiv" class="dynamicTableDiv">
464 <table class="dynamicTable">
465 <thead>
466 <tr class="dynamicTableHeader"></tr>
467 </thead>
468 <tbody></tbody>
469 </table>
470 </div>
471 </div>
472 </div>
473 </body>
475 </html>