diff --git a/Changelog b/Changelog index 33b62e606..8af679433 100644 --- a/Changelog +++ b/Changelog @@ -6,7 +6,7 @@ Mon Nov 27th 2023 - sledgehammer999 - v4.6.2 - WINDOWS: NSIS: Display correct Minimum Windows OS requirement (xavier2k6) - WINDOWS: NSIS: Add Hebrew translation (avivmu) - LINUX: WAYLAND: Fix parent widget of "Lock qBittorrent" submenu (Vlad Zahorodnii) - + Mon Nov 20th 2023 - sledgehammer999 - v4.6.1 - FEATURE: Add option to enable previous Add new torrent dialog behavior (glassez) - BUGFIX: Prevent crash due to race condition when adding magnet link (glassez) diff --git a/src/app/main.cpp b/src/app/main.cpp index 31e744732..eab0156ee 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -94,6 +94,7 @@ void showSplashScreen(); #ifdef Q_OS_UNIX void adjustFileDescriptorLimit(); +void adjustLocale(); #endif // Main @@ -104,6 +105,7 @@ int main(int argc, char *argv[]) #endif #ifdef Q_OS_UNIX + adjustLocale(); adjustFileDescriptorLimit(); #endif @@ -392,4 +394,12 @@ void adjustFileDescriptorLimit() limit.rlim_cur = limit.rlim_max; setrlimit(RLIMIT_NOFILE, &limit); } + +void adjustLocale() +{ + // specify the default locale just in case if user has not set any other locale + // only `C` locale is available universally without installing locale packages + if (qEnvironmentVariableIsEmpty("LANG")) + qputenv("LANG", "C.UTF-8"); +} #endif diff --git a/src/base/bittorrent/addtorrentparams.cpp b/src/base/bittorrent/addtorrentparams.cpp index e78939232..c657038dc 100644 --- a/src/base/bittorrent/addtorrentparams.cpp +++ b/src/base/bittorrent/addtorrentparams.cpp @@ -44,6 +44,7 @@ const QString PARAM_DOWNLOADPATH = u"download_path"_s; const QString PARAM_OPERATINGMODE = u"operating_mode"_s; const QString PARAM_QUEUETOP = u"add_to_top_of_queue"_s; const QString PARAM_STOPPED = u"stopped"_s; +const QString PARAM_STOPCONDITION = u"stop_condition"_s; const QString PARAM_SKIPCHECKING = u"skip_checking"_s; const QString PARAM_CONTENTLAYOUT = u"content_layout"_s; const QString PARAM_AUTOTMM = u"use_auto_tmm"_s; @@ -126,17 +127,18 @@ BitTorrent::AddTorrentParams BitTorrent::parseAddTorrentParams(const QJsonObject params.savePath = Path(jsonObj.value(PARAM_SAVEPATH).toString()); params.useDownloadPath = getOptionalBool(jsonObj, PARAM_USEDOWNLOADPATH); params.downloadPath = Path(jsonObj.value(PARAM_DOWNLOADPATH).toString()); - params.addForced = (getEnum(jsonObj, PARAM_OPERATINGMODE) == BitTorrent::TorrentOperatingMode::Forced); + params.addForced = (getEnum(jsonObj, PARAM_OPERATINGMODE) == TorrentOperatingMode::Forced); params.addToQueueTop = getOptionalBool(jsonObj, PARAM_QUEUETOP); params.addPaused = getOptionalBool(jsonObj, PARAM_STOPPED); + params.stopCondition = getOptionalEnum(jsonObj, PARAM_STOPCONDITION); params.skipChecking = jsonObj.value(PARAM_SKIPCHECKING).toBool(); - params.contentLayout = getOptionalEnum(jsonObj, PARAM_CONTENTLAYOUT); + params.contentLayout = getOptionalEnum(jsonObj, PARAM_CONTENTLAYOUT); params.useAutoTMM = getOptionalBool(jsonObj, PARAM_AUTOTMM); params.uploadLimit = jsonObj.value(PARAM_UPLOADLIMIT).toInt(-1); params.downloadLimit = jsonObj.value(PARAM_DOWNLOADLIMIT).toInt(-1); - params.seedingTimeLimit = jsonObj.value(PARAM_SEEDINGTIMELIMIT).toInt(BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME); - params.inactiveSeedingTimeLimit = jsonObj.value(PARAM_INACTIVESEEDINGTIMELIMIT).toInt(BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME); - params.ratioLimit = jsonObj.value(PARAM_RATIOLIMIT).toDouble(BitTorrent::Torrent::USE_GLOBAL_RATIO); + params.seedingTimeLimit = jsonObj.value(PARAM_SEEDINGTIMELIMIT).toInt(Torrent::USE_GLOBAL_SEEDING_TIME); + params.inactiveSeedingTimeLimit = jsonObj.value(PARAM_INACTIVESEEDINGTIMELIMIT).toInt(Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME); + params.ratioLimit = jsonObj.value(PARAM_RATIOLIMIT).toDouble(Torrent::USE_GLOBAL_RATIO); return params; } @@ -149,7 +151,7 @@ QJsonObject BitTorrent::serializeAddTorrentParams(const AddTorrentParams ¶ms {PARAM_SAVEPATH, params.savePath.data()}, {PARAM_DOWNLOADPATH, params.downloadPath.data()}, {PARAM_OPERATINGMODE, Utils::String::fromEnum(params.addForced - ? BitTorrent::TorrentOperatingMode::Forced : BitTorrent::TorrentOperatingMode::AutoManaged)}, + ? TorrentOperatingMode::Forced : TorrentOperatingMode::AutoManaged)}, {PARAM_SKIPCHECKING, params.skipChecking}, {PARAM_UPLOADLIMIT, params.uploadLimit}, {PARAM_DOWNLOADLIMIT, params.downloadLimit}, @@ -162,6 +164,8 @@ QJsonObject BitTorrent::serializeAddTorrentParams(const AddTorrentParams ¶ms jsonObj[PARAM_QUEUETOP] = *params.addToQueueTop; if (params.addPaused) jsonObj[PARAM_STOPPED] = *params.addPaused; + if (params.stopCondition) + jsonObj[PARAM_STOPCONDITION] = Utils::String::fromEnum(*params.stopCondition); if (params.contentLayout) jsonObj[PARAM_CONTENTLAYOUT] = Utils::String::fromEnum(*params.contentLayout); if (params.useAutoTMM) diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 725ad7735..d4f96515c 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -2681,6 +2681,7 @@ LoadTorrentParams SessionImpl::initLoadTorrentParams(const AddTorrentParams &add loadTorrentParams.addToQueueTop = addTorrentParams.addToQueueTop.value_or(isAddTorrentToQueueTop()); loadTorrentParams.ratioLimit = addTorrentParams.ratioLimit; loadTorrentParams.seedingTimeLimit = addTorrentParams.seedingTimeLimit; + loadTorrentParams.inactiveSeedingTimeLimit = addTorrentParams.inactiveSeedingTimeLimit; const QString category = addTorrentParams.category; if (!category.isEmpty() && !m_categories.contains(category) && !addCategory(category)) @@ -5831,7 +5832,7 @@ void SessionImpl::handleFileErrorAlert(const lt::file_error_alert *p) const QString msg = QString::fromStdString(p->message()); LogMsg(tr("File error alert. Torrent: \"%1\". File: \"%2\". Reason: \"%3\"") - .arg(torrent->name(), QString::fromLocal8Bit(p->filename()), msg) + .arg(torrent->name(), QString::fromUtf8(p->filename()), msg) , Log::WARNING); emit fullDiskError(torrent, msg); } diff --git a/src/gui/properties/propertieswidget.cpp b/src/gui/properties/propertieswidget.cpp index 6b846e2bb..9c74c9ed4 100644 --- a/src/gui/properties/propertieswidget.cpp +++ b/src/gui/properties/propertieswidget.cpp @@ -291,6 +291,8 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent) // Info hashes m_ui->labelInfohash1Val->setText(m_torrent->infoHash().v1().isValid() ? m_torrent->infoHash().v1().toString() : tr("N/A")); m_ui->labelInfohash2Val->setText(m_torrent->infoHash().v2().isValid() ? m_torrent->infoHash().v2().toString() : tr("N/A")); + // URL seeds + loadUrlSeeds(); if (m_torrent->hasMetadata()) { // Creation date @@ -301,9 +303,6 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent) // Comment m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped())); - // URL seeds - loadUrlSeeds(); - m_ui->labelCreatedByVal->setText(m_torrent->creator()); } // Load dynamic data diff --git a/src/gui/torrentoptionsdialog.cpp b/src/gui/torrentoptionsdialog.cpp index 5d6ae2552..a618453da 100644 --- a/src/gui/torrentoptionsdialog.cpp +++ b/src/gui/torrentoptionsdialog.cpp @@ -285,7 +285,8 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QVector + * Copyright (C) 2016-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -38,6 +38,9 @@ class CategoryModelItem { public: + inline static const QString UID_ALL {QChar(1)}; + inline static const QString UID_UNCATEGORIZED; + CategoryModelItem() = default; CategoryModelItem(CategoryModelItem *parent, const QString &categoryName, const int torrentsCount = 0) @@ -99,9 +102,21 @@ public: int pos() const { - if (!m_parent) return -1; + if (!m_parent) + return -1; - return m_parent->m_childUids.indexOf(m_name); + if (const int posByName = m_parent->m_childUids.indexOf(m_name); posByName >= 0) + return posByName; + + // special cases + if (this == m_parent->m_children[UID_ALL]) + return 0; + + if (this == m_parent->m_children[UID_UNCATEGORIZED]) + return 1; + + Q_ASSERT(false); + return -1; } bool hasChild(const QString &name) const @@ -202,7 +217,8 @@ int CategoryFilterModel::columnCount(const QModelIndex &) const QVariant CategoryFilterModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) return {}; + if (!index.isValid()) + return {}; const auto *item = static_cast(index.internalPointer()); @@ -248,8 +264,8 @@ QModelIndex CategoryFilterModel::index(int row, int column, const QModelIndex &p if (parent.isValid() && (parent.column() != 0)) return {}; - auto *parentItem = parent.isValid() ? static_cast(parent.internalPointer()) - : m_rootItem; + auto *parentItem = parent.isValid() + ? static_cast(parent.internalPointer()) : m_rootItem; if (row < parentItem->childCount()) return createIndex(row, column, parentItem->childAt(row)); @@ -262,7 +278,8 @@ QModelIndex CategoryFilterModel::parent(const QModelIndex &index) const return {}; auto *item = static_cast(index.internalPointer()); - if (!item) return {}; + if (!item) + return {}; return this->index(item->parent()); } @@ -276,7 +293,8 @@ int CategoryFilterModel::rowCount(const QModelIndex &parent) const return m_rootItem->childCount(); auto *item = static_cast(parent.internalPointer()); - if (!item) return 0; + if (!item) + return 0; return item->childCount(); } @@ -288,13 +306,16 @@ QModelIndex CategoryFilterModel::index(const QString &categoryName) const QString CategoryFilterModel::categoryName(const QModelIndex &index) const { - if (!index.isValid()) return {}; + if (!index.isValid()) + return {}; + return static_cast(index.internalPointer())->fullName(); } QModelIndex CategoryFilterModel::index(CategoryModelItem *item) const { - if (!item || !item->parent()) return {}; + if (!item || !item->parent()) + return {}; return index(item->pos(), 0, index(item->parent())); } @@ -337,8 +358,17 @@ void CategoryFilterModel::torrentsLoaded(const QVector &t Q_ASSERT(item); item->increaseTorrentsCount(); + QModelIndex i = index(item); + while (i.isValid()) + { + emit dataChanged(i, i); + i = parent(i); + } + m_rootItem->childAt(0)->increaseTorrentsCount(); } + + emit dataChanged(index(0, 0), index(0, 0)); } void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) @@ -347,18 +377,24 @@ void CategoryFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const tor Q_ASSERT(item); item->decreaseTorrentsCount(); + QModelIndex i = index(item); + while (i.isValid()) + { + emit dataChanged(i, i); + i = parent(i); + } + m_rootItem->childAt(0)->decreaseTorrentsCount(); + emit dataChanged(index(0, 0), index(0, 0)); } void CategoryFilterModel::torrentCategoryChanged(BitTorrent::Torrent *const torrent, const QString &oldCategory) { - QModelIndex i; - auto *item = findItem(oldCategory); Q_ASSERT(item); item->decreaseTorrentsCount(); - i = index(item); + QModelIndex i = index(item); while (i.isValid()) { emit dataChanged(i, i); @@ -392,17 +428,16 @@ void CategoryFilterModel::populate() const auto torrents = session->torrents(); m_isSubcategoriesEnabled = session->isSubcategoriesEnabled(); - const QString UID_ALL; - const QString UID_UNCATEGORIZED(QChar(1)); - // All torrents - m_rootItem->addChild(UID_ALL, new CategoryModelItem(nullptr, tr("All"), torrents.count())); + m_rootItem->addChild(CategoryModelItem::UID_ALL + , new CategoryModelItem(nullptr, tr("All"), torrents.count())); // Uncategorized torrents using Torrent = BitTorrent::Torrent; const int torrentsCount = std::count_if(torrents.begin(), torrents.end() - , [](Torrent *torrent) { return torrent->category().isEmpty(); }); - m_rootItem->addChild(UID_UNCATEGORIZED, new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount)); + , [](Torrent *torrent) { return torrent->category().isEmpty(); }); + m_rootItem->addChild(CategoryModelItem::UID_UNCATEGORIZED + , new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount)); using BitTorrent::Torrent; if (m_isSubcategoriesEnabled) @@ -446,7 +481,9 @@ CategoryModelItem *CategoryFilterModel::findItem(const QString &fullName) const for (const QString &subcat : asConst(BitTorrent::Session::expandCategory(fullName))) { const QString subcatName = shortName(subcat); - if (!item->hasChild(subcatName)) return nullptr; + if (!item->hasChild(subcatName)) + return nullptr; + item = item->child(subcatName); } diff --git a/src/gui/transferlistfilters/categoryfiltermodel.h b/src/gui/transferlistfilters/categoryfiltermodel.h index 9576f094a..8aa3425cf 100644 --- a/src/gui/transferlistfilters/categoryfiltermodel.h +++ b/src/gui/transferlistfilters/categoryfiltermodel.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2016 Vladimir Golovnev + * Copyright (C) 2016-2023 Vladimir Golovnev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License diff --git a/src/gui/transferlistfilters/tagfiltermodel.cpp b/src/gui/transferlistfilters/tagfiltermodel.cpp index 35c74a42c..92b1c294c 100644 --- a/src/gui/transferlistfilters/tagfiltermodel.cpp +++ b/src/gui/transferlistfilters/tagfiltermodel.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2017 Tony Gregerson * * This program is free software; you can redistribute it and/or @@ -36,6 +37,9 @@ #include "base/global.h" #include "gui/uithememanager.h" +const int ROW_ALL = 0; +const int ROW_UNTAGGED = 1; + namespace { QString getSpecialAllTag() @@ -203,21 +207,29 @@ void TagFilterModel::tagRemoved(const QString &tag) void TagFilterModel::torrentTagAdded(BitTorrent::Torrent *const torrent, const QString &tag) { if (torrent->tags().count() == 1) + { untaggedItem()->decreaseTorrentsCount(); + const QModelIndex i = index(ROW_UNTAGGED, 0); + emit dataChanged(i, i); + } const int row = findRow(tag); Q_ASSERT(isValidRow(row)); TagModelItem &item = m_tagItems[row]; item.increaseTorrentsCount(); - const QModelIndex i = index(row, 0, QModelIndex()); + const QModelIndex i = index(row, 0); emit dataChanged(i, i); } void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const QString &tag) { if (torrent->tags().empty()) + { untaggedItem()->increaseTorrentsCount(); + const QModelIndex i = index(ROW_UNTAGGED, 0); + emit dataChanged(i, i); + } const int row = findRow(tag); if (row < 0) @@ -225,7 +237,7 @@ void TagFilterModel::torrentTagRemoved(BitTorrent::Torrent *const torrent, const m_tagItems[row].decreaseTorrentsCount(); - const QModelIndex i = index(row, 0, QModelIndex()); + const QModelIndex i = index(row, 0); emit dataChanged(i, i); } @@ -242,17 +254,39 @@ void TagFilterModel::torrentsLoaded(const QVector &torren for (TagModelItem *item : items) item->increaseTorrentsCount(); } + + emit dataChanged(index(0, 0), index((rowCount() - 1), 0)); } void TagFilterModel::torrentAboutToBeRemoved(BitTorrent::Torrent *const torrent) { allTagsItem()->decreaseTorrentsCount(); - if (torrent->tags().isEmpty()) - untaggedItem()->decreaseTorrentsCount(); + { + const QModelIndex i = index(ROW_ALL, 0); + emit dataChanged(i, i); + } - for (TagModelItem *item : asConst(findItems(torrent->tags()))) - item->decreaseTorrentsCount(); + if (torrent->tags().isEmpty()) + { + untaggedItem()->decreaseTorrentsCount(); + const QModelIndex i = index(ROW_UNTAGGED, 0); + emit dataChanged(i, i); + } + else + { + for (const QString &tag : asConst(torrent->tags())) + { + const int row = findRow(tag); + Q_ASSERT(isValidRow(row)); + if (Q_UNLIKELY(!isValidRow(row))) + continue; + + m_tagItems[row].decreaseTorrentsCount(); + const QModelIndex i = index(row, 0); + emit dataChanged(i, i); + } + } } QString TagFilterModel::tagDisplayName(const QString &tag) @@ -299,11 +333,15 @@ void TagFilterModel::removeFromModel(int row) int TagFilterModel::findRow(const QString &tag) const { + if (!BitTorrent::Session::isValidTag(tag)) + return -1; + for (int i = 0; i < m_tagItems.size(); ++i) { if (m_tagItems[i].tag() == tag) return i; } + return -1; } @@ -333,11 +371,11 @@ QVector TagFilterModel::findItems(const TagSet &tags) TagModelItem *TagFilterModel::allTagsItem() { Q_ASSERT(!m_tagItems.isEmpty()); - return &m_tagItems[0]; + return &m_tagItems[ROW_ALL]; } TagModelItem *TagFilterModel::untaggedItem() { - Q_ASSERT(m_tagItems.size() > 1); - return &m_tagItems[1]; + Q_ASSERT(m_tagItems.size() > ROW_UNTAGGED); + return &m_tagItems[ROW_UNTAGGED]; } diff --git a/src/gui/transferlistfilters/tagfiltermodel.h b/src/gui/transferlistfilters/tagfiltermodel.h index 577530ac8..d7033941d 100644 --- a/src/gui/transferlistfilters/tagfiltermodel.h +++ b/src/gui/transferlistfilters/tagfiltermodel.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2017 Tony Gregerson * * This program is free software; you can redistribute it and/or