Skip to content

Commit

Permalink
WebUI: Allow to move state icon to name column in torrents table
Browse files Browse the repository at this point in the history
  • Loading branch information
skomerko committed Jan 18, 2025
1 parent 1ee8403 commit 08272b6
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 73 deletions.
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) {
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];
},

getRowCells: (tr) => {
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) => {
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

0 comments on commit 08272b6

Please sign in to comment.