mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Client] Keep Archidekt navigation tied to its active request (#7723)
* [Client] Keep Archidekt navigation tied to its active request [skip ci] * [Client] Keep Archidekt listing pagination after a failed navigation Only update list mode and the next-page link when a listing or deck response is accepted, so a failed deck navigation leaves the existing listing scrollable. Retry still re-issues the failed request. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
81a0b97240
commit
75d7764643
2 changed files with 136 additions and 42 deletions
|
|
@ -25,6 +25,7 @@
|
|||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValueRef>
|
||||
#include <QLabel>
|
||||
#include <QLayoutItem>
|
||||
|
|
@ -60,8 +61,6 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
|
|||
{
|
||||
// Initialize network
|
||||
networkManager = NetworkManagerFactory::create(this);
|
||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
||||
connect(networkManager, &QNetworkAccessManager::finished, this, &TabArchidekt::processApiJson);
|
||||
|
||||
// Initialize debounce timer
|
||||
searchDebounceTimer = new QTimer(this);
|
||||
|
|
@ -77,6 +76,48 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
|
|||
getTopDecks();
|
||||
}
|
||||
|
||||
TabArchidekt::~TabArchidekt()
|
||||
{
|
||||
cancelRequest();
|
||||
}
|
||||
|
||||
void TabArchidekt::cancelRequest()
|
||||
{
|
||||
const QPointer<QNetworkReply> previousReply = activeReply;
|
||||
activeReply = nullptr;
|
||||
isLoadingMore = false;
|
||||
if (previousReply) {
|
||||
disconnect(previousReply, nullptr, this, nullptr);
|
||||
previousReply->abort();
|
||||
previousReply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
void TabArchidekt::startRequest(QUrl url, RequestKind kind, int page)
|
||||
{
|
||||
cancelRequest();
|
||||
requestUrl = url;
|
||||
requestKind = kind;
|
||||
requestedPage = page;
|
||||
isLoadingMore = true;
|
||||
retryButton->hide();
|
||||
requestStatusLabel->setText(kind == RequestKind::Deck ? tr("Loading deck…") : tr("Loading decks…"));
|
||||
requestStatusLabel->show();
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
auto *reply = networkManager->get(request);
|
||||
activeReply = reply;
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() { processApiJson(reply); });
|
||||
}
|
||||
|
||||
void TabArchidekt::showRequestError(const QString &error)
|
||||
{
|
||||
requestStatusLabel->setText(tr("Unable to load decks: %1").arg(error));
|
||||
requestStatusLabel->show();
|
||||
retryButton->show();
|
||||
}
|
||||
|
||||
void TabArchidekt::initializeUi()
|
||||
{
|
||||
// Main container
|
||||
|
|
@ -194,6 +235,18 @@ void TabArchidekt::initializeUi()
|
|||
|
||||
mainLayout->addWidget(primaryToolbar);
|
||||
mainLayout->addWidget(secondaryToolbar);
|
||||
|
||||
auto *requestStatusRow = new QHBoxLayout;
|
||||
requestStatusLabel = new QLabel(container);
|
||||
requestStatusLabel->setTextFormat(Qt::PlainText);
|
||||
requestStatusLabel->setWordWrap(true);
|
||||
retryButton = new QPushButton(tr("Retry"), container);
|
||||
retryButton->hide();
|
||||
connect(retryButton, &QPushButton::clicked, this,
|
||||
[this]() { startRequest(requestUrl, requestKind, requestedPage); });
|
||||
requestStatusRow->addWidget(requestStatusLabel, 1);
|
||||
requestStatusRow->addWidget(retryButton);
|
||||
mainLayout->addLayout(requestStatusRow);
|
||||
mainLayout->addWidget(scrollArea);
|
||||
|
||||
setCentralWidget(container);
|
||||
|
|
@ -404,6 +457,7 @@ void TabArchidekt::retranslateUi()
|
|||
minDeckSizeLogicCombo->setItemText(0, tr("Exact"));
|
||||
|
||||
searchButton->setText(tr("Search"));
|
||||
retryButton->setText(tr("Retry"));
|
||||
|
||||
settingsButton->setToolTip(tr("Display Settings"));
|
||||
}
|
||||
|
|
@ -517,82 +571,95 @@ QString TabArchidekt::buildSearchUrl()
|
|||
|
||||
void TabArchidekt::doSearch()
|
||||
{
|
||||
// Reset to first page on new search
|
||||
currentPage = 1;
|
||||
// We're searching, so we'll be in list mode
|
||||
isListMode = true;
|
||||
// Don't debounce - only called by explicit user actions now
|
||||
doSearchImmediate();
|
||||
}
|
||||
|
||||
void TabArchidekt::doSearchImmediate()
|
||||
{
|
||||
QString url = buildSearchUrl();
|
||||
QNetworkRequest req{QUrl(url)};
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(req);
|
||||
searchDebounceTimer->stop();
|
||||
currentPage = 1;
|
||||
startRequest(QUrl(buildSearchUrl()), RequestKind::Search);
|
||||
}
|
||||
|
||||
void TabArchidekt::loadNextPage()
|
||||
{
|
||||
if (isLoadingMore) {
|
||||
if (activeReply || !isListMode || !listingsWidget || nextPageUrl.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingMore = true;
|
||||
currentPage++;
|
||||
|
||||
QString url = buildSearchUrl();
|
||||
QNetworkRequest req{QUrl(url)};
|
||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(req);
|
||||
// Continue the accepted search, even if the user has edited filters without submitting them.
|
||||
// Advance currentPage only after this response has been accepted, so failures can retry it.
|
||||
startRequest(nextPageUrl, RequestKind::NextPage, currentPage + 1);
|
||||
}
|
||||
|
||||
void TabArchidekt::actNavigatePage(QString url)
|
||||
{
|
||||
QNetworkRequest request{QUrl(url)};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(request);
|
||||
searchDebounceTimer->stop();
|
||||
startRequest(QUrl(url), RequestKind::Deck);
|
||||
}
|
||||
|
||||
void TabArchidekt::getTopDecks()
|
||||
{
|
||||
currentPage = 1;
|
||||
QNetworkRequest request{QUrl(buildSearchUrl())};
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
||||
networkManager->get(request);
|
||||
doSearchImmediate();
|
||||
}
|
||||
|
||||
void TabArchidekt::processApiJson(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (reply != activeReply) {
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
activeReply = nullptr;
|
||||
isLoadingMore = false;
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
showRequestError(reply->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &parseError);
|
||||
|
||||
if (!jsonDoc.isObject()) {
|
||||
isLoadingMore = false;
|
||||
reply->deleteLater();
|
||||
if (parseError.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
|
||||
showRequestError(tr("Invalid response from Archidekt."));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject jsonObj = jsonDoc.object();
|
||||
QString responseUrl = reply->url().toString();
|
||||
|
||||
if (responseUrl.startsWith("https://archidekt.com/api/decks/v3/")) {
|
||||
processTopDecksResponse(jsonObj);
|
||||
} else if (responseUrl.startsWith("https://archidekt.com/api/decks/")) {
|
||||
processDeckResponse(jsonObj);
|
||||
} else {
|
||||
prettyPrintJson(jsonObj, 4);
|
||||
if (requestKind != RequestKind::Deck) {
|
||||
if (!jsonObj.value("results").isArray() ||
|
||||
(!jsonObj.value("next").isNull() && !jsonObj.value("next").isString())) {
|
||||
showRequestError(tr("Invalid deck listing from Archidekt."));
|
||||
return;
|
||||
}
|
||||
for (const auto &value : jsonObj.value("results").toArray()) {
|
||||
if (!value.isObject() || value.toObject().value("id").toInt() <= 0) {
|
||||
showRequestError(tr("Invalid deck listing from Archidekt."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
currentPage = requestedPage;
|
||||
const QString next = jsonObj.value("next").toString();
|
||||
nextPageUrl = next.isEmpty() ? QUrl() : reply->url().resolved(QUrl(next));
|
||||
// An empty or self-repeating page cannot make forward progress.
|
||||
if (jsonObj.value("results").toArray().isEmpty() || nextPageUrl == reply->url()) {
|
||||
nextPageUrl.clear();
|
||||
}
|
||||
requestStatusLabel->setText(
|
||||
currentPage == 1 && jsonObj.value("results").toArray().isEmpty() ? tr("No decks found.") : QString());
|
||||
requestStatusLabel->setVisible(!requestStatusLabel->text().isEmpty());
|
||||
processTopDecksResponse(jsonObj);
|
||||
} else {
|
||||
if (jsonObj.value("id").toInt() <= 0 || !jsonObj.value("cards").isArray()) {
|
||||
showRequestError(tr("Invalid deck from Archidekt."));
|
||||
return;
|
||||
}
|
||||
requestStatusLabel->hide();
|
||||
processDeckResponse(jsonObj);
|
||||
}
|
||||
|
||||
isLoadingMore = false;
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void TabArchidekt::processTopDecksResponse(QJsonObject reply)
|
||||
|
|
@ -600,8 +667,12 @@ void TabArchidekt::processTopDecksResponse(QJsonObject reply)
|
|||
ArchidektDeckListingApiResponse deckData;
|
||||
deckData.fromJson(reply);
|
||||
|
||||
// An accepted listing restores list mode; a failed deck navigation leaves the listing usable.
|
||||
isListMode = true;
|
||||
|
||||
// New search → clear everything
|
||||
if (currentPage == 1) {
|
||||
if (requestKind == RequestKind::Search) {
|
||||
listingsWidget = nullptr;
|
||||
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
||||
|
||||
listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
||||
|
|
@ -626,8 +697,10 @@ void TabArchidekt::processDeckResponse(QJsonObject reply)
|
|||
|
||||
// We're in single deck mode - disable infinite scroll
|
||||
isListMode = false;
|
||||
nextPageUrl.clear();
|
||||
|
||||
// Clear existing results for single deck view
|
||||
listingsWidget = nullptr;
|
||||
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
||||
|
||||
auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@
|
|||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QList>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ public:
|
|||
* connects signals and slots, and triggers an initial fetch of top decks.
|
||||
*/
|
||||
explicit TabArchidekt(TabSupervisor *_tabSupervisor);
|
||||
~TabArchidekt() override;
|
||||
|
||||
/**
|
||||
* @brief Update all UI text to reflect the current language or translation
|
||||
|
|
@ -190,6 +193,17 @@ protected:
|
|||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private:
|
||||
enum class RequestKind
|
||||
{
|
||||
Search,
|
||||
NextPage,
|
||||
Deck
|
||||
};
|
||||
|
||||
void startRequest(QUrl url, RequestKind kind, int page = 1);
|
||||
void cancelRequest();
|
||||
void showRequestError(const QString &error);
|
||||
|
||||
/**
|
||||
* @brief Initialize the main UI layout and toolbars
|
||||
*
|
||||
|
|
@ -236,7 +250,14 @@ private:
|
|||
int currentPage; ///< Current page number for infinite scroll
|
||||
bool isLoadingMore; ///< Flag to prevent multiple simultaneous page loads
|
||||
bool isListMode;
|
||||
ArchidektApiResponseDeckListingsDisplayWidget *listingsWidget = nullptr;
|
||||
QPointer<QNetworkReply> activeReply;
|
||||
RequestKind requestKind = RequestKind::Search;
|
||||
QUrl requestUrl;
|
||||
QUrl nextPageUrl;
|
||||
int requestedPage = 1;
|
||||
QPointer<ArchidektApiResponseDeckListingsDisplayWidget> listingsWidget;
|
||||
QLabel *requestStatusLabel;
|
||||
QPushButton *retryButton;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Layout Containers
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue