Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebUI: Allow to move state icon to name column in torrents table #22118

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions src/webui/www/private/css/dynamicTable.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
}

#transferList .dynamicTable td {
padding: 3px 2px;
padding: 2px;
}

.dynamicTableDiv table.dynamicTable tbody tr.selected {
Expand All @@ -22,10 +22,61 @@
color: var(--color-text-white);
}

#transferList img.stateIcon {
height: 1.3em;
margin-bottom: -1px;
vertical-align: middle;
#transferList .stateIcon {
background: left center / contain no-repeat;
margin-left: 3px;
padding-left: 1.65em;

&.stateIconColumn {
height: 14px;
margin: auto;
padding-left: 0;
width: 14px;
}

&.stateDownloading {
background-image: url("../images/downloading.svg");
}

&.stateUploading {
background-image: url("../images/upload.svg");
}

&.stateStalledUP {
background-image: url("../images/stalledUP.svg");
}

&.stateStalledDL {
background-image: url("../images/stalledDL.svg");
}

&.stateStoppedDL {
background-image: url("../images/stopped.svg");
}

&.stateStoppedUP {
background-image: url("../images/checked-completed.svg");
}

&.stateQueued {
background-image: url("../images/queued.svg");
}

&.stateChecking {
background-image: url("../images/force-recheck.svg");
}

&.stateMoving {
background-image: url("../images/set-location.svg");
}

&.stateError {
background-image: url("../images/error.svg");
}

&.stateUnknown {
background-image: none;
}
}

#transferList #transferList_pad {
Expand Down
158 changes: 90 additions & 68 deletions src/webui/www/private/scripts/dynamicTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,8 @@ window.qBittorrent.DynamicTable ??= (() => {
let width = this.startWidth + (event.event.pageX - this.dragStartX);
if (width < 16)
width = 16;
this.columns[this.resizeTh.columnName].width = width;
this.updateColumn(this.resizeTh.columnName);

this._setColumnWidth(this.resizeTh.columnName, width);
}
}.bind(this);

Expand Down Expand Up @@ -371,6 +371,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.columns[columnName].visible = show ? "1" : "0";
LocalPreferences.set(`column_${columnName}_visible_${this.dynamicTableDivId}`, show ? "1" : "0");
this.updateColumn(columnName);
this.columns[columnName].onVisibilityChange?.(columnName);
},

