mirror of
https://github.com/d47081/qBittorrent.git
synced 2025-01-11 07:18:08 +00:00
parent
d75fd3fcde
commit
466314675c
@ -57,7 +57,8 @@ h2 {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
#error_div {
|
||||
#error_div,
|
||||
#rename_error {
|
||||
color: #f00;
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
|
@ -30,6 +30,7 @@
|
||||
<script src="scripts/piecesbar.js?v=${CACHEID}"></script>
|
||||
<script src="scripts/file-tree.js?v=${CACHEID}"></script>
|
||||
<script src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script src="scripts/rename-files.js?v=${CACHEID}"></script>
|
||||
<script src="scripts/client.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script src="scripts/contextmenu.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
</head>
|
||||
@ -136,8 +137,13 @@
|
||||
<li class="separator"><a href="#delete"><img src="images/list-remove.svg" alt="QBT_TR(Remove)QBT_TR[CONTEXT=TransferListWidget]" /> QBT_TR(Remove)QBT_TR[CONTEXT=TransferListWidget]</a></li>
|
||||
<li class="separator">
|
||||
<a href="#setLocation"><img src="images/set-location.svg" alt="QBT_TR(Set location...)QBT_TR[CONTEXT=TransferListWidget]" /> QBT_TR(Set location...)QBT_TR[CONTEXT=TransferListWidget]</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#rename"><img src="images/edit-rename.svg" alt="QBT_TR(Rename...)QBT_TR[CONTEXT=TransferListWidget]" /> QBT_TR(Rename...)QBT_TR[CONTEXT=TransferListWidget]</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#renameFiles"><img src="images/edit-rename.svg" alt="QBT_TR(Rename Files...)QBT_TR[CONTEXT=TransferListWidget]" /> QBT_TR(Rename Files...)QBT_TR[CONTEXT=TransferListWidget]</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#Category" class="arrow-right"><img src="images/view-categories.svg" alt="QBT_TR(Category)QBT_TR[CONTEXT=TransferListWidget]" /> QBT_TR(Category)QBT_TR[CONTEXT=TransferListWidget]</a>
|
||||
<ul id="contextCategoryList" class="scrollableMenu"></ul>
|
||||
@ -225,6 +231,9 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul id="multiRenameFilesMenu" class="contextMenu">
|
||||
<li><a href="#ToggleSelection"><img src="images/edit-rename.svg" alt="QBT_TR(Toggle Selection)QBT_TR[CONTEXT=PropertiesWidget]" /> QBT_TR(Toggle Selection)QBT_TR[CONTEXT=PropertiesWidget]</a></li>
|
||||
</ul>
|
||||
<div id="desktopFooterWrapper">
|
||||
<div id="desktopFooter">
|
||||
<span id="error_div"></span>
|
||||
|
489
src/webui/www/private/rename_files.html
Normal file
489
src/webui/www/private/rename_files.html
Normal file
@ -0,0 +1,489 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="${LANG}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>QBT_TR(Renaming))QBT_TR[CONTEXT=TorrentContentTreeView]</title>
|
||||
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
|
||||
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
|
||||
<script src="scripts/filesystem.js?v=${CACHEID}"></script>
|
||||
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script src="scripts/file-tree.js?v=${CACHEID}"></script>
|
||||
<script src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script>
|
||||
<script src="scripts/rename-files.js?v=${CACHEID}"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
if (window.parent.qBittorrent !== undefined) {
|
||||
window.qBittorrent = window.parent.qBittorrent;
|
||||
}
|
||||
window.qBittorrent = window.parent.qBittorrent;
|
||||
|
||||
var TriState = window.qBittorrent.FileTree.TriState;
|
||||
var data = window.MUI.Windows.instances['multiRenamePage'].options.data;
|
||||
var bulkRenameFilesContextMenu;
|
||||
if (!bulkRenameFilesContextMenu) {
|
||||
bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
|
||||
targets: '#bulkRenameFilesTableDiv tr',
|
||||
menu: 'multiRenameFilesMenu',
|
||||
actions: {
|
||||
ToggleSelection: function(element, ref) {
|
||||
const rowId = parseInt(element.get('data-row-id'));
|
||||
const row = bulkRenameFilesTable.getNode(rowId);
|
||||
const checkState = row.checked == 1 ? 0 : 1;
|
||||
bulkRenameFilesTable.toggleNodeTreeCheckbox(rowId, checkState);
|
||||
bulkRenameFilesTable.updateGlobalCheckbox();
|
||||
bulkRenameFilesTable.onRowSelectionChange(bulkRenameFilesTable.getSelectedRows());
|
||||
}
|
||||
},
|
||||
offsets: {
|
||||
x: -15,
|
||||
y: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup the dynamic table for bulk renaming
|
||||
var bulkRenameFilesTable = new window.qBittorrent.DynamicTable.BulkRenameTorrentFilesTable();
|
||||
bulkRenameFilesTable.setup('bulkRenameFilesTableDiv', 'bulkRenameFilesTableFixedHeaderDiv', bulkRenameFilesContextMenu);
|
||||
|
||||
// Inject checkbox into the first column of the table header
|
||||
var tableHeaders = $$('#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th');
|
||||
var checkboxHeader;
|
||||
if (tableHeaders.length > 0) {
|
||||
if (checkboxHeader) {
|
||||
checkboxHeader.remove();
|
||||
}
|
||||
checkboxHeader = new Element('input');
|
||||
checkboxHeader.set('type', 'checkbox');
|
||||
checkboxHeader.set('id', 'rootMultiRename_cb');
|
||||
checkboxHeader.addEvent('click', function(e) {
|
||||
bulkRenameFilesTable.toggleGlobalCheckbox();
|
||||
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows();
|
||||
fileRenamer.update();
|
||||
});
|
||||
|
||||
const checkboxTH = tableHeaders[0];
|
||||
checkboxHeader.injectInside(checkboxTH);
|
||||
}
|
||||
|
||||
// Register keyboard events to modal window
|
||||
if (!keyboard) {
|
||||
var keyboard = new Keyboard({
|
||||
defaultEventType: 'keydown',
|
||||
events: {
|
||||
'Escape': function(event) {
|
||||
window.parent.closeWindows();
|
||||
event.preventDefault();
|
||||
},
|
||||
'Esc': function(event) {
|
||||
window.parent.closeWindows();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
keyboard.activate();
|
||||
}
|
||||
|
||||
var fileRenamer = new window.qBittorrent.MultiRename.RenameFiles();
|
||||
fileRenamer.hash = data.hash;
|
||||
|
||||
// Load Multi Rename Preferences
|
||||
var multiRenamePrefChecked = LocalPreferences.get('multirename_rememberPreferences', "true") === "true";
|
||||
$('multirename_rememberprefs_checkbox').setProperty('checked', multiRenamePrefChecked);
|
||||
|
||||
if (multiRenamePrefChecked) {
|
||||
var multirename_search = LocalPreferences.get('multirename_search', '');
|
||||
fileRenamer.setSearch(multirename_search);
|
||||
$('multiRenameSearch').set('value', multirename_search);
|
||||
|
||||
var multirename_useRegex = LocalPreferences.get('multirename_useRegex', false);
|
||||
fileRenamer.useRegex = multirename_useRegex === 'true';
|
||||
$('use_regex_search').checked = fileRenamer.useRegex;
|
||||
|
||||
var multirename_matchAllOccurences = LocalPreferences.get('multirename_matchAllOccurences', false);
|
||||
fileRenamer.matchAllOccurences = multirename_matchAllOccurences === 'true';
|
||||
$('match_all_occurences').checked = fileRenamer.matchAllOccurences;
|
||||
|
||||
var multirename_caseSensitive = LocalPreferences.get('multirename_caseSensitive', false);
|
||||
fileRenamer.caseSensitive = multirename_caseSensitive === 'true';
|
||||
$('case_sensitive').checked = fileRenamer.caseSensitive;
|
||||
|
||||
var multirename_replace = LocalPreferences.get('multirename_replace', '');
|
||||
fileRenamer.setReplacement(multirename_replace);
|
||||
$('multiRenameReplace').set('value', multirename_replace);
|
||||
|
||||
var multirename_appliesTo = LocalPreferences.get('multirename_appliesTo', window.qBittorrent.MultiRename.AppliesTo.FilenameExtension);
|
||||
fileRenamer.appliesTo = window.qBittorrent.MultiRename.AppliesTo[multirename_appliesTo];
|
||||
$('applies_to_option').set('value', fileRenamer.appliesTo);
|
||||
|
||||
var multirename_includeFiles = LocalPreferences.get('multirename_includeFiles', true);
|
||||
fileRenamer.includeFiles = multirename_includeFiles === 'true';
|
||||
$('include_files').checked = fileRenamer.includeFiles;
|
||||
|
||||
var multirename_includeFolders = LocalPreferences.get('multirename_includeFolders', false);
|
||||
fileRenamer.includeFolders = multirename_includeFolders === 'true';
|
||||
$('include_folders').checked = fileRenamer.includeFolders;
|
||||
|
||||
var multirename_fileEnumerationStart = LocalPreferences.get('multirename_fileEnumerationStart', 0);
|
||||
fileRenamer.fileEnumerationStart = parseInt(multirename_fileEnumerationStart);
|
||||
$('file_counter').set('value', fileRenamer.fileEnumerationStart);
|
||||
|
||||
var multirename_replaceAll = LocalPreferences.get('multirename_replaceAll', false);
|
||||
fileRenamer.replaceAll = multirename_replaceAll === 'true';
|
||||
var renameButtonValue = fileRenamer.replaceAll ? 'Replace All' : 'Replace';
|
||||
$('renameOptions').set('value', renameButtonValue);
|
||||
$('renameButton').set('value', renameButtonValue);
|
||||
}
|
||||
|
||||
// Fires everytime a row's selection changes
|
||||
bulkRenameFilesTable.onRowSelectionChange = function(row) {
|
||||
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows();
|
||||
fileRenamer.update();
|
||||
};
|
||||
|
||||
// Setup Search Events that control renaming
|
||||
$('multiRenameSearch').addEvent('input', function(e) {
|
||||
let sanitized = e.target.value.replace(/\n/g, '');
|
||||
$('multiRenameSearch').set('value', sanitized);
|
||||
|
||||
// Search input has changed
|
||||
$('multiRenameSearch').style['border-color'] = '';
|
||||
LocalPreferences.set('multirename_search', sanitized);
|
||||
fileRenamer.setSearch(sanitized);
|
||||
});
|
||||
$('use_regex_search').addEvent('change', function(e) {
|
||||
fileRenamer.useRegex = e.target.checked;
|
||||
LocalPreferences.set('multirename_useRegex', e.target.checked);
|
||||
fileRenamer.update();
|
||||
});
|
||||
$('match_all_occurences').addEvent('change', function(e) {
|
||||
fileRenamer.matchAllOccurences = e.target.checked;
|
||||
LocalPreferences.set('multirename_matchAllOccurences', e.target.checked);
|
||||
fileRenamer.update();
|
||||
});
|
||||
$('case_sensitive').addEvent('change', function(e) {
|
||||
fileRenamer.caseSensitive = e.target.checked;
|
||||
LocalPreferences.set('multirename_caseSensitive', e.target.checked);
|
||||
fileRenamer.update();
|
||||
});
|
||||
|
||||
/**
|
||||
* Fires every time the filerenamer gets changed, it will update all the rows in the table
|
||||
*/
|
||||
fileRenamer.onChanged = function(matchedRows) {
|
||||
// Clear renamed column
|
||||
document
|
||||
.querySelectorAll("span[id^='filesTablefileRenamed']")
|
||||
.forEach(function(span) {
|
||||
span.set('text', "");
|
||||
});
|
||||
|
||||
// Update renamed column for matched rows
|
||||
for (let i = 0; i < matchedRows.length; ++i) {
|
||||
const row = matchedRows[i];
|
||||
$('filesTablefileRenamed' + row.rowId).set('text', row.renamed);
|
||||
}
|
||||
};
|
||||
fileRenamer.onInvalidRegex = function(err) {
|
||||
$('multiRenameSearch').style['border-color'] = '#CC0033';
|
||||
};
|
||||
|
||||
// Setup Replace Events that control renaming
|
||||
$('multiRenameReplace').addEvent('input', function(e) {
|
||||
let sanitized = e.target.value.replace(/\n/g, '');
|
||||
$('multiRenameReplace').set('value', sanitized);
|
||||
|
||||
// Replace input has changed
|
||||
$('multiRenameReplace').style['border-color'] = '';
|
||||
LocalPreferences.set('multirename_replace', sanitized);
|
||||
fileRenamer.setReplacement(sanitized);
|
||||
});
|
||||
$('applies_to_option').addEvent('change', function(e) {
|
||||
fileRenamer.appliesTo = e.target.value;
|
||||
LocalPreferences.set('multirename_appliesTo', e.target.value);
|
||||
fileRenamer.update();
|
||||
});
|
||||
$('include_files').addEvent('change', function(e) {
|
||||
fileRenamer.includeFiles = e.target.checked;
|
||||
LocalPreferences.set('multirename_includeFiles', e.target.checked);
|
||||
fileRenamer.update();
|
||||
});
|
||||
$('include_folders').addEvent('change', function(e) {
|
||||
fileRenamer.includeFolders = e.target.checked;
|
||||
LocalPreferences.set('multirename_includeFolders', e.target.checked);
|
||||
fileRenamer.update();
|
||||
});
|
||||
$('file_counter').addEvent('input', function(e) {
|
||||
let value = e.target.valueAsNumber;
|
||||
if (!value) { value = 0; }
|
||||
if (value < 0) { value = 0; }
|
||||
if (value > 99999999) { value = 99999999; }
|
||||
fileRenamer.fileEnumerationStart = value;
|
||||
$('file_counter').set('value', value);
|
||||
LocalPreferences.set('multirename_fileEnumerationStart', value);
|
||||
fileRenamer.update();
|
||||
});
|
||||
|
||||
// Setup Rename Operation Events
|
||||
$('renameButton').addEvent('click', function(e) {
|
||||
// Disable Search Options
|
||||
$('multiRenameSearch').disabled = true;
|
||||
$('use_regex_search').disabled = true;
|
||||
$('match_all_occurences').disabled = true;
|
||||
$('case_sensitive').disabled = true;
|
||||
// Disable Replace Options
|
||||
$('multiRenameReplace').disabled = true;
|
||||
$('applies_to_option').disabled = true;
|
||||
$('include_files').disabled = true;
|
||||
$('include_folders').disabled = true;
|
||||
$('file_counter').disabled = true;
|
||||
// Disable Rename Buttons
|
||||
$('renameButton').disabled = true;
|
||||
$('renameOptions').disabled = true;
|
||||
// Clear error text
|
||||
$('rename_error').set('text', '');
|
||||
fileRenamer.rename();
|
||||
});
|
||||
fileRenamer.onRenamed = function(rows) {
|
||||
// Disable Search Options
|
||||
$('multiRenameSearch').disabled = false;
|
||||
$('use_regex_search').disabled = false;
|
||||
$('match_all_occurences').disabled = false;
|
||||
$('case_sensitive').disabled = false;
|
||||
// Disable Replace Options
|
||||
$('multiRenameReplace').disabled = false;
|
||||
$('applies_to_option').disabled = false;
|
||||
$('include_files').disabled = false;
|
||||
$('include_folders').disabled = false;
|
||||
$('file_counter').disabled = false;
|
||||
// Disable Rename Buttons
|
||||
$('renameButton').disabled = false;
|
||||
$('renameOptions').disabled = false;
|
||||
|
||||
// Recreate table
|
||||
let selectedRows = bulkRenameFilesTable.getSelectedRows().map(row => row.rowId.toString());
|
||||
for (let renamedRow of rows) {
|
||||
selectedRows = selectedRows.filter(selectedRow => selectedRow !== renamedRow.rowId.toString());
|
||||
}
|
||||
bulkRenameFilesTable.clear();
|
||||
|
||||
// Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts
|
||||
if (!fileRenamer.replaceAll) {
|
||||
fileRenamer.fileEnumerationStart++;
|
||||
$('file_counter').set('value', fileRenamer.fileEnumerationStart);
|
||||
}
|
||||
setupTable(selectedRows);
|
||||
};
|
||||
fileRenamer.onRenameError = function(err, row) {
|
||||
if (err.xhr.status === 409) {
|
||||
$('rename_error').set('text', `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}\``);
|
||||
}
|
||||
};
|
||||
$('renameOptions').addEvent('change', function(e) {
|
||||
const combobox = e.target;
|
||||
const replaceOperation = combobox.value;
|
||||
if (replaceOperation == "Replace") {
|
||||
fileRenamer.replaceAll = false;
|
||||
}
|
||||
else if (replaceOperation == "Replace All") {
|
||||
fileRenamer.replaceAll = true;
|
||||
}
|
||||
else {
|
||||
fileRenamer.replaceAll = false;
|
||||
}
|
||||
LocalPreferences.set('multirename_replaceAll', fileRenamer.replaceAll);
|
||||
$('renameButton').set('value', replaceOperation);
|
||||
});
|
||||
$('closeButton').addEvent('click', function() {
|
||||
window.parent.closeWindows();
|
||||
event.preventDefault();
|
||||
});
|
||||
// synchronize header scrolling to table body
|
||||
$('bulkRenameFilesTableDiv').onscroll = function() {
|
||||
const length = $(this).scrollLeft;
|
||||
$('bulkRenameFilesTableFixedHeaderDiv').scrollLeft = length;
|
||||
};
|
||||
|
||||
var handleTorrentFiles = function(files, selectedRows) {
|
||||
const rows = files.map(function(file, index) {
|
||||
|
||||
const row = {
|
||||
fileId: index,
|
||||
checked: 1, // unchecked
|
||||
path: file.name,
|
||||
original: window.qBittorrent.Filesystem.fileName(file.name),
|
||||
renamed: "",
|
||||
size: file.size
|
||||
};
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
addRowsToTable(rows, selectedRows);
|
||||
};
|
||||
|
||||
var addRowsToTable = function(rows, selectedRows) {
|
||||
let rowId = 0;
|
||||
const rootNode = new window.qBittorrent.FileTree.FolderNode();
|
||||
rootNode.autoCheckFolders = false;
|
||||
|
||||
rows.forEach(function(row) {
|
||||
const pathItems = row.path.split(window.qBittorrent.Filesystem.PathSeparator);
|
||||
|
||||
pathItems.pop(); // remove last item (i.e. file name)
|
||||
let parent = rootNode;
|
||||
pathItems.forEach(function(folderName) {
|
||||
if (folderName === '.unwanted') {
|
||||
return;
|
||||
}
|
||||
|
||||
let folderNode = null;
|
||||
if (parent.children !== null) {
|
||||
for (let i = 0; i < parent.children.length; ++i) {
|
||||
const childFolder = parent.children[i];
|
||||
if (childFolder.original === folderName) {
|
||||
folderNode = childFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (folderNode === null) {
|
||||
folderNode = new window.qBittorrent.FileTree.FolderNode();
|
||||
folderNode.autoCheckFolders = false;
|
||||
folderNode.rowId = rowId;
|
||||
folderNode.path = (parent.path === "")
|
||||
? folderName
|
||||
: [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator);
|
||||
folderNode.checked = selectedRows.includes(rowId.toString()) ? 0 : 1;
|
||||
folderNode.original = folderName;
|
||||
folderNode.renamed = "";
|
||||
folderNode.root = parent;
|
||||
parent.addChild(folderNode);
|
||||
|
||||
++rowId;
|
||||
}
|
||||
|
||||
parent = folderNode;
|
||||
});
|
||||
|
||||
const childNode = new window.qBittorrent.FileTree.FileNode();
|
||||
childNode.rowId = rowId;
|
||||
childNode.path = row.path;
|
||||
childNode.checked = selectedRows.includes(rowId.toString()) ? 0 : 1;
|
||||
childNode.original = row.original;
|
||||
childNode.renamed = "";
|
||||
childNode.root = parent;
|
||||
childNode.data = row;
|
||||
parent.addChild(childNode);
|
||||
|
||||
++rowId;
|
||||
});
|
||||
|
||||
bulkRenameFilesTable.populateTable(rootNode);
|
||||
bulkRenameFilesTable.updateTable(false);
|
||||
bulkRenameFilesTable.altRow();
|
||||
|
||||
if (selectedRows !== undefined) {
|
||||
bulkRenameFilesTable.reselectRows(selectedRows);
|
||||
}
|
||||
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows();
|
||||
fileRenamer.update();
|
||||
};
|
||||
|
||||
var setupTable = function(selectedRows) {
|
||||
new Request.JSON({
|
||||
url: new URI('api/v2/torrents/files?hash=' + data.hash),
|
||||
noCache: true,
|
||||
method: 'get',
|
||||
onSuccess: function(files) {
|
||||
if (files.length === 0) {
|
||||
bulkRenameFilesTable.clear();
|
||||
}
|
||||
else {
|
||||
handleTorrentFiles(files, selectedRows);
|
||||
}
|
||||
}
|
||||
}).send();
|
||||
};
|
||||
setupTable(data.selectedRows);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="min-width: 400px; min-height: 300px;">
|
||||
<div style="padding: 0px 10px 0px 0px;">
|
||||
<div style="float: left; height: 100%; width: 228px;">
|
||||
<div class="formRow">
|
||||
<input type="checkbox" id="multirename_rememberprefs_checkbox" onchange="LocalPreferences.set('multirename_rememberPreferences', this.checked);" />
|
||||
<label for="multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]</label>
|
||||
</div>
|
||||
<hr>
|
||||
<textarea id="multiRenameSearch" placeholder="QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea>
|
||||
<div class="formRow">
|
||||
<input type="checkbox" id="use_regex_search" />
|
||||
<label for="use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]</label>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<input type="checkbox" id="match_all_occurences" />
|
||||
<label for="match_all_occurences">QBT_TR(Match all occurences)QBT_TR[CONTEXT=PropertiesWidget]</label>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<input type="checkbox" id="case_sensitive" />
|
||||
<label for="case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]</label>
|
||||
</div>
|
||||
<hr>
|
||||
<textarea id="multiRenameReplace" placeholder="QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea>
|
||||
<select id="applies_to_option" name="applies_to_option" style="width: 100%; margin-bottom: 5px;">
|
||||
<option selected value="FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]</option>
|
||||
<option value="Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]</option>
|
||||
<option value="Extension">QBT_TR(Extension)QBT_TR[CONTEXT=PropertiesWidget]</option>
|
||||
</select>
|
||||
<div class="formRow">
|
||||
<input type="checkbox" id="include_files" checked />
|
||||
<label for="include_files">QBT_TR(Include files)QBT_TR[CONTEXT=PropertiesWidget]</label>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<input type="checkbox" id="include_folders" />
|
||||
<label for="include_folders">QBT_TR(Include folders)QBT_TR[CONTEXT=PropertiesWidget]</label>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<input type="number" min="0" max="99999999" value="0" id="file_counter" style="width: 80px;" />
|
||||
<label for="file_counter">QBT_TR(Enumerate Files)QBT_TR[CONTEXT=PropertiesWidget]</label>
|
||||
</div>
|
||||
</div>
|
||||
<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;">
|
||||
<div style="overflow: auto;">
|
||||
<span id="rename_error" style="float: unset; font-size: unset;"></span>
|
||||
</div>
|
||||
<hr>
|
||||
<div style="width: 60%; float: left;">
|
||||
<input id="renameButton" type="button" value="Replace" style="float: left; width: 86px;">
|
||||
<select id="renameOptions" name="renameOptions" style="width: 22px;">
|
||||
<option selected value="Replace">QBT_TR(Replace)QBT_TR[CONTEXT=PropertiesWidget]</option>
|
||||
<option value="Replace All">QBT_TR(Replace All)QBT_TR[CONTEXT=PropertiesWidget]</option>
|
||||
</select>
|
||||
</div>
|
||||
<input id="closeButton" type="button" value="Close" style="float: right; width: 30%;">
|
||||
</div>
|
||||
<div id="torrentFiles" class="panel" style="position: absolute; top: 0; right: 0; bottom: 0; left: 228px; margin: 35px 10px 45px 20px; border-bottom: 0">
|
||||
<div id="bulkRenameFilesTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
|
||||
<table class="dynamicTable">
|
||||
<thead>
|
||||
<tr class="dynamicTableHeader"></tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div id="bulkRenameFilesTableDiv" class="dynamicTableDiv">
|
||||
<table class="dynamicTable">
|
||||
<thead>
|
||||
<tr class="dynamicTableHeader"></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -362,6 +362,19 @@ window.qBittorrent.ContextMenu = (function() {
|
||||
|
||||
let show_seq_dl = true;
|
||||
|
||||
// hide renameFiles when more than 1 torrent is selected
|
||||
if (h.length == 1) {
|
||||
const data = torrentsTable.rows.get(h[0]).full_data;
|
||||
let metadata_downloaded = !(data['state'] == 'metaDL' || data['state'] == 'forcedMetaDL' || data['total_size'] == -1);
|
||||
|
||||
// hide renameFiles when metadata hasn't been downloaded yet
|
||||
metadata_downloaded
|
||||
? this.showItem('renameFiles')
|
||||
: this.hideItem('renameFiles');
|
||||
}
|
||||
else
|
||||
this.hideItem('renameFiles');
|
||||
|
||||
if (!all_are_seq_dl && there_are_seq_dl)
|
||||
show_seq_dl = false;
|
||||
|
||||
|
@ -45,6 +45,7 @@ window.qBittorrent.DynamicTable = (function() {
|
||||
SearchResultsTable: SearchResultsTable,
|
||||
SearchPluginsTable: SearchPluginsTable,
|
||||
TorrentTrackersTable: TorrentTrackersTable,
|
||||
BulkRenameTorrentFilesTable: BulkRenameTorrentFilesTable,
|
||||
TorrentFilesTable: TorrentFilesTable,
|
||||
LogMessageTable: LogMessageTable,
|
||||
LogPeerTable: LogPeerTable,
|
||||
@ -128,7 +129,14 @@ window.qBittorrent.DynamicTable = (function() {
|
||||
// Workaround. Resize event is called not always (for example it isn't called when browser window changes it's size)
|
||||
|
||||
const checkResizeFn = function() {
|
||||
const panel = $(this.dynamicTableDivId).getParent('.panel');
|
||||
const tableDiv = $(this.dynamicTableDivId);
|
||||
|
||||
// dynamicTableDivId is not visible on the UI
|
||||
if (!tableDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = tableDiv.getParent('.panel');
|
||||
if (this.lastPanelHeight != panel.getBoundingClientRect().height) {
|
||||
this.lastPanelHeight = panel.getBoundingClientRect().height;
|
||||
panel.fireEvent('resize');
|
||||
@ -333,7 +341,8 @@ window.qBittorrent.DynamicTable = (function() {
|
||||
|
||||
const menuId = this.dynamicTableDivId + '_headerMenu';
|
||||
|
||||
const ul = new Element('ul', {
|
||||
// reuse menu if already exists
|
||||
const ul = $(menuId) ?? new Element('ul', {
|
||||
id: menuId,
|
||||
class: 'contextMenu scrollableMenu'
|
||||
});
|
||||
@ -351,6 +360,13 @@ window.qBittorrent.DynamicTable = (function() {
|
||||
this.showColumn(action, this.columns[action].visible === '0');
|
||||
}.bind(this);
|
||||
|
||||
// recreate child nodes when reusing (enables the context menu to work correctly)
|
||||
if (ul.hasChildNodes()) {
|
||||
while (ul.firstChild) {
|
||||
ul.removeChild(ul.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.columns.length; ++i) {
|
||||
const text = this.columns[i].caption;
|
||||
if (text === '')
|
||||
@ -1777,6 +1793,431 @@ window.qBittorrent.DynamicTable = (function() {
|
||||
},
|
||||
});
|
||||
|
||||
const BulkRenameTorrentFilesTable = new Class({
|
||||
Extends: DynamicTable,
|
||||
|
||||
filterTerms: [],
|
||||
prevFilterTerms: [],
|
||||
prevRowsString: null,
|
||||
prevFilteredRows: [],
|
||||
prevSortedColumn: null,
|
||||
prevReverseSort: null,
|
||||
fileTree: new window.qBittorrent.FileTree.FileTree(),
|
||||
|
||||
populateTable: function(root) {
|
||||
this.fileTree.setRoot(root);
|
||||
root.children.each(function(node) {
|
||||
this._addNodeToTable(node, 0);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_addNodeToTable: function(node, depth) {
|
||||
node.depth = depth;
|
||||
|
||||
if (node.isFolder) {
|
||||
const data = {
|
||||
rowId: node.rowId,
|
||||
fileId: -1,
|
||||
checked: node.checked,
|
||||
path: node.path,
|
||||
original: node.original,
|
||||
renamed: node.renamed
|
||||
};
|
||||
|
||||
node.data = data;
|
||||
node.full_data = data;
|
||||
this.updateRowData(data);
|
||||
}
|
||||
else {
|
||||
node.data.rowId = node.rowId;
|
||||
node.full_data = node.data;
|
||||
this.updateRowData(node.data);
|
||||
}
|
||||
|
||||
node.children.each(function(child) {
|
||||
this._addNodeToTable(child, depth + 1);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getRoot: function() {
|
||||
return this.fileTree.getRoot();
|
||||
},
|
||||
|
||||
getNode: function(rowId) {
|
||||
return this.fileTree.getNode(rowId);
|
||||
},
|
||||
|
||||
getRow: function(node) {
|
||||
const rowId = this.fileTree.getRowId(node);
|
||||
return this.rows.get(rowId);
|
||||
},
|
||||
|
||||
getSelectedRows: function() {
|
||||
const nodes = this.fileTree.toArray();
|
||||
|
||||
return nodes.filter(x => x.checked == 0);
|
||||
},
|
||||
|
||||
initColumns: function() {
|
||||
// Blocks saving header width (because window width isn't saved)
|
||||
LocalPreferences.remove('column_' + "checked" + '_width_' + this.dynamicTableDivId);
|
||||
LocalPreferences.remove('column_' + "original" + '_width_' + this.dynamicTableDivId);
|
||||
LocalPreferences.remove('column_' + "renamed" + '_width_' + this.dynamicTableDivId);
|
||||
this.newColumn('checked', '', '', 50, true);
|
||||
this.newColumn('original', '', 'QBT_TR(Original)QBT_TR[CONTEXT=TrackerListWidget]', 270, true);
|
||||
this.newColumn('renamed', '', 'QBT_TR(Renamed)QBT_TR[CONTEXT=TrackerListWidget]', 220, true);
|
||||
|
||||
this.initColumnsFunctions();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the global checkbox and all checkboxes underneath
|
||||
*/
|
||||
toggleGlobalCheckbox: function() {
|
||||
const checkbox = $('rootMultiRename_cb');
|
||||
const checkboxes = $$('input.RenamingCB');
|
||||
|
||||
for (let i = 0; i < checkboxes.length; ++i) {
|
||||
const node = this.getNode(i);
|
||||
|
||||
if (checkbox.checked || checkbox.indeterminate) {
|
||||
let cb = checkboxes[i];
|
||||
cb.checked = true;
|
||||
cb.indeterminate = false;
|
||||
cb.state = "checked";
|
||||
node.checked = 0;
|
||||
node.full_data.checked = node.checked;
|
||||
}
|
||||
else {
|
||||
let cb = checkboxes[i];
|
||||
cb.checked = false;
|
||||
cb.indeterminate = false;
|
||||
cb.state = "unchecked";
|
||||
node.checked = 1;
|
||||
node.full_data.checked = node.checked;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateGlobalCheckbox();
|
||||
},
|
||||
|
||||
toggleNodeTreeCheckbox: function(rowId, checkState) {
|
||||
const node = this.getNode(rowId);
|
||||
node.checked = checkState;
|
||||
node.full_data.checked = checkState;
|
||||
const checkbox = $(`cbRename${rowId}`);
|
||||
checkbox.checked = node.checked == 0;
|
||||
checkbox.state = checkbox.checked ? "checked" : "unchecked";
|
||||
|
||||
for (let i = 0; i < node.children.length; ++i) {
|
||||
this.toggleNodeTreeCheckbox(node.children[i].rowId, checkState);
|
||||
}
|
||||
},
|
||||
|
||||
updateGlobalCheckbox: function() {
|
||||
const checkbox = $('rootMultiRename_cb');
|
||||
const checkboxes = $$('input.RenamingCB');
|
||||
const isAllChecked = function() {
|
||||
for (let i = 0; i < checkboxes.length; ++i) {
|
||||
if (!checkboxes[i].checked)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const isAllUnchecked = function() {
|
||||
for (let i = 0; i < checkboxes.length; ++i) {
|
||||
if (checkboxes[i].checked)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (isAllChecked()) {
|
||||
checkbox.state = "checked";
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = true;
|
||||
}
|
||||
else if (isAllUnchecked()) {
|
||||
checkbox.state = "unchecked";
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = false;
|
||||
}
|
||||
else {
|
||||
checkbox.state = "partial";
|
||||
checkbox.indeterminate = true;
|
||||
checkbox.checked = false;
|
||||
}
|
||||
},
|
||||
|
||||
initColumnsFunctions: function() {
|
||||
const that = this;
|
||||
|
||||
// checked
|
||||
this.columns['checked'].updateTd = function(td, row) {
|
||||
const id = row.rowId;
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
const treeImg = new Element('img', {
|
||||
src: 'images/L.gif',
|
||||
styles: {
|
||||
'margin-bottom': -2
|
||||
}
|
||||
});
|
||||
const checkbox = new Element('input');
|
||||
checkbox.set('type', 'checkbox');
|
||||
checkbox.set('id', 'cbRename' + id);
|
||||
checkbox.set('data-id', id);
|
||||
checkbox.set('class', 'RenamingCB');
|
||||
checkbox.addEvent('click', function(e) {
|
||||
const node = that.getNode(id);
|
||||
node.checked = e.target.checked ? 0 : 1;
|
||||
node.full_data.checked = node.checked;
|
||||
that.updateGlobalCheckbox();
|
||||
that.onRowSelectionChange(node);
|
||||
e.stopPropagation();
|
||||
});
|
||||
checkbox.checked = value == 0;
|
||||
checkbox.state = checkbox.checked ? "checked" : "unchecked";
|
||||
checkbox.indeterminate = false;
|
||||
td.adopt(treeImg, checkbox);
|
||||
};
|
||||
|
||||
// original
|
||||
this.columns['original'].updateTd = function(td, row) {
|
||||
const id = row.rowId;
|
||||
const fileNameId = 'filesTablefileName' + id;
|
||||
const node = that.getNode(id);
|
||||
|
||||
if (node.isFolder) {
|
||||
const value = this.getRowValue(row);
|
||||
const dirImgId = 'renameTableDirImg' + id;
|
||||
if ($(dirImgId)) {
|
||||
// just update file name
|
||||
$(fileNameId).set('text', value);
|
||||
}
|
||||
else {
|
||||
const span = new Element('span', {
|
||||
text: value,
|
||||
id: fileNameId
|
||||
});
|
||||
const dirImg = new Element('img', {
|
||||
src: 'images/directory.svg',
|
||||
styles: {
|
||||
'width': 15,
|
||||
'padding-right': 5,
|
||||
'margin-bottom': -3,
|
||||
'margin-left': (node.depth * 20)
|
||||
},
|
||||
id: dirImgId
|
||||
});
|
||||
const html = dirImg.outerHTML + span.outerHTML;
|
||||
td.set('html', html);
|
||||
}
|
||||
}
|
||||
else { // is file
|
||||
const value = this.getRowValue(row);
|
||||
const span = new Element('span', {
|
||||
text: value,
|
||||
id: fileNameId,
|
||||
styles: {
|
||||
'margin-left': ((node.depth + 1) * 20)
|
||||
}
|
||||
});
|
||||
td.set('html', span.outerHTML);
|
||||
}
|
||||
};
|
||||
|
||||
// renamed
|
||||
this.columns['renamed'].updateTd = function(td, row) {
|
||||
const id = row.rowId;
|
||||
const fileNameRenamedId = 'filesTablefileRenamed' + id;
|
||||
const value = this.getRowValue(row);
|
||||
|
||||
const span = new Element('span', {
|
||||
text: value,
|
||||
id: fileNameRenamedId,
|
||||
});
|
||||
td.set('html', span.outerHTML);
|
||||
};
|
||||
},
|
||||
|
||||
onRowSelectionChange: function(row) {},
|
||||
|
||||
selectRow: function() {
|
||||
return;
|
||||
},
|
||||
|
||||
reselectRows: function(rowIds) {
|
||||
const that = this;
|
||||
this.deselectAll();
|
||||
this.tableBody.getElements('tr').each(function(tr) {
|
||||
if (rowIds.indexOf(tr.rowId) > -1) {
|
||||
const node = that.getNode(tr.rowId);
|
||||
node.checked = 0;
|
||||
node.full_data.checked = 0;
|
||||
|
||||
const checkbox = tr.children[0].getElement('input');
|
||||
checkbox.state = "checked";
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateGlobalCheckbox();
|
||||
},
|
||||
|
||||
altRow: function() {
|
||||
let addClass = false;
|
||||
const trs = this.tableBody.getElements('tr');
|
||||
trs.each(function(tr) {
|
||||
if (tr.hasClass("invisible"))
|
||||
return;
|
||||
|
||||
if (addClass) {
|
||||
tr.addClass("alt");
|
||||
tr.removeClass("nonAlt");
|
||||
}
|
||||
else {
|
||||
tr.removeClass("alt");
|
||||
tr.addClass("nonAlt");
|
||||
}
|
||||
addClass = !addClass;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_sortNodesByColumn: function(nodes, column) {
|
||||
nodes.sort(function(row1, row2) {
|
||||
// list folders before files when sorting by name
|
||||
if (column.name === "original") {
|
||||
const node1 = this.getNode(row1.data.rowId);
|
||||
const node2 = this.getNode(row2.data.rowId);
|
||||
if (node1.isFolder && !node2.isFolder)
|
||||
return -1;
|
||||
if (node2.isFolder && !node1.isFolder)
|
||||
return 1;
|
||||
}
|
||||
|
||||
const res = column.compareRows(row1, row2);
|
||||
return (this.reverseSort === '0') ? res : -res;
|
||||
}.bind(this));
|
||||
|
||||
nodes.each(function(node) {
|
||||
if (node.children.length > 0)
|
||||
this._sortNodesByColumn(node.children, column);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_filterNodes: function(node, filterTerms, filteredRows) {
|
||||
if (node.isFolder) {
|
||||
const childAdded = node.children.reduce(function(acc, child) {
|
||||
// we must execute the function before ORing w/ acc or we'll stop checking child nodes after the first successful match
|
||||
return (this._filterNodes(child, filterTerms, filteredRows) || acc);
|
||||
}.bind(this), false);
|
||||
|
||||
if (childAdded) {
|
||||
const row = this.getRow(node);
|
||||
filteredRows.push(row);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.qBittorrent.Misc.containsAllTerms(node.original, filterTerms)) {
|
||||
const row = this.getRow(node);
|
||||
filteredRows.push(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
setFilter: function(text) {
|
||||
const filterTerms = text.trim().toLowerCase().split(' ');
|
||||
if ((filterTerms.length === 1) && (filterTerms[0] === ''))
|
||||
this.filterTerms = [];
|
||||
else
|
||||
this.filterTerms = filterTerms;
|
||||
},
|
||||
|
||||
getFilteredAndSortedRows: function() {
|
||||
if (this.getRoot() === null)
|
||||
return [];
|
||||
|
||||
const generateRowsSignature = function(rows) {
|
||||
const rowsData = rows.map(function(row) {
|
||||
return row.full_data;
|
||||
});
|
||||
return JSON.stringify(rowsData);
|
||||
};
|
||||
|
||||
const getFilteredRows = function() {
|
||||
if (this.filterTerms.length === 0) {
|
||||
const nodeArray = this.fileTree.toArray();
|
||||
const filteredRows = nodeArray.map(function(node) {
|
||||
return this.getRow(node);
|
||||
}.bind(this));
|
||||
return filteredRows;
|
||||
}
|
||||
|
||||
const filteredRows = [];
|
||||
this.getRoot().children.each(function(child) {
|
||||
this._filterNodes(child, this.filterTerms, filteredRows);
|
||||
}.bind(this));
|
||||
filteredRows.reverse();
|
||||
return filteredRows;
|
||||
}.bind(this);
|
||||
|
||||
const hasRowsChanged = function(rowsString, prevRowsStringString) {
|
||||
const rowsChanged = (rowsString !== prevRowsStringString);
|
||||
const isFilterTermsChanged = this.filterTerms.reduce(function(acc, term, index) {
|
||||
return (acc || (term !== this.prevFilterTerms[index]));
|
||||
}.bind(this), false);
|
||||
const isFilterChanged = ((this.filterTerms.length !== this.prevFilterTerms.length)
|
||||
|| ((this.filterTerms.length > 0) && isFilterTermsChanged));
|
||||
const isSortedColumnChanged = (this.prevSortedColumn !== this.sortedColumn);
|
||||
const isReverseSortChanged = (this.prevReverseSort !== this.reverseSort);
|
||||
|
||||
return (rowsChanged || isFilterChanged || isSortedColumnChanged || isReverseSortChanged);
|
||||
}.bind(this);
|
||||
|
||||
const rowsString = generateRowsSignature(this.rows);
|
||||
if (!hasRowsChanged(rowsString, this.prevRowsString)) {
|
||||
return this.prevFilteredRows;
|
||||
}
|
||||
|
||||
// sort, then filter
|
||||
const column = this.columns[this.sortedColumn];
|
||||
this._sortNodesByColumn(this.getRoot().children, column);
|
||||
const filteredRows = getFilteredRows();
|
||||
|
||||
this.prevFilterTerms = this.filterTerms;
|
||||
this.prevRowsString = rowsString;
|
||||
this.prevFilteredRows = filteredRows;
|
||||
this.prevSortedColumn = this.sortedColumn;
|
||||
this.prevReverseSort = this.reverseSort;
|
||||
return filteredRows;
|
||||
},
|
||||
|
||||
setIgnored: function(rowId, ignore) {
|
||||
const row = this.rows.get(rowId);
|
||||
if (ignore)
|
||||
row.full_data.remaining = 0;
|
||||
else
|
||||
row.full_data.remaining = (row.full_data.size * (1.0 - (row.full_data.progress / 100)));
|
||||
},
|
||||
|
||||
setupTr: function(tr) {
|
||||
tr.addEvent('keydown', function(event) {
|
||||
switch (event.key) {
|
||||
case "left":
|
||||
qBittorrent.PropFiles.collapseFolder(this._this.getSelectedRowId());
|
||||
return false;
|
||||
case "right":
|
||||
qBittorrent.PropFiles.expandFolder(this._this.getSelectedRowId());
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const TorrentFilesTable = new Class({
|
||||
Extends: DynamicTable,
|
||||
|
||||
|
@ -135,6 +135,11 @@ window.qBittorrent.FileTree = (function() {
|
||||
const FolderNode = new Class({
|
||||
Extends: FileNode,
|
||||
|
||||
/**
|
||||
* Will automatically tick the checkbox for a folder if all subfolders and files are also ticked
|
||||
*/
|
||||
autoCheckFolders: true,
|
||||
|
||||
initialize: function() {
|
||||
this.isFolder = true;
|
||||
},
|
||||
@ -184,7 +189,7 @@ window.qBittorrent.FileTree = (function() {
|
||||
|
||||
this.size = size;
|
||||
this.remaining = remaining;
|
||||
this.checked = checked;
|
||||
this.checked = this.autoCheckFolders ? checked : TriState.Checked;
|
||||
this.progress = (progress / size);
|
||||
this.priority = priority;
|
||||
this.availability = (availability / size);
|
||||
|
@ -62,6 +62,7 @@ let recheckFN = function() {};
|
||||
let reannounceFN = function() {};
|
||||
let setLocationFN = function() {};
|
||||
let renameFN = function() {};
|
||||
let renameFilesFN = function() {};
|
||||
let torrentNewCategoryFN = function() {};
|
||||
let torrentSetCategoryFN = function() {};
|
||||
let createCategoryFN = function() {};
|
||||
@ -523,6 +524,31 @@ const initializeWindows = function() {
|
||||
}
|
||||
};
|
||||
|
||||
renameFilesFN = function() {
|
||||
const hashes = torrentsTable.selectedRowsIds();
|
||||
if (hashes.length == 1) {
|
||||
const hash = hashes[0];
|
||||
const row = torrentsTable.rows[hash];
|
||||
if (row) {
|
||||
new MochaUI.Window({
|
||||
id: 'multiRenamePage',
|
||||
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
|
||||
data: { hash: hash, selectedRows: [] },
|
||||
loadMethod: 'xhr',
|
||||
contentURL: 'rename_files.html',
|
||||
scrollbars: false,
|
||||
resizable: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: 800,
|
||||
height: 420,
|
||||
resizeLimit: { 'x': [800], 'y': [420] }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
torrentNewCategoryFN = function() {
|
||||
const action = "set";
|
||||
const hashes = torrentsTable.selectedRowsIds();
|
||||
|
@ -54,6 +54,15 @@ window.qBittorrent.LocalPreferences = (function() {
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
remove: function(key) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -536,6 +536,51 @@ window.qBittorrent.PropFiles = (function() {
|
||||
setFilePriority(Object.keys(uniqueRowIds), Object.keys(uniqueFileIds), priority);
|
||||
};
|
||||
|
||||
const singleFileRename = function(hash) {
|
||||
const rowId = torrentFilesTable.selectedRowsIds()[0];
|
||||
if (rowId === undefined)
|
||||
return;
|
||||
const row = torrentFilesTable.rows[rowId];
|
||||
if (!row)
|
||||
return;
|
||||
|
||||
const node = torrentFilesTable.getNode(rowId);
|
||||
const path = node.path;
|
||||
|
||||
new MochaUI.Window({
|
||||
id: 'renamePage',
|
||||
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
|
||||
loadMethod: 'iframe',
|
||||
contentURL: 'rename_file.html?hash=' + hash + '&isFolder=' + node.isFolder
|
||||
+ '&path=' + encodeURIComponent(path),
|
||||
scrollbars: false,
|
||||
resizable: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: 400,
|
||||
height: 100
|
||||
});
|
||||
};
|
||||
|
||||
const multiFileRename = function(hash) {
|
||||
const win = new MochaUI.Window({
|
||||
id: 'multiRenamePage',
|
||||
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
|
||||
data: { hash: hash, selectedRows: torrentFilesTable.selectedRows },
|
||||
loadMethod: 'xhr',
|
||||
contentURL: 'rename_files.html',
|
||||
scrollbars: false,
|
||||
resizable: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: 800,
|
||||
height: 420,
|
||||
resizeLimit: { 'x': [800], 'y': [420] }
|
||||
});
|
||||
};
|
||||
|
||||
const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
|
||||
targets: '#torrentFilesTableDiv tr',
|
||||
menu: 'torrentFilesMenu',
|
||||
@ -544,30 +589,13 @@ window.qBittorrent.PropFiles = (function() {
|
||||
const hash = torrentsTable.getCurrentTorrentID();
|
||||
if (!hash)
|
||||
return;
|
||||
const rowId = torrentFilesTable.selectedRowsIds()[0];
|
||||
if (rowId === undefined)
|
||||
return;
|
||||
const row = torrentFilesTable.rows[rowId];
|
||||
if (!row)
|
||||
return;
|
||||
|
||||
const node = torrentFilesTable.getNode(rowId);
|
||||
const path = node.path;
|
||||
|
||||
new MochaUI.Window({
|
||||
id: 'renamePage',
|
||||
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
|
||||
loadMethod: 'iframe',
|
||||
contentURL: 'rename_file.html?hash=' + hash + '&isFolder=' + node.isFolder
|
||||
+ '&path=' + encodeURIComponent(path),
|
||||
scrollbars: false,
|
||||
resizable: true,
|
||||
maximizable: false,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: 400,
|
||||
height: 100
|
||||
});
|
||||
if (torrentFilesTable.selectedRowsIds().length > 1) {
|
||||
multiFileRename(hash);
|
||||
}
|
||||
else {
|
||||
singleFileRename(hash);
|
||||
}
|
||||
},
|
||||
|
||||
FilePrioIgnore: function(element, ref) {
|
||||
|
286
src/webui/www/private/scripts/rename-files.js
Normal file
286
src/webui/www/private/scripts/rename-files.js
Normal file
@ -0,0 +1,286 @@
|
||||
'use strict';
|
||||
|
||||
if (window.qBittorrent === undefined) {
|
||||
window.qBittorrent = {};
|
||||
}
|
||||
|
||||
window.qBittorrent.MultiRename = (function() {
|
||||
const exports = function() {
|
||||
return {
|
||||
AppliesTo: AppliesTo,
|
||||
RenameFiles: RenameFiles
|
||||
};
|
||||
};
|
||||
|
||||
const AppliesTo = {
|
||||
"FilenameExtension": "FilenameExtension",
|
||||
"Filename": "Filename",
|
||||
"Extension": "Extension"
|
||||
};
|
||||
|
||||
const RenameFiles = new Class({
|
||||
hash: '',
|
||||
selectedFiles: [],
|
||||
matchedFiles: [],
|
||||
|
||||
// Search Options
|
||||
_inner_search: "",
|
||||
setSearch(val) {
|
||||
this._inner_search = val;
|
||||
this._inner_update();
|
||||
this.onChanged(this.matchedFiles);
|
||||
},
|
||||
useRegex: false,
|
||||
matchAllOccurences: false,
|
||||
caseSensitive: false,
|
||||
|
||||
// Replacement Options
|
||||
_inner_replacement: "",
|
||||
setReplacement(val) {
|
||||
this._inner_replacement = val;
|
||||
this._inner_update();
|
||||
this.onChanged(this.matchedFiles);
|
||||
},
|
||||
appliesTo: AppliesTo.FilenameExtension,
|
||||
includeFiles: true,
|
||||
includeFolders: false,
|
||||
replaceAll: false,
|
||||
fileEnumerationStart: 0,
|
||||
|
||||
onChanged: function(rows) {},
|
||||
onInvalidRegex: function(err) {},
|
||||
onRenamed: function(rows) {},
|
||||
onRenameError: function(err) {},
|
||||
|
||||
_inner_update: function() {
|
||||
const findMatches = (regex, str) => {
|
||||
let result;
|
||||
let count = 0;
|
||||
let lastIndex = 0;
|
||||
regex.lastIndex = 0;
|
||||
let matches = [];
|
||||
do {
|
||||
result = regex.exec(str);
|
||||
|
||||
if (result == null) { break; }
|
||||
matches.push(result);
|
||||
|
||||
// regex assertions don't modify lastIndex,
|
||||
// so we need to explicitly break out to prevent infinite loop
|
||||
if (lastIndex == regex.lastIndex) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
// Maximum of 250 matches per file
|
||||
++count;
|
||||
} while (regex.global && count < 250);
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
||||
const replaceBetween = (input, start, end, replacement) => {
|
||||
return input.substring(0, start) + replacement + input.substring(end);
|
||||
};
|
||||
const replaceGroup = (input, search, replacement, escape, stripEscape = true) => {
|
||||
let result = '';
|
||||
let i = 0;
|
||||
while (i < input.length) {
|
||||
// Check if the current index contains the escape string
|
||||
if (input.substring(i, i + escape.length) === escape) {
|
||||
// Don't replace escape chars when they don't precede the current search being performed
|
||||
if (input.substring(i + escape.length, i + escape.length + search.length) !== search) {
|
||||
result += input[i];
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
// Replace escape chars when they precede the current search being performed, unless explicitly told not to
|
||||
if (stripEscape) {
|
||||
result += input.substring(i + escape.length, i + escape.length + search.length);
|
||||
i += escape.length + search.length;
|
||||
}
|
||||
else {
|
||||
result += input.substring(i, i + escape.length + search.length);
|
||||
i += escape.length + search.length;
|
||||
}
|
||||
// Check if the current index contains the search string
|
||||
}
|
||||
else if (input.substring(i, i + search.length) === search) {
|
||||
result += replacement;
|
||||
i += search.length;
|
||||
// Append characters that didn't meet the previous critera
|
||||
}
|
||||
else {
|
||||
result += input[i];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
this.matchedFiles = [];
|
||||
|
||||
// Ignore empty searches
|
||||
if (!this._inner_search) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup regex flags
|
||||
let regexFlags = "";
|
||||
if (this.matchAllOccurences) { regexFlags += "g"; }
|
||||
if (!this.caseSensitive) { regexFlags += "i"; }
|
||||
|
||||
// Setup regex search
|
||||
const regexEscapeExp = new RegExp(/[/\-\\^$*+?.()|[\]{}]/g);
|
||||
const standardSearch = new RegExp(this._inner_search.replace(regexEscapeExp, '\\$&'), regexFlags);
|
||||
let regexSearch;
|
||||
try {
|
||||
regexSearch = new RegExp(this._inner_search, regexFlags);
|
||||
}
|
||||
catch (err) {
|
||||
if (this.useRegex) {
|
||||
this.onInvalidRegex(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const search = this.useRegex ? regexSearch : standardSearch;
|
||||
|
||||
let fileEnumeration = this.fileEnumerationStart;
|
||||
for (let i = 0; i < this.selectedFiles.length; ++i) {
|
||||
const row = this.selectedFiles[i];
|
||||
|
||||
// Ignore files
|
||||
if (!row.isFolder && !this.includeFiles) {
|
||||
continue;
|
||||
}
|
||||
// Ignore folders
|
||||
else if (row.isFolder && !this.includeFolders) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get file extension and reappend the "." (only when the file has an extension)
|
||||
let fileExtension = window.qBittorrent.Filesystem.fileExtension(row.original);
|
||||
if (fileExtension) { fileExtension = "." + fileExtension; }
|
||||
|
||||
const fileNameWithoutExt = row.original.slice(0, row.original.lastIndexOf(fileExtension));
|
||||
|
||||
let matches = [];
|
||||
let offset = 0;
|
||||
switch (this.appliesTo) {
|
||||
case "FilenameExtension":
|
||||
matches = findMatches(search, `${fileNameWithoutExt}${fileExtension}`);
|
||||
break;
|
||||
case "Filename":
|
||||
matches = findMatches(search, `${fileNameWithoutExt}`);
|
||||
break;
|
||||
case "Extension":
|
||||
// Adjust the offset to ensure we perform the replacement at the extension location
|
||||
offset = fileNameWithoutExt.length;
|
||||
matches = findMatches(search, `${fileExtension}`);
|
||||
break;
|
||||
}
|
||||
// Ignore rows without a match
|
||||
if (!matches || matches.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let renamed = row.original;
|
||||
for (let i = matches.length - 1; i >= 0; --i) {
|
||||
const match = matches[i];
|
||||
let replacement = this._inner_replacement;
|
||||
// Replace numerical groups
|
||||
for (let g = 0; g < match.length; ++g) {
|
||||
let group = match[g];
|
||||
if (!group) { continue; }
|
||||
replacement = replaceGroup(replacement, `$${g}`, group, '\\', false);
|
||||
}
|
||||
// Replace named groups
|
||||
for (let namedGroup in match.groups) {
|
||||
replacement = replaceGroup(replacement, `$${namedGroup}`, match.groups[namedGroup], '\\', false);
|
||||
}
|
||||
// Replace auxillary variables
|
||||
for (let v = 'dddddddd'; v !== ''; v = v.substring(1)) {
|
||||
let fileCount = fileEnumeration.toString().padStart(v.length, '0');
|
||||
replacement = replaceGroup(replacement, `$${v}`, fileCount, '\\', false);
|
||||
}
|
||||
// Remove empty $ variable
|
||||
replacement = replaceGroup(replacement, '$', '', '\\');
|
||||
const wholeMatch = match[0];
|
||||
const index = match['index'];
|
||||
renamed = replaceBetween(renamed, index + offset, index + offset + wholeMatch.length, replacement);
|
||||
}
|
||||
|
||||
row.renamed = renamed;
|
||||
++fileEnumeration;
|
||||
this.matchedFiles.push(row);
|
||||
}
|
||||
},
|
||||
|
||||
rename: async function() {
|
||||
if (!this.matchedFiles || this.matchedFiles.length === 0 || !this.hash) {
|
||||
this.onRenamed([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let replaced = [];
|
||||
const _inner_rename = async function(i) {
|
||||
const match = this.matchedFiles[i];
|
||||
const newName = match.renamed;
|
||||
if (newName === match.original) {
|
||||
// Original file name is identical to Renamed
|
||||
return;
|
||||
}
|
||||
|
||||
const isFolder = match.isFolder;
|
||||
const parentPath = window.qBittorrent.Filesystem.folderName(match.path);
|
||||
const oldPath = parentPath
|
||||
? parentPath + window.qBittorrent.Filesystem.PathSeparator + match.original
|
||||
: match.original;
|
||||
const newPath = parentPath
|
||||
? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName
|
||||
: newName;
|
||||
let renameRequest = new Request({
|
||||
url: isFolder ? 'api/v2/torrents/renameFolder' : 'api/v2/torrents/renameFile',
|
||||
method: 'post',
|
||||
data: {
|
||||
hash: this.hash,
|
||||
oldPath: oldPath,
|
||||
newPath: newPath
|
||||
}
|
||||
});
|
||||
try {
|
||||
await renameRequest.send();
|
||||
replaced.push(match);
|
||||
}
|
||||
catch (err) {
|
||||
this.onRenameError(err, match);
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
const replacements = this.matchedFiles.length;
|
||||
if (this.replaceAll) {
|
||||
// matchedFiles are in DFS order so we rename in reverse
|
||||
// in order to prevent unwanted folder creation
|
||||
for (let i = replacements - 1; i >= 0; --i) {
|
||||
await _inner_rename(i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// single replacements go linearly top-down because the
|
||||
// file tree gets recreated after every rename
|
||||
await _inner_rename(0);
|
||||
}
|
||||
this.onRenamed(replaced);
|
||||
},
|
||||
update: function() {
|
||||
this._inner_update();
|
||||
this.onChanged(this.matchedFiles);
|
||||
}
|
||||
});
|
||||
|
||||
return exports();
|
||||
})();
|
||||
|
||||
Object.freeze(window.qBittorrent.MultiRename);
|
@ -55,6 +55,9 @@
|
||||
rename: function(element, ref) {
|
||||
renameFN();
|
||||
},
|
||||
renameFiles: function(element, ref) {
|
||||
renameFilesFN();
|
||||
},
|
||||
queueTop: function(element, ref) {
|
||||
setQueuePositionFN('topPrio');
|
||||
},
|
||||
@ -106,7 +109,7 @@
|
||||
offsets: {
|
||||
x: -15,
|
||||
y: 2
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
torrentsTable.setup('torrentsTableDiv', 'torrentsTableFixedHeaderDiv', contextMenu);
|
||||
|
@ -372,6 +372,7 @@
|
||||
<file>private/rename.html</file>
|
||||
<file>private/rename_feed.html</file>
|
||||
<file>private/rename_file.html</file>
|
||||
<file>private/rename_files.html</file>
|
||||
<file>private/rename_rule.html</file>
|
||||
<file>private/scripts/client.js</file>
|
||||
<file>private/scripts/contextmenu.js</file>
|
||||
@ -393,6 +394,7 @@
|
||||
<file>private/scripts/prop-peers.js</file>
|
||||
<file>private/scripts/prop-trackers.js</file>
|
||||
<file>private/scripts/prop-webseeds.js</file>
|
||||
<file>private/scripts/rename-files.js</file>
|
||||
<file>private/scripts/speedslider.js</file>
|
||||
<file>private/scripts/lib/vanillaSelectBox.js</file>
|
||||
<file>private/setlocation.html</file>
|
||||
|
Loading…
Reference in New Issue
Block a user