mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-22 09:35:08 -07:00
[DeckShare] Browse and open public decks with loading, error and accessibility states
Add a public-decks tab that lists decks published by other users using the server's deck visibility feature, previewing each deck's banner card, color identity, tags and upload time without downloading the deck list until the user opens it. - Add a public-decks tab with a shared-settings widget and a remote model that fetches the target user's decks and refreshes both automatically and on user request, with a loading indicator and a server-error message instead of a blank tab when the fetch fails or the connection drops - Render each deck as a focusable preview tile whose banner, color identity, tags and upload time follow the existing Preview settings, with the deck name announced as the tile's accessible name and Space/Enter opening the deck, mirroring the shared-deck preview tile - Show a message box when opening a public deck fails or arrives corrupted - Publish and unpublish decks from the server storage toolbar and context menu, toggling the deck's own visibility bit (what the server persists) rather than the inherited effective state, and batch the visibility refresh until the last in-flight change is acknowledged - Add the Show Upload Time setting so the tile's upload stamp can be hidden like the other preview details - Update the retranslateUi wiring for the new public-decks tab and rename the share action tooltip from "Deck share" to "Share link"
This commit is contained in:
parent
20079dc471
commit
da1fafeed8
22 changed files with 1289 additions and 8 deletions
|
|
@ -324,6 +324,7 @@ bool AbstractTabDeckEditor::actSaveDeck()
|
|||
Command_DeckUpload cmd;
|
||||
cmd.set_deck_id(static_cast<google::protobuf::uint32>(loadedDeck.lastLoadInfo.remoteDeckId));
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
cmd.set_tags(loadedDeck.deckList.getTags().join(QStringLiteral(",")).toStdString());
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &AbstractTabDeckEditor::saveDeckRemoteFinished);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
#include "public_decks_quick_settings_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QSpinBox>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
PublicDecksQuickSettingsWidget::PublicDecksQuickSettingsWidget(QWidget *parent) : SettingsButtonWidget(parent)
|
||||
{
|
||||
// show color identity on preview tiles checkbox
|
||||
showColorIdentityCheckBox = new QCheckBox(this);
|
||||
showColorIdentityCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity());
|
||||
connect(showColorIdentityCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showColorIdentityChanged);
|
||||
connect(showColorIdentityCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowColorIdentity);
|
||||
|
||||
// show tags on preview tiles checkbox
|
||||
showTagsOnDeckPreviewsCheckBox = new QCheckBox(this);
|
||||
showTagsOnDeckPreviewsCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews());
|
||||
connect(showTagsOnDeckPreviewsCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showTagsOnDeckPreviewsChanged);
|
||||
connect(showTagsOnDeckPreviewsCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews);
|
||||
|
||||
// show the last modified / upload time on preview tiles checkbox
|
||||
showUploadTimeCheckBox = new QCheckBox(this);
|
||||
showUploadTimeCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime());
|
||||
connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showUploadTimeChanged);
|
||||
connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime);
|
||||
|
||||
// show tag filter box checkbox
|
||||
showTagFilterCheckBox = new QCheckBox(this);
|
||||
showTagFilterCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter());
|
||||
connect(showTagFilterCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showTagFilterChanged);
|
||||
connect(showTagFilterCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowTagFilter);
|
||||
|
||||
// draw unused color identities checkbox
|
||||
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
|
||||
drawUnusedColorIdentitiesCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities());
|
||||
connect(drawUnusedColorIdentitiesCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::drawUnusedColorIdentitiesChanged);
|
||||
connect(drawUnusedColorIdentitiesCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities);
|
||||
|
||||
// unused color identities opacity selector
|
||||
auto unusedColorIdentityOpacityWidget = new QWidget(this);
|
||||
|
||||
unusedColorIdentitiesOpacityLabel = new QLabel(unusedColorIdentityOpacityWidget);
|
||||
unusedColorIdentitiesOpacitySpinBox = new QSpinBox(unusedColorIdentityOpacityWidget);
|
||||
|
||||
unusedColorIdentitiesOpacitySpinBox->setMinimum(0);
|
||||
unusedColorIdentitiesOpacitySpinBox->setMaximum(100);
|
||||
unusedColorIdentitiesOpacitySpinBox->setValue(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageUnusedColorIdentitiesOpacity());
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&PublicDecksQuickSettingsWidget::unusedColorIdentitiesOpacityChanged);
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged),
|
||||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
|
||||
unusedColorIdentitiesOpacityLabel->setBuddy(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
auto unusedColorIdentityOpacityLayout = new QHBoxLayout(unusedColorIdentityOpacityWidget);
|
||||
unusedColorIdentityOpacityLayout->setContentsMargins(11, 0, 11, 0);
|
||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
|
||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
// card size slider (kept at the bottom, like the Visual Deck Storage)
|
||||
cardSizeWidget =
|
||||
new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize());
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
|
||||
&PublicDecksQuickSettingsWidget::cardSizeChanged);
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDeckStorageCardSize);
|
||||
|
||||
this->addSettingsWidget(showColorIdentityCheckBox);
|
||||
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
||||
this->addSettingsWidget(showUploadTimeCheckBox);
|
||||
this->addSettingsWidget(showTagFilterCheckBox);
|
||||
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
||||
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
||||
this->addSettingsWidget(cardSizeWidget);
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this,
|
||||
&PublicDecksQuickSettingsWidget::retranslateUi);
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PublicDecksQuickSettingsWidget::retranslateUi()
|
||||
{
|
||||
showColorIdentityCheckBox->setText(tr("Show Color Identity"));
|
||||
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
||||
showUploadTimeCheckBox->setText(tr("Show Upload Time"));
|
||||
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
||||
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
||||
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
||||
unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getDrawUnusedColorIdentities() const
|
||||
{
|
||||
return drawUnusedColorIdentitiesCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowColorIdentity() const
|
||||
{
|
||||
return showColorIdentityCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowTagFilter() const
|
||||
{
|
||||
return showTagFilterCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowTagsOnDeckPreviews() const
|
||||
{
|
||||
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowUploadTime() const
|
||||
{
|
||||
return showUploadTimeCheckBox->isChecked();
|
||||
}
|
||||
|
||||
int PublicDecksQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
||||
{
|
||||
return unusedColorIdentitiesOpacitySpinBox->value();
|
||||
}
|
||||
|
||||
CardSizeWidget *PublicDecksQuickSettingsWidget::getCardSizeWidget() const
|
||||
{
|
||||
return cardSizeWidget;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* @file public_decks_quick_settings_widget.h
|
||||
* @ingroup Tabs
|
||||
* @brief The quick settings menu for the public decks tab.
|
||||
* Manages the widgets in the quick settings menu dropdown of the public decks
|
||||
* tab, and syncs their values with the same SettingsCache keys the Visual Deck
|
||||
* Storage uses, so shared preview widgets (color identity, tags) behave the
|
||||
* same way in both places.
|
||||
*/
|
||||
|
||||
#ifndef PUBLIC_DECKS_QUICK_SETTINGS_WIDGET_H
|
||||
#define PUBLIC_DECKS_QUICK_SETTINGS_WIDGET_H
|
||||
|
||||
#include "../quick_settings/settings_button_widget.h"
|
||||
|
||||
class CardSizeWidget;
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QSpinBox;
|
||||
|
||||
class PublicDecksQuickSettingsWidget : public SettingsButtonWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QCheckBox *showColorIdentityCheckBox;
|
||||
QCheckBox *drawUnusedColorIdentitiesCheckBox;
|
||||
QCheckBox *showTagFilterCheckBox;
|
||||
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
||||
QCheckBox *showUploadTimeCheckBox;
|
||||
QLabel *unusedColorIdentitiesOpacityLabel;
|
||||
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
||||
public:
|
||||
explicit PublicDecksQuickSettingsWidget(QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
[[nodiscard]] bool getDrawUnusedColorIdentities() const;
|
||||
[[nodiscard]] bool getShowColorIdentity() const;
|
||||
[[nodiscard]] bool getShowTagFilter() const;
|
||||
[[nodiscard]] bool getShowTagsOnDeckPreviews() const;
|
||||
[[nodiscard]] bool getShowUploadTime() const;
|
||||
[[nodiscard]] int getUnusedColorIdentitiesOpacity() const;
|
||||
[[nodiscard]] CardSizeWidget *getCardSizeWidget() const;
|
||||
|
||||
signals:
|
||||
void drawUnusedColorIdentitiesChanged(bool enabled);
|
||||
void showColorIdentityChanged(bool enabled);
|
||||
void showTagFilterChanged(bool enabled);
|
||||
void showTagsOnDeckPreviewsChanged(bool enabled);
|
||||
void showUploadTimeChanged(bool enabled);
|
||||
void unusedColorIdentitiesOpacityChanged(int opacity);
|
||||
void cardSizeChanged(int scale);
|
||||
};
|
||||
|
||||
#endif // PUBLIC_DECKS_QUICK_SETTINGS_WIDGET_H
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../../pixel_map_generator.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
#include "../deck_share/deck_share_utils.h"
|
||||
#include "../deck_share/share_bar_widget.h"
|
||||
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
|
||||
|
|
@ -25,11 +26,13 @@
|
|||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_set_visibility.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
|
|
@ -168,6 +171,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share")));
|
||||
connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks);
|
||||
|
||||
aPublishDeck = new QAction(this);
|
||||
aPublishDeck->setIcon(QPixmap("theme:icons/lock"));
|
||||
connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck);
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalDeck);
|
||||
leftToolBar->addAction(aRenameLocal);
|
||||
|
|
@ -180,6 +187,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
rightToolBar->addAction(aOpenRemoteDeck);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aShareDecks);
|
||||
rightToolBar->addAction(aPublishDeck);
|
||||
rightToolBar->addAction(aNewFolder);
|
||||
rightToolBar->addAction(aDeleteRemoteDeck);
|
||||
|
||||
|
|
@ -209,6 +217,7 @@ void TabDeckStorage::retranslateUi()
|
|||
aDeleteLocalDeck->setText(tr("Delete"));
|
||||
aDeleteRemoteDeck->setText(tr("Delete"));
|
||||
aShareDecks->setText(tr("Share decks"));
|
||||
aPublishDeck->setText(tr("Publish/unpublish deck"));
|
||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||
shareBar->retranslateUi();
|
||||
if (shareBar->isVisible()) {
|
||||
|
|
@ -256,6 +265,7 @@ void TabDeckStorage::setRemoteEnabled(bool enabled)
|
|||
aOpenRemoteDeck->setEnabled(enabled);
|
||||
aDownload->setEnabled(enabled);
|
||||
aShareDecks->setEnabled(enabled);
|
||||
aPublishDeck->setEnabled(enabled);
|
||||
aNewFolder->setEnabled(enabled);
|
||||
aDeleteRemoteDeck->setEnabled(enabled);
|
||||
|
||||
|
|
@ -384,6 +394,12 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa
|
|||
cmd.set_path(targetPath.toStdString());
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
|
||||
const CardRef bannerCard = deck.getBannerCard();
|
||||
cmd.set_banner_card_name(bannerCard.name.toStdString());
|
||||
cmd.set_banner_card_provider(bannerCard.providerId.toStdString());
|
||||
cmd.set_color_identity(getDeckColorIdentity(deck, CardDatabaseManager::query()).toStdString());
|
||||
cmd.set_tags(deck.getTags().join(QStringLiteral(",")).toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::uploadFinished);
|
||||
client->sendCommand(pend);
|
||||
|
|
@ -814,7 +830,7 @@ void TabDeckStorage::shareFromTreeFinished(const Response &response, const Comma
|
|||
|
||||
void TabDeckStorage::showShareNotice(const QString &message, bool warning)
|
||||
{
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message,
|
||||
QMessageBox::Ok, this);
|
||||
box.exec();
|
||||
}
|
||||
|
|
@ -828,3 +844,45 @@ void TabDeckStorage::onShareFromTreeTimeout()
|
|||
shareBar->setCreateEnabled(true);
|
||||
showShareNotice(tr("The server did not respond in time. Try again."), true);
|
||||
}
|
||||
|
||||
void TabDeckStorage::actPublishDeck()
|
||||
{
|
||||
const auto selection = serverDirView->getCurrentSelection();
|
||||
for (const auto *node : selection) {
|
||||
Command_DeckSetVisibility cmd;
|
||||
if (const auto *fileNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||
cmd.set_deck_id(fileNode->getId());
|
||||
} else if (const auto *dirNode = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
const QString path = dirNode->getPath();
|
||||
if (path.isEmpty()) {
|
||||
continue; // the root folder cannot be published
|
||||
}
|
||||
cmd.set_folder_path(path.toStdString());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
// Toggle the node's own visibility bit (what the server persists); the
|
||||
// effective visibility shown by the column may additionally be inherited
|
||||
// from a parent folder.
|
||||
cmd.set_is_public(!node->isPublic());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::setVisibilityFinished);
|
||||
++pendingVisibilityChanges;
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::setVisibilityFinished(const Response &r, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (r.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to change deck visibility on server (response code %1).")
|
||||
.arg(QString::number(static_cast<int>(r.response_code()))));
|
||||
}
|
||||
// Refresh once the last in-flight change has been acknowledged so the
|
||||
// Public/Private column reflects every selected node.
|
||||
if (--pendingVisibilityChanges == 0) {
|
||||
serverDirView->refreshTree();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ private:
|
|||
|
||||
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
||||
QAction *aOpenDecksFolder;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck;
|
||||
int pendingVisibilityChanges = 0;
|
||||
QString getTargetPath() const;
|
||||
|
||||
void setRemoteEnabled(bool enabled);
|
||||
|
|
@ -92,6 +93,9 @@ private slots:
|
|||
void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
void onShareFromTreeTimeout();
|
||||
|
||||
void actPublishDeck();
|
||||
void setVisibilityFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
void actDeleteRemoteDeck();
|
||||
void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
|
|
|
|||
228
cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp
Normal file
228
cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#include "tab_public_decks.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h"
|
||||
#include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h"
|
||||
#include "../visual_deck_storage/remote_public_decks_model.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_search_widget.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_tag_filter_widget.h"
|
||||
#include "public_decks_quick_settings_widget.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QStringList>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <optional>
|
||||
|
||||
TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &_userName)
|
||||
: Tab(_tabSupervisor), client(_client), userName(_userName)
|
||||
{
|
||||
model = new RemotePublicDecksModel(client, this);
|
||||
cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize();
|
||||
|
||||
titleLabel = new QLabel(tr("Public decks of %1").arg(userName), this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setBold(true);
|
||||
titleLabel->setFont(titleFont);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout;
|
||||
headerLayout->addWidget(titleLabel);
|
||||
headerLayout->addStretch(1);
|
||||
|
||||
// Filter/toolbar row, matching the Visual Deck Storage: color identity filter
|
||||
// first, the search bar stretching in the middle, and the quick settings
|
||||
// cogwheel at the end. The card size slider lives inside the cogwheel popup.
|
||||
emptyLabel = new QLabel(tr("This user has not published any decks."), this);
|
||||
emptyLabel->setAlignment(Qt::AlignCenter);
|
||||
emptyLabel->setVisible(false);
|
||||
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
statusLabel->setVisible(false);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
flowWidget->setSpacing(8, 8);
|
||||
|
||||
colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this);
|
||||
searchWidget = new VisualDeckStorageSearchWidget(this);
|
||||
refreshButton = new QToolButton(this);
|
||||
refreshButton->setIcon(QPixmap("theme:icons/reload"));
|
||||
refreshButton->setFixedSize(32, 32);
|
||||
quickSettingsWidget = new PublicDecksQuickSettingsWidget(this);
|
||||
|
||||
auto *filterLayout = new QHBoxLayout;
|
||||
filterLayout->addWidget(colorIdentityFilter);
|
||||
filterLayout->addWidget(searchWidget, 1);
|
||||
filterLayout->addWidget(refreshButton);
|
||||
filterLayout->addWidget(quickSettingsWidget);
|
||||
|
||||
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
||||
tagFilterWidget->setAllTagsProvider([this] { return model->allTags(); });
|
||||
updateTagsVisibility(quickSettingsWidget->getShowTagFilter());
|
||||
|
||||
auto *layout = new QVBoxLayout;
|
||||
layout->addLayout(headerLayout);
|
||||
layout->addLayout(filterLayout);
|
||||
layout->addWidget(tagFilterWidget);
|
||||
layout->addWidget(statusLabel);
|
||||
layout->addWidget(emptyLabel);
|
||||
layout->addWidget(flowWidget, 1);
|
||||
|
||||
auto *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(layout);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
connect(refreshButton, &QToolButton::clicked, this, [this] { model->refresh(userName); });
|
||||
connect(model, &QAbstractItemModel::modelReset, this, &TabPublicDecks::rebuildGrid);
|
||||
connect(model, &RemotePublicDecksModel::loadingChanged, this, &TabPublicDecks::updateLoadingState);
|
||||
connect(model, &RemotePublicDecksModel::loadFailed, this, [this](const QString &message) {
|
||||
statusLabel->setText(message);
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
});
|
||||
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
||||
[this](const QString &text) { model->setSearchText(text); });
|
||||
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this,
|
||||
&TabPublicDecks::updateColorFilter);
|
||||
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this,
|
||||
&TabPublicDecks::updateColorFilter);
|
||||
connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, &TabPublicDecks::updateTagFilter);
|
||||
connect(quickSettingsWidget, &PublicDecksQuickSettingsWidget::cardSizeChanged, this,
|
||||
&TabPublicDecks::updateCardSize);
|
||||
connect(quickSettingsWidget, &PublicDecksQuickSettingsWidget::showTagFilterChanged, this,
|
||||
&TabPublicDecks::updateTagsVisibility);
|
||||
|
||||
model->refresh(userName);
|
||||
}
|
||||
|
||||
QString TabPublicDecks::getTabText() const
|
||||
{
|
||||
return tr("Public decks of %1").arg(userName);
|
||||
}
|
||||
|
||||
void TabPublicDecks::retranslateUi()
|
||||
{
|
||||
titleLabel->setText(tr("Public decks of %1").arg(userName));
|
||||
emptyLabel->setText(tr("This user has not published any decks."));
|
||||
refreshButton->setToolTip(tr("Refresh"));
|
||||
quickSettingsWidget->setToolTip(tr("Public Decks Settings"));
|
||||
emit tabTextChanged(this, getTabText());
|
||||
}
|
||||
|
||||
bool TabPublicDecks::closeRequest()
|
||||
{
|
||||
emit closing(this);
|
||||
return Tab::closeRequest();
|
||||
}
|
||||
|
||||
void TabPublicDecks::rebuildGrid()
|
||||
{
|
||||
flowWidget->clearLayout();
|
||||
|
||||
const int count = model->rowCount();
|
||||
if (count == 0) {
|
||||
emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.")
|
||||
: tr("This user has not published any decks."));
|
||||
}
|
||||
emptyLabel->setVisible(count == 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto *tile = new PublicDeckPreviewWidget(flowWidget, model->entryAt(i));
|
||||
tile->setScaleFactor(cardSize);
|
||||
connect(tile, &PublicDeckPreviewWidget::openDeckRequested, this, &TabPublicDecks::openDeck);
|
||||
flowWidget->addWidget(tile);
|
||||
}
|
||||
|
||||
// The deck set changed, so the tag filter chips are re-gathered from it.
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateColorFilter()
|
||||
{
|
||||
model->setColorFilter(colorIdentityFilter->getFilterMode(), colorIdentityFilter->getActiveColors());
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateTagFilter()
|
||||
{
|
||||
const QStringList selectedTags = tagFilterWidget->selectedTags();
|
||||
const QStringList excludedTags = tagFilterWidget->excludedTags();
|
||||
model->setTagFilter(QSet<QString>(selectedTags.cbegin(), selectedTags.cend()),
|
||||
QSet<QString>(excludedTags.cbegin(), excludedTags.cend()));
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateTagsVisibility(bool visible)
|
||||
{
|
||||
tagFilterWidget->setVisible(visible);
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateLoadingState(bool loading)
|
||||
{
|
||||
if (loading) {
|
||||
statusLabel->setText(tr("Loading public decks…"));
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
} else {
|
||||
statusLabel->setVisible(false);
|
||||
flowWidget->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateCardSize(int scale)
|
||||
{
|
||||
cardSize = scale;
|
||||
applyCardSize(scale);
|
||||
}
|
||||
|
||||
void TabPublicDecks::applyCardSize(int scale)
|
||||
{
|
||||
const auto tiles = flowWidget->findChildren<PublicDeckPreviewWidget *>();
|
||||
for (PublicDeckPreviewWidget *tile : tiles) {
|
||||
tile->setScaleFactor(scale);
|
||||
}
|
||||
flowWidget->setMinimumSizeToMaxSizeHint();
|
||||
}
|
||||
|
||||
void TabPublicDecks::openDeck(int deckId)
|
||||
{
|
||||
Command_DeckDownloadPublic cmd;
|
||||
cmd.set_deck_id(deckId);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabPublicDecks::openDeckFinished);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabPublicDecks::openDeckFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::warning(this, tr("Open public deck"),
|
||||
tr("Failed to open the public deck (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_DeckDownload &resp = response.GetExtension(Response_DeckDownload::ext);
|
||||
std::optional<LoadedDeck> deckOpt =
|
||||
DeckLoader::loadFromRemote(QString::fromStdString(resp.deck()), LoadedDeck::LoadInfo::NON_REMOTE_ID);
|
||||
if (!deckOpt) {
|
||||
QMessageBox::warning(this, tr("Open public deck"), tr("The public deck could not be parsed."));
|
||||
return;
|
||||
}
|
||||
|
||||
tabSupervisor->openDeckInNewTab(deckOpt.value());
|
||||
}
|
||||
79
cockatrice/src/interface/widgets/tabs/tab_public_decks.h
Normal file
79
cockatrice/src/interface/widgets/tabs/tab_public_decks.h
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* @file tab_public_decks.h
|
||||
* @ingroup Tabs
|
||||
*/
|
||||
|
||||
#ifndef TAB_PUBLIC_DECKS_H
|
||||
#define TAB_PUBLIC_DECKS_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckPreviewColorIdentityFilterWidget;
|
||||
class FlowWidget;
|
||||
class PublicDeckPreviewWidget;
|
||||
class PublicDecksQuickSettingsWidget;
|
||||
class QLabel;
|
||||
class QToolButton;
|
||||
class RemotePublicDecksModel;
|
||||
class Response;
|
||||
class VisualDeckStorageSearchWidget;
|
||||
class VisualDeckStorageTagFilterWidget;
|
||||
|
||||
/**
|
||||
* @brief A visual grid of the public decks published by another user.
|
||||
*
|
||||
* The grid is rendered from the preview metadata the server stores for the
|
||||
* decks, so browsing costs no downloads; the deck list is fetched via
|
||||
* Command_DeckDownloadPublic only when the user opens a deck. Multiple users
|
||||
* can be browsed simultaneously; each gets its own tab.
|
||||
*/
|
||||
class TabPublicDecks final : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TabPublicDecks(TabSupervisor *tabSupervisor, AbstractClient *client, const QString &userName);
|
||||
|
||||
[[nodiscard]] QString getTabText() const override;
|
||||
void retranslateUi() override;
|
||||
bool closeRequest() override;
|
||||
|
||||
[[nodiscard]] QString getUserName() const
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
signals:
|
||||
void closing(TabPublicDecks *tab);
|
||||
|
||||
private slots:
|
||||
void openDeck(int deckId);
|
||||
void openDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void updateColorFilter();
|
||||
void updateTagFilter();
|
||||
void updateCardSize(int scale);
|
||||
void updateTagsVisibility(bool visible);
|
||||
void updateLoadingState(bool loading);
|
||||
|
||||
private:
|
||||
void rebuildGrid();
|
||||
void applyCardSize(int scale);
|
||||
|
||||
AbstractClient *client;
|
||||
QString userName;
|
||||
RemotePublicDecksModel *model;
|
||||
FlowWidget *flowWidget;
|
||||
VisualDeckStorageSearchWidget *searchWidget;
|
||||
DeckPreviewColorIdentityFilterWidget *colorIdentityFilter;
|
||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||
QToolButton *refreshButton;
|
||||
PublicDecksQuickSettingsWidget *quickSettingsWidget;
|
||||
QLabel *titleLabel;
|
||||
QLabel *statusLabel;
|
||||
QLabel *emptyLabel;
|
||||
int cardSize = 100;
|
||||
};
|
||||
|
||||
#endif // TAB_PUBLIC_DECKS_H
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
#include "tab_logs.h"
|
||||
#include "tab_message.h"
|
||||
#include "tab_moderation.h"
|
||||
#include "tab_public_decks.h"
|
||||
#include "tab_replays.h"
|
||||
#include "tab_report.h"
|
||||
#include "tab_room.h"
|
||||
|
|
@ -274,6 +275,10 @@ void TabSupervisor::retranslateUi()
|
|||
while (gameIterator.hasNext()) {
|
||||
tabs.append(gameIterator.next().value());
|
||||
}
|
||||
QMapIterator<QString, TabPublicDecks *> publicDecksIterator(publicDecksTabs);
|
||||
while (publicDecksIterator.hasNext()) {
|
||||
tabs.append(publicDecksIterator.next().value());
|
||||
}
|
||||
QListIterator<TabGame *> replayIterator(replayTabs);
|
||||
while (replayIterator.hasNext()) {
|
||||
tabs.append(replayIterator.next());
|
||||
|
|
@ -1037,6 +1042,30 @@ void TabSupervisor::roomLeft(TabRoom *tab)
|
|||
removeTab(indexOf(tab));
|
||||
}
|
||||
|
||||
void TabSupervisor::openTabPublicDecks(const QString &userName)
|
||||
{
|
||||
if (auto *existing = publicDecksTabs.value(userName, nullptr)) {
|
||||
setCurrentWidget(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
auto *tab = new TabPublicDecks(this, client, userName);
|
||||
connect(tab, &TabPublicDecks::closing, this, &TabSupervisor::publicDecksClosed);
|
||||
myAddTab(tab);
|
||||
publicDecksTabs.insert(userName, tab);
|
||||
setCurrentWidget(tab);
|
||||
}
|
||||
|
||||
void TabSupervisor::publicDecksClosed(TabPublicDecks *tab)
|
||||
{
|
||||
if (tab == currentWidget()) {
|
||||
emit setMenu();
|
||||
}
|
||||
|
||||
publicDecksTabs.remove(tab->getUserName());
|
||||
removeTab(indexOf(tab));
|
||||
}
|
||||
|
||||
void TabSupervisor::switchToFirstAvailableNetworkTab()
|
||||
{
|
||||
if (!roomTabs.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class TabAccount;
|
|||
class TabDeckEditor;
|
||||
class TabDeveloper;
|
||||
class TabLog;
|
||||
class TabPublicDecks;
|
||||
class RoomEvent;
|
||||
class GameEventContainer;
|
||||
class Event_GameJoined;
|
||||
|
|
@ -114,6 +115,7 @@ private:
|
|||
QMap<int, TabGame *> gameTabs;
|
||||
QList<TabGame *> replayTabs;
|
||||
QMap<QString, TabMessage *> messageTabs;
|
||||
QMap<QString, TabPublicDecks *> publicDecksTabs;
|
||||
QList<AbstractTabDeckEditor *> deckEditorTabs;
|
||||
bool isLocalGame;
|
||||
|
||||
|
|
@ -203,6 +205,7 @@ public slots:
|
|||
void actTabReplays(bool checked);
|
||||
void openTabServer();
|
||||
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
|
||||
void openTabPublicDecks(const QString &userName);
|
||||
private slots:
|
||||
void refreshShortcuts();
|
||||
|
||||
|
|
@ -235,6 +238,7 @@ private slots:
|
|||
void localGameJoined(const Event_GameJoined &event);
|
||||
void gameLeft(TabGame *tab);
|
||||
void roomLeft(TabRoom *tab);
|
||||
void publicDecksClosed(TabPublicDecks *tab);
|
||||
TabMessage *addMessageTab(const QString &userName, bool focus);
|
||||
void replayLeft(TabGame *tab);
|
||||
void processUserLeft(const QString &userName);
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status)
|
|||
|
||||
void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning)
|
||||
{
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message,
|
||||
QMessageBox::Ok, this);
|
||||
box.exec();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue