mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-20 15:37:26 -06:00
@@ -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);
|
||||
Reference in New Issue
Block a user