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 <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
#include <QJsonValueRef>
|
#include <QJsonValueRef>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLayoutItem>
|
#include <QLayoutItem>
|
||||||
|
|
@ -60,8 +61,6 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
|
||||||
{
|
{
|
||||||
// Initialize network
|
// Initialize network
|
||||||
networkManager = NetworkManagerFactory::create(this);
|
networkManager = NetworkManagerFactory::create(this);
|
||||||
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
|
|
||||||
connect(networkManager, &QNetworkAccessManager::finished, this, &TabArchidekt::processApiJson);
|
|
||||||
|
|
||||||
// Initialize debounce timer
|
// Initialize debounce timer
|
||||||
searchDebounceTimer = new QTimer(this);
|
searchDebounceTimer = new QTimer(this);
|
||||||
|
|
@ -77,6 +76,48 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor)
|
||||||
getTopDecks();
|
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()
|
void TabArchidekt::initializeUi()
|
||||||
{
|
{
|
||||||
// Main container
|
// Main container
|
||||||
|
|
@ -194,6 +235,18 @@ void TabArchidekt::initializeUi()
|
||||||
|
|
||||||
mainLayout->addWidget(primaryToolbar);
|
mainLayout->addWidget(primaryToolbar);
|
||||||
mainLayout->addWidget(secondaryToolbar);
|
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);
|
mainLayout->addWidget(scrollArea);
|
||||||
|
|
||||||
setCentralWidget(container);
|
setCentralWidget(container);
|
||||||
|
|
@ -404,6 +457,7 @@ void TabArchidekt::retranslateUi()
|
||||||
minDeckSizeLogicCombo->setItemText(0, tr("Exact"));
|
minDeckSizeLogicCombo->setItemText(0, tr("Exact"));
|
||||||
|
|
||||||
searchButton->setText(tr("Search"));
|
searchButton->setText(tr("Search"));
|
||||||
|
retryButton->setText(tr("Retry"));
|
||||||
|
|
||||||
settingsButton->setToolTip(tr("Display Settings"));
|
settingsButton->setToolTip(tr("Display Settings"));
|
||||||
}
|
}
|
||||||
|
|
@ -517,82 +571,95 @@ QString TabArchidekt::buildSearchUrl()
|
||||||
|
|
||||||
void TabArchidekt::doSearch()
|
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
|
// Don't debounce - only called by explicit user actions now
|
||||||
doSearchImmediate();
|
doSearchImmediate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabArchidekt::doSearchImmediate()
|
void TabArchidekt::doSearchImmediate()
|
||||||
{
|
{
|
||||||
QString url = buildSearchUrl();
|
searchDebounceTimer->stop();
|
||||||
QNetworkRequest req{QUrl(url)};
|
currentPage = 1;
|
||||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
startRequest(QUrl(buildSearchUrl()), RequestKind::Search);
|
||||||
networkManager->get(req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabArchidekt::loadNextPage()
|
void TabArchidekt::loadNextPage()
|
||||||
{
|
{
|
||||||
if (isLoadingMore) {
|
if (activeReply || !isListMode || !listingsWidget || nextPageUrl.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingMore = true;
|
// Continue the accepted search, even if the user has edited filters without submitting them.
|
||||||
currentPage++;
|
// Advance currentPage only after this response has been accepted, so failures can retry it.
|
||||||
|
startRequest(nextPageUrl, RequestKind::NextPage, currentPage + 1);
|
||||||
QString url = buildSearchUrl();
|
|
||||||
QNetworkRequest req{QUrl(url)};
|
|
||||||
req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
|
||||||
networkManager->get(req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabArchidekt::actNavigatePage(QString url)
|
void TabArchidekt::actNavigatePage(QString url)
|
||||||
{
|
{
|
||||||
QNetworkRequest request{QUrl(url)};
|
searchDebounceTimer->stop();
|
||||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
startRequest(QUrl(url), RequestKind::Deck);
|
||||||
networkManager->get(request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabArchidekt::getTopDecks()
|
void TabArchidekt::getTopDecks()
|
||||||
{
|
{
|
||||||
currentPage = 1;
|
doSearchImmediate();
|
||||||
QNetworkRequest request{QUrl(buildSearchUrl())};
|
|
||||||
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
|
|
||||||
networkManager->get(request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabArchidekt::processApiJson(QNetworkReply *reply)
|
void TabArchidekt::processApiJson(QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
if (reply->error() != QNetworkReply::NoError) {
|
if (reply != activeReply) {
|
||||||
|
reply->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeReply = nullptr;
|
||||||
isLoadingMore = false;
|
isLoadingMore = false;
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
showRequestError(reply->errorString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray responseData = reply->readAll();
|
QByteArray responseData = reply->readAll();
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &parseError);
|
||||||
|
|
||||||
if (!jsonDoc.isObject()) {
|
if (parseError.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
|
||||||
isLoadingMore = false;
|
showRequestError(tr("Invalid response from Archidekt."));
|
||||||
reply->deleteLater();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject jsonObj = jsonDoc.object();
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
QString responseUrl = reply->url().toString();
|
if (requestKind != RequestKind::Deck) {
|
||||||
|
if (!jsonObj.value("results").isArray() ||
|
||||||
if (responseUrl.startsWith("https://archidekt.com/api/decks/v3/")) {
|
(!jsonObj.value("next").isNull() && !jsonObj.value("next").isString())) {
|
||||||
processTopDecksResponse(jsonObj);
|
showRequestError(tr("Invalid deck listing from Archidekt."));
|
||||||
} else if (responseUrl.startsWith("https://archidekt.com/api/decks/")) {
|
return;
|
||||||
processDeckResponse(jsonObj);
|
}
|
||||||
} else {
|
for (const auto &value : jsonObj.value("results").toArray()) {
|
||||||
prettyPrintJson(jsonObj, 4);
|
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)
|
void TabArchidekt::processTopDecksResponse(QJsonObject reply)
|
||||||
|
|
@ -600,8 +667,12 @@ void TabArchidekt::processTopDecksResponse(QJsonObject reply)
|
||||||
ArchidektDeckListingApiResponse deckData;
|
ArchidektDeckListingApiResponse deckData;
|
||||||
deckData.fromJson(reply);
|
deckData.fromJson(reply);
|
||||||
|
|
||||||
|
// An accepted listing restores list mode; a failed deck navigation leaves the listing usable.
|
||||||
|
isListMode = true;
|
||||||
|
|
||||||
// New search → clear everything
|
// New search → clear everything
|
||||||
if (currentPage == 1) {
|
if (requestKind == RequestKind::Search) {
|
||||||
|
listingsWidget = nullptr;
|
||||||
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
||||||
|
|
||||||
listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
listingsWidget = new ArchidektApiResponseDeckListingsDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
||||||
|
|
@ -626,8 +697,10 @@ void TabArchidekt::processDeckResponse(QJsonObject reply)
|
||||||
|
|
||||||
// We're in single deck mode - disable infinite scroll
|
// We're in single deck mode - disable infinite scroll
|
||||||
isListMode = false;
|
isListMode = false;
|
||||||
|
nextPageUrl.clear();
|
||||||
|
|
||||||
// Clear existing results for single deck view
|
// Clear existing results for single deck view
|
||||||
|
listingsWidget = nullptr;
|
||||||
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
QtUtils::clearLayoutRec(resultsLayout, QtUtils::DeletePolicy::Immediate);
|
||||||
|
|
||||||
auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
auto display = new ArchidektApiResponseDeckDisplayWidget(resultsContainer, deckData, cardSizeSlider);
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QPointer>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <qtmetamacros.h>
|
#include <qtmetamacros.h>
|
||||||
|
|
||||||
|
|
@ -55,6 +57,7 @@ public:
|
||||||
* connects signals and slots, and triggers an initial fetch of top decks.
|
* connects signals and slots, and triggers an initial fetch of top decks.
|
||||||
*/
|
*/
|
||||||
explicit TabArchidekt(TabSupervisor *_tabSupervisor);
|
explicit TabArchidekt(TabSupervisor *_tabSupervisor);
|
||||||
|
~TabArchidekt() override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Update all UI text to reflect the current language or translation
|
* @brief Update all UI text to reflect the current language or translation
|
||||||
|
|
@ -190,6 +193,17 @@ protected:
|
||||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
|
|
||||||
private:
|
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
|
* @brief Initialize the main UI layout and toolbars
|
||||||
*
|
*
|
||||||
|
|
@ -236,7 +250,14 @@ private:
|
||||||
int currentPage; ///< Current page number for infinite scroll
|
int currentPage; ///< Current page number for infinite scroll
|
||||||
bool isLoadingMore; ///< Flag to prevent multiple simultaneous page loads
|
bool isLoadingMore; ///< Flag to prevent multiple simultaneous page loads
|
||||||
bool isListMode;
|
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
|
// Layout Containers
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue