Compare commits

...

6 Commits

Author SHA1 Message Date
Chocobo1
f6ee6b92a4 Revise label wordings
Such that action subject is truly unambiguous to the user.

PR #22894.
2025-06-22 16:01:00 +08:00
Chocobo1
fe1679d778 Provide testing cases for path concatenation
PR #22893.
2025-06-22 15:40:14 +08:00
Thomas Piccirello
67ef356064 WebUI: Delete correct rows after re-sort
The previous logic assumed that trs was properly sorted, which is no longer the case.
Follow up to #22827.

PR #22884.
2025-06-22 15:35:07 +08:00
Thomas Piccirello
254f39f89d WebUI: Restore node default collapse state
By default, nodes should be expanded until explicitly collapsed. This restores the default behavior which changed in b4a16f6464.

Relevant: https://github.com/qbittorrent/qBittorrent/pull/21645#discussion_r2150695297

PR #22879.
2025-06-22 14:55:37 +08:00
tehcneko
d702a02c1f WebUI: Avoid forced reflow on virtual list rerender
Avoid forced synchronous layout caused by offsetHeight/scrollTop access.

PR #22858.
2025-06-22 14:27:16 +08:00
xavier2k6
86e11d344f GHA CI: Bump pandoc to latest
* Bump `pandoc` to latest (3.7.0.2)
* Apply upstream suggestions

PR #22708.
2025-06-22 14:21:50 +08:00
8 changed files with 84 additions and 40 deletions

View File

@@ -36,7 +36,7 @@ jobs:
curl \
-L \
-o "${{ runner.temp }}/pandoc.tar.gz" \
"https://github.com/jgm/pandoc/releases/download/3.6/pandoc-3.6-linux-amd64.tar.gz"
"https://github.com/jgm/pandoc/releases/download/3.7.0.2/pandoc-3.7.0.2-linux-amd64.tar.gz"
tar -xf "${{ runner.temp }}/pandoc.tar.gz" -C "${{ github.workspace }}/.."
mv "${{ github.workspace }}/.."/pandoc-* "${{ env.pandoc_path }}"
# run pandoc

View File