_calculateColumnBodyWidth: function(column) {
Expand All @@ -397,6 +398,18 @@ window.qBittorrent.DynamicTable ??= (() => {
return longestTd.width + 10;
},

_setColumnWidth: function(columnName, width) {
const column = this.columns[columnName];
column.width = width;

const pos = this.getColumnPos(column.name);
const style = `width: ${column.width}px; ${column.style}`;
this.getRowCells(this.hiddenTableHeader)[pos].style.cssText = style;
this.getRowCells(this.fixedTableHeader)[pos].style.cssText = style;

column.onResize?.(column.name);
},

autoResizeColumn: function(columnName) {
const column = this.columns[columnName];

Expand All @@ -418,8 +431,7 @@ window.qBittorrent.DynamicTable ??= (() => {
width = Math.max(headTextWidth, bodyTextWidth);
}

column.width = width;
this.updateColumn(column.name);
this._setColumnWidth(column.name, width);
this.saveColumnWidth(column.name);
},

Expand Down Expand Up @@ -545,7 +557,11 @@ window.qBittorrent.DynamicTable ??= (() => {
td.textContent = value;
td.title = value;
};
column["isVisible"] = function() {
return (this.visible === "1") && !this.force_hide;
};
column["onResize"] = null;
column["onVisibilityChange"] = null;
column["staticWidth"] = null;
column["calculateBuffer"] = () => 0;
this.columns.push(column);
Expand Down Expand Up @@ -612,31 +628,21 @@ window.qBittorrent.DynamicTable ??= (() => {
return -1;
},

updateColumn: function(columnName) {
updateColumn: function(columnName, updateCellData = false) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chocobo1, changes made around this method are not strictly necessery - name column could be updated from within onVisibilityChange callback but I thought it's better to improve updateColumn a bit instead (though things are definitely not perfect but I didn't want to change too much).

const column = this.columns[columnName];
const pos = this.getColumnPos(columnName);
const visible = ((this.columns[pos].visible !== "0") && !this.columns[pos].force_hide);
const ths = this.hiddenTableHeader.getElements("th");
const fths = this.fixedTableHeader.getElements("th");
const trs = this.tableBody.getElements("tr");
const style = `width: ${this.columns[pos].width}px; ${this.columns[pos].style}`;
const ths = this.getRowCells(this.hiddenTableHeader);
const fths = this.getRowCells(this.fixedTableHeader);
const action = column.isVisible() ? "remove" : "add";
ths[pos].classList[action]("invisible");
fths[pos].classList[action]("invisible");

ths[pos].style.cssText = style;
fths[pos].style.cssText = style;

if (visible) {
ths[pos].classList.remove("invisible");
fths[pos].classList.remove("invisible");
for (let i = 0; i < trs.length; ++i)
trs[i].getElements("td")[pos].classList.remove("invisible");
}
else {
ths[pos].classList.add("invisible");
fths[pos].classList.add("invisible");
for (let j = 0; j < trs.length; ++j)
trs[j].getElements("td")[pos].classList.add("invisible");
for (const tr of this.getTrs()) {
const td = this.getRowCells(tr)[pos];
td.classList[action]("invisible");
if (updateCellData)
column.updateTd(td, this.rows.get(tr.rowId));
}
if (this.columns[pos].onResize !== null)
this.columns[pos].onResize(columnName);
},

getSortedColumn: function() {
Expand Down Expand Up @@ -789,6 +795,14 @@ window.qBittorrent.DynamicTable ??= (() => {
}
},

getTrs: function() {
return [...this.tableBody.rows];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

Suggested change
return [...this.tableBody.rows];
return this.tableBody.rows;

It should be enough if you only need iteration.

Copy link
Contributor Author

@skomerko skomerko Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both rows & cells properties return a live HTMLCollection which is automatically updated when the underlying document is changed, so I convert them to a static one to avoid it. IIRC getElements called with a simple tag does the same thing - it uses getElementsByTagName internally and then creates Elements collection (live -> static).

Maybe it would be better to return this.tableBody.querySelectorAll("tr") instead? I can't tell if there is a difference in performance between these two approaches.

},

getRowCells: (tr) => {
return [...tr.cells];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too:

Suggested change
return [...tr.cells];
return tr.cells;

},

getRow: function(rowId) {
return this.rows.get(rowId);
},
Expand Down Expand Up @@ -895,9 +909,9 @@ window.qBittorrent.DynamicTable ??= (() => {
const row = this.rows.get(tr.rowId);
const data = row[fullUpdate ? "full_data" : "data"];

const tds = tr.getElements("td");
const tds = this.getRowCells(tr);
for (let i = 0; i < this.columns.length; ++i) {
if (Object.hasOwn(data, this.columns[i].dataProperties[0]))
if (this.columns[i].dataProperties.some(prop => Object.hasOwn(data, prop)))
this.columns[i].updateTd(tds[i], row);
}
row["data"] = {};
Expand Down Expand Up @@ -988,7 +1002,7 @@ window.qBittorrent.DynamicTable ??= (() => {

initColumns: function() {
this.newColumn("priority", "", "#", 30, true);
this.newColumn("state_icon", "cursor: default", "", 22, true);
this.newColumn("state_icon", "", "QBT_TR(State Icon)QBT_TR[CONTEXT=TransferListModel]", 30, false);
this.newColumn("name", "", "QBT_TR(Name)QBT_TR[CONTEXT=TransferListModel]", 200, true);
this.newColumn("size", "", "QBT_TR(Size)QBT_TR[CONTEXT=TransferListModel]", 100, true);
this.newColumn("total_size", "", "QBT_TR(Total Size)QBT_TR[CONTEXT=TransferListModel]", 100, false);
Expand Down Expand Up @@ -1026,9 +1040,8 @@ window.qBittorrent.DynamicTable ??= (() => {
this.newColumn("reannounce", "", "QBT_TR(Reannounce In)QBT_TR[CONTEXT=TransferListModel]", 100, false);
this.newColumn("private", "", "QBT_TR(Private)QBT_TR[CONTEXT=TransferListModel]", 100, false);

this.columns["state_icon"].onclick = "";
this.columns["state_icon"].dataProperties[0] = "state";

this.columns["name"].dataProperties.push("state");
this.columns["num_seeds"].dataProperties.push("num_complete");
this.columns["num_leechs"].dataProperties.push("num_incomplete");
this.columns["time_active"].dataProperties.push("seeding_time");
Expand All @@ -1037,83 +1050,92 @@ window.qBittorrent.DynamicTable ??= (() => {
},

initColumnsFunctions: function() {

// state_icon
this.columns["state_icon"].updateTd = function(td, row) {
let state = this.getRowValue(row);
let img_path;
const getStateIconClasses = (state) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be singular?

Suggested change
const getStateIconClasses = (state) => {
const getStateIconClass = (state) => {

let stateClass = "stateUnknown";
// normalize states
switch (state) {
case "forcedDL":
case "metaDL":
case "forcedMetaDL":
case "downloading":
state = "downloading";
img_path = "images/downloading.svg";
stateClass = "stateDownloading";
break;
case "forcedUP":
case "uploading":
state = "uploading";
img_path = "images/upload.svg";
stateClass = "stateUploading";
break;
case "stalledUP":
state = "stalledUP";
img_path = "images/stalledUP.svg";
stateClass = "stateStalledUP";
break;
case "stalledDL":
state = "stalledDL";
img_path = "images/stalledDL.svg";
stateClass = "stateStalledDL";
break;
case "stoppedDL":
state = "torrent-stop";
img_path = "images/stopped.svg";
stateClass = "stateStoppedDL";
break;
case "stoppedUP":
state = "checked-completed";
img_path = "images/checked-completed.svg";
stateClass = "stateStoppedUP";
break;
case "queuedDL":
case "queuedUP":
state = "queued";
img_path = "images/queued.svg";
stateClass = "stateQueued";
break;
case "checkingDL":
case "checkingUP":
case "queuedForChecking":
case "checkingResumeData":
state = "force-recheck";
img_path = "images/force-recheck.svg";
stateClass = "stateChecking";
break;
case "moving":
state = "moving";
img_path = "images/set-location.svg";
stateClass = "stateMoving";
break;
case "error":
case "unknown":
case "missingFiles":
state = "error";
img_path = "images/error.svg";
stateClass = "stateError";
break;
default:
break; // do nothing
}

if (td.getChildren("img").length > 0) {
const img = td.getChildren("img")[0];
if (!img.src.includes(img_path)) {
img.src = img_path;
img.title = state;
}
return `stateIcon ${stateClass}`;
};

// state_icon
this.columns["state_icon"].updateTd = function(td, row) {
const state = this.getRowValue(row);
let div = td.firstElementChild;
if (div === null) {
div = document.createElement("div");
td.append(div);
}
else {
const img = document.createElement("img");
img.src = img_path;
img.className = "stateIcon";
img.title = state;
td.append(img);

div.className = `${getStateIconClasses(state)} stateIconColumn`;
};

this.columns["state_icon"].onVisibilityChange = (columnName) => {
// show state icon in name column only when standalone
// state icon column is hidden
this.updateColumn("name", true);
};

// name
this.columns["name"].updateTd = function(td, row) {
const name = this.getRowValue(row, 0);
const state = this.getRowValue(row, 1);
let span = td.firstElementChild;
if (span === null) {
span = document.createElement("span");
td.append(span);
}

span.className = this.isStateIconShown() ? `${getStateIconClasses(state)}` : "";
span.textContent = name;
td.title = name;
};

this.columns["name"].isStateIconShown = () => !this.columns["state_icon"].isVisible();

// status
this.columns["status"].updateTd = function(td, row) {
const state = this.getRowValue(row);
Expand Down
Loading