@@ -1,4 +1,4 @@
.\" Automatically generated by Pandoc 3.4
.\" Automatically generated by Pandoc 3.7.0.2
.\"
.TH "QBITTORRENT\-NOX" "1" "January 16th 2010" "Command line Bittorrent client written in C++ / Qt"
.SH NAME
@@ -26,7 +26,7 @@ compatible).
qBittorrent\-nox is meant to be controlled via its feature\-rich Web UI
which is accessible as a default on http://localhost:8080.
The Web UI access is secured and the default account user name is
\[lq]admin\[rq] with \[lq]adminadmin\[rq] as a password.
\(lqadmin\(rq with \(lqadminadmin\(rq as a password.
.SH OPTIONS
\f[B]\f[CB]\-\-help\f[B]\f[R] Prints the command line options.
.PP

View File

@@ -1,4 +1,4 @@
.\" Automatically generated by Pandoc 3.4
.\" Automatically generated by Pandoc 3.7.0.2
.\"
.TH "QBITTORRENT" "1" "January 16th 2010" "Bittorrent client written in C++ / Qt"
.SH NAME

View File

@@ -1,8 +1,8 @@
.\" Automatically generated by Pandoc 3.4
.\" Automatically generated by Pandoc 3.7.0.2
.\"
.TH "QBITTORRENT\-NOX" "1" "16 января 2010" "Клиент сети БитТоррент для командной строки"
.SH НАЗВАНИЕ
qBittorrent\-nox \[em] клиент сети БитТоррент для командной строки.
qBittorrent\-nox \(em клиент сети БитТоррент для командной строки.
.SH АВТОРЫ
Christophe Dumez \c
.MT chris@qbittorrent.org

View File

@@ -1,8 +1,8 @@
.\" Automatically generated by Pandoc 3.4
.\" Automatically generated by Pandoc 3.7.0.2
.\"
.TH "QBITTORRENT" "1" "16 января 2010" "Клиент сети БитТоррент"
.SH НАЗВАНИЕ
qBittorrent \[em] клиент сети БитТоррент.
qBittorrent \(em клиент сети БитТоррент.
.SH АВТОРЫ
Christophe Dumez \c
.MT chris@qbittorrent.org

View File

@@ -827,11 +827,11 @@ void SearchWidget::showTabMenu(const int index)
if (auto *searchJobWidget = static_cast<SearchJobWidget *>(m_ui->tabWidget->widget(index));
searchJobWidget->status() != SearchJobWidget::Status::Ongoing)
{
menu->addAction(tr("Refresh"), this, [this, searchJobWidget] { refreshTab(searchJobWidget); });
menu->addAction(tr("Refresh tab"), this, [this, searchJobWidget] { refreshTab(searchJobWidget); });
}
else
{
menu->addAction(tr("Stop"), this, [searchJobWidget] { searchJobWidget->cancelSearch(); });
menu->addAction(tr("Stop search"), this, [searchJobWidget] { searchJobWidget->cancelSearch(); });
}
menu->addSeparator();

View File

@@ -99,6 +99,7 @@ window.qBittorrent.DynamicTable ??= (() => {
return;
this.table.style.position = "relative";
this.renderedOffset = this.dynamicTableDiv.scrollTop;
this.renderedHeight = this.dynamicTableDiv.offsetHeight;
const resizeCallback = window.qBittorrent.Misc.createDebounceHandler(100, () => {
const height = this.dynamicTableDiv.offsetHeight;
@@ -117,8 +118,10 @@ window.qBittorrent.DynamicTable ??= (() => {
this.dynamicTableDiv.addEventListener("scroll", (e) => {
tableElement.style.left = `${-this.dynamicTableDiv.scrollLeft}px`;
// rerender on scroll
if (this.useVirtualList)
if (this.useVirtualList) {
this.renderedOffset = this.dynamicTableDiv.scrollTop;
this.rerender();
}
});
this.dynamicTableDiv.addEventListener("click", (e) => {
@@ -883,6 +886,7 @@ window.qBittorrent.DynamicTable ??= (() => {
for (let rowPos = 0; rowPos < rows.length; ++rowPos) {
const { rowId } = rows[rowPos];
const tr = trMap.get(rowId);
trMap.delete(rowId);
const isInCorrectLocation = rowId === trs[rowPos]?.rowId;
if (!isInCorrectLocation) {
@@ -901,26 +905,27 @@ window.qBittorrent.DynamicTable ??= (() => {
prevTr = tr;
}
const rowPos = rows.length;
while ((rowPos < trs.length) && (trs.length > 0))
trs.pop().remove();
for (const tr of trMap.values())
tr.remove();
}
}
rerender(rows = this.getFilteredAndSortedRows()) {
// set the scrollable height
this.table.style.height = `${rows.length * this.rowHeight}px`;
const tableHeight = rows.length * this.rowHeight;
if (tableHeight !== this.previousTableHeight) {
this.previousTableHeight = tableHeight;
this.table.style.height = `${tableHeight}px`;
}
if (this.dynamicTableDiv.offsetHeight === 0)
if (this.renderedHeight === 0)
return;
this.renderedHeight = this.dynamicTableDiv.offsetHeight;
// show extra rows at top/bottom to reduce flickering
const extraRowCount = 20;
// how many rows can be shown in the visible area
const visibleRowCount = Math.ceil(this.renderedHeight / this.rowHeight) + (extraRowCount * 2);
// start position of visible rows, offsetted by scrollTop
let startRow = Math.max((Math.trunc(this.dynamicTableDiv.scrollTop / this.rowHeight) - extraRowCount), 0);
// start position of visible rows, offsetted by renderedOffset
let startRow = Math.max((Math.trunc(this.renderedOffset / this.rowHeight) - extraRowCount), 0);
// ensure startRow is even
if ((startRow % 2) === 1)
startRow = Math.max(0, startRow - 1);
@@ -949,24 +954,6 @@ window.qBittorrent.DynamicTable ??= (() => {
// update visible rows
for (const row of this.tableBody.children)
this.updateRow(row, true);
// refresh row height based on first row
const tr = this.tableBody.firstChild;
if (tr !== null) {
const updateRowHeight = () => {
if (tr.offsetHeight === 0)
return;
if (this.rowHeight !== tr.offsetHeight) {
this.rowHeight = tr.offsetHeight;
// rerender on row height change
this.rerender();
}
};
if (tr.offsetHeight === 0)
setTimeout(updateRowHeight);
else
updateRowHeight();
}
}
createRowElement(row, top = -1) {
@@ -998,6 +985,7 @@ window.qBittorrent.DynamicTable ??= (() => {
if (this.useVirtualList) {
tr.style.position = "absolute";
tr.style.top = `${top}px`;
tr.style.height = `${this.rowHeight}px`;
}
}
@@ -2599,8 +2587,10 @@ window.qBittorrent.DynamicTable ??= (() => {
this.dynamicTableDiv.addEventListener("scroll", (e) => {
headerDiv.scrollLeft = this.dynamicTableDiv.scrollLeft;
// rerender on scroll
if (this.useVirtualList)
if (this.useVirtualList) {
this.renderedOffset = this.dynamicTableDiv.scrollTop;
this.rerender();
}
});
}
}
@@ -2733,7 +2723,7 @@ window.qBittorrent.DynamicTable ??= (() => {
if (node.isFolder) {
if (!this.collapseState.has(node.rowId))
this.collapseState.set(node.rowId, { depth: depth, collapsed: depth > 0 });
this.collapseState.set(node.rowId, { depth: depth, collapsed: false });
const data = {
rowId: node.rowId,
size: node.size,

View File

@@ -287,6 +287,60 @@ private slots:
#endif
}
// Path &operator/=(const Path &other);
void testOperatorPathAppendAssign() const
{
QCOMPARE((Path() /= Path()), Path());
QCOMPARE((Path(u"a"_s) /= Path()), Path(u"a"_s));
QCOMPARE((Path() /= Path(u"b"_s)), Path(u"b"_s));
QCOMPARE((Path(u"a"_s) /= Path(u"b"_s)), Path(u"a/b"_s));
#ifdef Q_OS_WIN
QCOMPARE((Path(u"c:/"_s) /= Path(u"/"_s)), Path(u"c:/"_s));
QCOMPARE((Path(u"c:/"_s) /= Path(u"a/"_s)), Path(u"c:/a"_s));
#else
QCOMPARE((Path(u"/"_s) /= Path(u"/"_s)), Path(u"/"_s));
QCOMPARE((Path(u"/"_s) /= Path(u"a/"_s)), Path(u"/a"_s));
#endif
}
// Path &operator+=(QStringView str);
void testOperatorAppendAssign() const
{
QCOMPARE((Path() += QString()), Path());
QCOMPARE((Path(u"a"_s) += QString()), Path(u"a"_s));
QCOMPARE((Path() += u"b"), Path(u"b"_s));
QCOMPARE((Path(u"a"_s) += u"b"), Path(u"ab"_s));
QCOMPARE((Path(u"a"_s) += u"/b/"), Path(u"a/b"_s));
}
// Path operator/(const Path &lhs, const Path &rhs)
void testOperatorPathConcat() const
{
QCOMPARE((Path() / Path()), Path());
QCOMPARE((Path(u"a"_s) / Path()), Path(u"a"_s));
QCOMPARE((Path() / Path(u"b"_s)), Path(u"b"_s));
QCOMPARE((Path(u"a"_s) / Path(u"b"_s)), Path(u"a/b"_s));
#ifdef Q_OS_WIN
QCOMPARE((Path(u"c:/"_s) / Path(u"/"_s)), Path(u"c:/"_s));
QCOMPARE((Path(u"c:/"_s) / Path(u"a/"_s)), Path(u"c:/a"_s));
#else
QCOMPARE((Path(u"/"_s) / Path(u"/"_s)), Path(u"/"_s));
QCOMPARE((Path(u"/"_s) / Path(u"a/"_s)), Path(u"/a"_s));
#endif
}
// Path operator+(const Path &lhs, QStringView rhs);
void testOperatorAppend() const
{
QCOMPARE((Path() + QString()), Path());
QCOMPARE((Path(u"a"_s) + QString()), Path(u"a"_s));
QCOMPARE((Path() + u"b"), Path(u"b"_s));
QCOMPARE((Path(u"a"_s) + u"b"), Path(u"ab"_s));
QCOMPARE((Path(u"a"_s) + u"/b/"), Path(u"a/b"_s));
}
// TODO: add tests for remaining methods
};