[DeckShare] Create temporary share links for local and server decks (#7243)

* [DeckShare] Create temporary share links for local and server decks

* [DeckShare] Address review findings and harden the share flows

Gate every share entry point on login, de-duplicate the share-link and
color-identity logic behind DeckShareUtils and an injected querier, and
replace the silent tray/status-bar notices with always-visible dialogs.

- abstract_tab_deck_editor: explain that sharing requires a connection
  instead of silently doing nothing when logged out
- tab_deck_storage: disable the share action on disconnect, reject
  folder/deck mixes and the root folder with clear warnings, re-enable
  Create on every entry/response so a dropped connection cannot leave
  the button disabled
- tab_deck_storage_visual: same login gate for the context-menu entry,
  visible success/error dialogs, and a symmetric in-flight guard
- getDeckColorIdentity now takes a CardDatabaseQuerier, dropping the
  CardDatabaseManager singleton access and enabling unit tests

* [DeckShare] Fix share-link expiry build on the minimum-supported Qt

QTimeZone::UTC (the Initialization enum) only exists since Qt 6.7, so
Debian 12 and Ubuntu 24.04 (Qt 6.4) fail to compile the share-link expiry
handling in the share dialog and the two deck-storage tabs. Mirror the
existing games_model guard and fall back to Qt::UTC on older Qt.

* [DeckShare] Use the stable server client for the visual deck storage tab

* [DeckShare] Extract the share-creation response handling into DeckShareUtils

* [DeckShare] Drop includes left unused by the share-response extraction

* [DeckShare] Format share expiry with the locale-aware short format

* [DeckShare] Build share links with QUrl and QUrlQuery for percent-encoding

* [DeckShare] Replace the duplicate computeColorIdentity with the shared getDeckColorIdentity

* [DeckShare] Recover the share controls when the server never answers

* [DeckShare] Provide the full share hint in each plural form

* [DeckShare] Join the selected-count label with a non-translatable separator

* [DeckShare] Retranslate the share button tooltip with the storage widget

* [DeckShare] Forward retranslateUi to the visual deck storage widget

* [DeckShare] Let the share bar owners supply the hint text

* [DeckShare] End the share-related headers and sources with a trailing newline

* [DeckShare] Keep the settings include in the project include block

* [DeckShare] Include the network settings header used by the share timeout

* [DeckShare] Resolve the share theme icon through themePixmap

QPixmap("theme:icons/share") has no file extension, so ThemeManager::assetPath()
is bypassed and the pixmap is always null. Use themePixmap(QStringLiteral("icons/share"))
like every other toolbar action, so the .svg (and dark/light variants) resolves.

* [DeckShare] Keep the share selection consistent with the visible decks

Filtered-out previews are hidden but kept alive, so selectedFilePaths() counted
them in the share and the selection highlight. Only decks the user can see are
now shared, and a deck that stops matching the filters is deselectd as the deck
pass runs, keeping the %n count and the highlight in sync with the screen.

* [DeckShare] Abandon an in-flight tree share on cancel

Leaving share mode never stopped the timeout timer, and a late response still
ran shareFromTreeFinished, copying the link and announcing success for a share
the user backed out of. Stopping the timer and tracking the outstanding request
by sequence number means a stale reply (or a timed-out one) after cancel is
ignored, and cancelling + re-entering share mode can no longer confuse the two
requests.

* [DeckShare] Abandon an in-flight tile share on cancel

exitShareMode() left shareTimeoutTimer running and did not abandon the pending
Command_DeckShareCreate, so a timer pop or a late success still reported the
share after the user cancelled. Stop the timer and ignore stale responses via a
sequence number, mirroring the tree tab.

* [DeckShare] Wire the status-changed handler after shareBar exists

handleConnectionChanged() dereferences shareBar->isVisible(), but the connection
was set up before shareBar was constructed and shareBar had no in-class
initializer. On any status change delivered before construction the slot read an
indeterminate pointer. Seed the connection (and the initial share availability)
after shareBar exists and give shareBar a = nullptr initializer.

* [DeckShare] Explain why a blank deck cannot be shared

A blank deck exited the share flow silently. The menu only disables the entry
via setSaveStatus(), a different predicate, so the path is reachable (e.g. add a
card and remove it again). Mirror the not-logged-in branch with a short
information dialog.

* [DeckShare] Restore the banner-text doc comment

Re-add the doc block above refreshBannerCardText() that was removed as part of
the share-selection work; it documents the coupling to refreshBannerCardToolTip.

* [DeckShare] Resolve the stable server client in the deck editor gate

actShareDeck went through tabSupervisor->getClient(), which hands back a
LocalClient while an offline game is running. LocalClient never sets its status,
so a logged-in user could not share from the deck editor during a local game,
and got a misleading "You must be connected" message. Expose the supervisor's
stable remote client and use it for the gate and the dialog, matching the other
share tabs.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-09-20 20:22:16 +02:00 committed by GitHub
parent a289d61765
commit ba2900dcb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1173 additions and 52 deletions

View file

@ -19,6 +19,7 @@
#include "../interface/widgets/dialogs/dlg_load_deck.h"
#include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
#include "../interface/widgets/dialogs/dlg_load_deck_from_website.h"
#include "../interface/widgets/dialogs/dlg_share_deck.h"
#include "../utility/visibility_change_listener.h"
#include "tab_supervisor.h"
@ -382,6 +383,27 @@ bool AbstractTabDeckEditor::actSaveDeckAs()
return true;
}
/**
* @brief Opens the deck share dialog with the current deck preselected.
*/
void AbstractTabDeckEditor::actShareDeck()
{
AbstractClient *client = tabSupervisor->getServerClient();
if (client->getStatus() != StatusLoggedIn) {
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
return;
}
const QSharedPointer<DeckList> deck = deckStateManager->getDeckListShared();
if (deck->isBlankDeck()) {
QMessageBox::information(this, tr("Share deck"), tr("The deck is empty. Add cards before sharing it."));
return;
}
DlgShareDeck shareDialog(client, deck, this);
shareDialog.exec();
}
/**
* @brief Callback for remote deck save completion.
* @param response Server response.

View file

@ -214,6 +214,9 @@ protected slots:
/** @brief Saves the current deck under a new name. */
virtual bool actSaveDeckAs();
/** @brief Opens the deck share dialog for the current deck. */
void actShareDeck();
/** @brief Loads a deck from the clipboard. */
virtual void actLoadDeckFromClipboard();

View file

@ -3,18 +3,24 @@
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h"
#include "../../pixel_map_generator.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"
#include "../interface/widgets/utility/get_text_with_max.h"
#include <QAction>
#include <QApplication>
#include <QDateTime>
#include <QDebug>
#include <QDesktopServices>
#include <QFileSystemModel>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QLineEdit>
#include <QMessageBox>
#include <QTimer>
#include <QToolBar>
#include <QTreeView>
#include <QUrl>
@ -24,11 +30,14 @@
#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_share_create.pb.h>
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/utility/string_limits.h>
@ -92,8 +101,24 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
serverDirView = new RemoteDeckList_TreeWidget(client);
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
connect(serverDirView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
[this] { onServerSelectionChanged(); });
// Share bar for creating a share link from the selected server decks/folders.
shareBar = new ShareBarWidget(this);
connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorage::actShareSelection);
connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorage::cancelShareDecks);
shareBar->setVisible(false);
shareTimeoutTimer = new QTimer(this);
shareTimeoutTimer->setSingleShot(true);
shareTimeoutTimer->setInterval(
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
SettingsCache::instance().network().getKeepAlive() * 1000));
connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorage::onShareFromTreeTimeout);
QVBoxLayout *rightVbox = new QVBoxLayout;
rightVbox->addWidget(shareBar);
rightVbox->addWidget(serverDirView);
rightVbox->addLayout(rightToolBarLayout);
rightGroupBox = new QGroupBox;
@ -139,6 +164,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
aDeleteRemoteDeck->setIcon(themePixmap(QStringLiteral("icons/remove_row")));
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
aShareDecks = new QAction(this);
aShareDecks->setIcon(themePixmap(QStringLiteral("icons/share")));
connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks);
// Add actions to toolbars
leftToolBar->addAction(aOpenLocalDeck);
leftToolBar->addAction(aRenameLocal);
@ -150,6 +179,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
rightToolBar->addAction(aOpenRemoteDeck);
rightToolBar->addAction(aDownload);
rightToolBar->addAction(aShareDecks);
rightToolBar->addAction(aNewFolder);
rightToolBar->addAction(aDeleteRemoteDeck);
@ -178,7 +208,12 @@ void TabDeckStorage::retranslateUi()
aNewFolder->setText(tr("New folder"));
aDeleteLocalDeck->setText(tr("Delete"));
aDeleteRemoteDeck->setText(tr("Delete"));
aShareDecks->setText(tr("Share decks"));
aOpenDecksFolder->setText(tr("Open decks folder"));
shareBar->retranslateUi();
if (shareBar->isVisible()) {
onServerSelectionChanged();
}
}
QString TabDeckStorage::getTargetPath() const
@ -220,12 +255,14 @@ void TabDeckStorage::setRemoteEnabled(bool enabled)
aUpload->setEnabled(enabled);
aOpenRemoteDeck->setEnabled(enabled);
aDownload->setEnabled(enabled);
aShareDecks->setEnabled(enabled);
aNewFolder->setEnabled(enabled);
aDeleteRemoteDeck->setEnabled(enabled);
if (enabled) {
serverDirView->refreshTree();
} else {
setShareModeEnabled(false);
serverDirView->clearTree();
}
}
@ -626,3 +663,168 @@ void TabDeckStorage::deleteFolderFinished(const Response &response, const Comman
serverDirView->removeNode(toDelete);
}
}
void TabDeckStorage::actShareDecks()
{
setShareModeEnabled(true);
}
void TabDeckStorage::cancelShareDecks()
{
setShareModeEnabled(false);
}
void TabDeckStorage::setShareModeEnabled(bool enabled)
{
shareBar->setVisible(enabled);
if (enabled) {
shareBar->setCreateEnabled(true);
shareBar->setName(tr("Shared decks"));
onServerSelectionChanged();
shareBar->focusName();
} else {
// Abandon any in-flight request: otherwise the timer keeps running and a late
// response reports the share as created after the user already backed out.
shareTimeoutTimer->stop();
shareInFlightSeq = 0;
serverDirView->clearSelection();
}
}
void TabDeckStorage::onServerSelectionChanged()
{
if (!shareBar->isVisible()) {
return;
}
const auto selection = serverDirView->getCurrentSelection();
int folders = 0;
int files = 0;
for (const auto *node : selection) {
if (dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
++folders;
} else {
++files;
}
}
QString hint;
if (folders > 1) {
hint = tr("Only one folder can be shared at a time.");
} else if (folders > 0 && files > 0) {
hint = tr("Share either a folder or decks, not both.");
} else if (folders == 0 && files == 0) {
hint = tr("Select folders or decks in the tree to share.");
}
shareBar->setHintText(hint, !hint.isEmpty());
QStringList parts;
if (folders > 0) {
parts << tr("%n folder(s)", "", folders);
}
if (files > 0) {
parts << tr("%n deck(s)", "", files);
}
shareBar->setCountText(parts.isEmpty() ? tr("No decks selected")
: tr("Selected: %1").arg(parts.join(QStringLiteral(", "))));
}
void TabDeckStorage::actShareSelection()
{
const auto selection = serverDirView->getCurrentSelection();
QString sharedFolder;
bool hasFile = false;
bool hasFolder = false;
for (const auto *node : selection) {
if (const auto *dirNode = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
hasFolder = true;
if (!sharedFolder.isEmpty()) {
showShareNotice(tr("Only one folder can be shared at a time."), true);
return;
}
sharedFolder = dirNode->getPath();
} else {
hasFile = true;
}
}
if (hasFile && hasFolder) {
showShareNotice(tr("Share either a folder or decks, not both."), true);
return;
}
if (hasFolder && sharedFolder.isEmpty()) {
showShareNotice(tr("The root folder cannot be shared."), true);
return;
}
Command_DeckShareCreate cmd;
cmd.set_name(shareBar->name().toStdString());
if (cmd.name().empty()) {
cmd.set_name(tr("Shared decks").toStdString());
}
if (!sharedFolder.isEmpty()) {
cmd.set_folder_path(sharedFolder.toStdString());
} else {
for (const auto *node : selection) {
if (const auto *fileNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(node)) {
DeckShareItem *item = cmd.add_items();
item->set_deck_id(fileNode->getId());
}
}
}
if (cmd.items_size() == 0 && cmd.folder_path().empty()) {
showShareNotice(tr("Select decks to share."), true);
return;
}
shareBar->setCreateEnabled(false);
const int seq = ++shareRequestSeq;
shareInFlightSeq = seq;
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this,
[this, seq](const Response &response, const CommandContainer &commandContainer) {
if (shareInFlightSeq != seq) {
return; // the user cancelled or a newer request superseded this one
}
shareInFlightSeq = 0;
shareFromTreeFinished(response, commandContainer);
});
client->sendCommand(pend);
shareTimeoutTimer->start();
}
void TabDeckStorage::shareFromTreeFinished(const Response &response, const CommandContainer & /*commandContainer*/)
{
shareTimeoutTimer->stop();
shareBar->setCreateEnabled(true);
if (response.response_code() != Response::RespOk) {
qWarning() << "failed to create deck share:" << response.response_code();
showShareNotice(tr("Failed to create the share link (server response code %1).")
.arg(QString::number(static_cast<int>(response.response_code()))),
true);
return;
}
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
showShareNotice(
tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry)));
setShareModeEnabled(false);
}
void TabDeckStorage::showShareNotice(const QString &message, bool warning)
{
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
QMessageBox::Ok, this);
box.exec();
}
void TabDeckStorage::onShareFromTreeTimeout()
{
if (shareInFlightSeq == 0) {
return; // share mode was left while the request was still outstanding
}
shareInFlightSeq = 0;
shareBar->setCreateEnabled(true);
showShareNotice(tr("The server did not respond in time. Try again."), true);
}

View file

@ -22,8 +22,10 @@ class QToolBar;
class QTreeWidget;
class QTreeWidgetItem;
class QGroupBox;
class QTimer;
class CommandContainer;
class Response;
class ShareBarWidget;
class TabDeckStorage : public Tab
{
@ -35,14 +37,22 @@ private:
QToolBar *leftToolBar, *rightToolBar;
RemoteDeckList_TreeWidget *serverDirView;
QGroupBox *leftGroupBox, *rightGroupBox;
ShareBarWidget *shareBar;
QTimer *shareTimeoutTimer;
int shareRequestSeq = 0;
int shareInFlightSeq = 0;
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
QAction *aOpenDecksFolder;
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aNewFolder, *aDeleteRemoteDeck;
QString getTargetPath() const;
void setRemoteEnabled(bool enabled);
void showShareNotice(const QString &message, bool warning = false);
void setShareModeEnabled(bool enabled);
void uploadDeck(const QString &filePath, const QString &targetPath);
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
@ -75,6 +85,13 @@ private slots:
void actNewFolder();
void newFolderFinished(const Response &response, const CommandContainer &commandContainer);
void actShareDecks();
void actShareSelection();
void cancelShareDecks();
void onServerSelectionChanged();
void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer);
void onShareFromTreeTimeout();
void actDeleteRemoteDeck();
void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer);
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);

View file

@ -672,7 +672,7 @@ void TabSupervisor::actTabVisualDeckStorage(bool checked)
void TabSupervisor::openTabVisualDeckStorage()
{
tabVisualDeckStorage = new TabDeckStorageVisual(this);
tabVisualDeckStorage = new TabDeckStorageVisual(this, client);
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
connect(tabVisualDeckStorage, &QObject::destroyed, this, [this] {
tabVisualDeckStorage = nullptr;

View file

@ -152,6 +152,10 @@ public:
return userInfo;
}
[[nodiscard]] AbstractClient *getClient() const;
[[nodiscard]] AbstractClient *getServerClient() const
{
return client;
}
[[nodiscard]] UserListManager *getUserListManager() const
{
return userListManager;

View file

@ -1,25 +1,78 @@
#include "tab_deck_storage_visual.h"
#include "../../../../client/settings/cache_settings.h"
#include "../../../deck_loader/deck_loader.h"
#include "../../cards/additional_info/deck_color_identity.h"
#include "../../deck_share/deck_share_utils.h"
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
#include "../tab_supervisor.h"
#include <QMessageBox>
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
#include <QTimer>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/network_settings.h>
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client)
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this)), client(_client),
shareTimeoutTimer(new QTimer(this))
{
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::openDeckInNewTab);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckLoadRequested, this,
&TabDeckStorageVisual::actOpenLocalDeck);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this,
&TabDeckStorageVisual::openDeckEditor);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareDeckRequested, this,
&TabDeckStorageVisual::actShareDeck);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareSelectionChanged, this,
&TabDeckStorageVisual::onShareSelectionChanged);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareRequested, this, [this] {
if (shareDeckAvailable) {
enterShareMode();
}
});
auto *widget = new QWidget(this);
auto *layout = new QVBoxLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
widget->setLayout(layout);
this->setCentralWidget(widget);
layout->addWidget(visualDeckStorageWidget);
shareBar = new ShareBarWidget(this);
connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorageVisual::actShareSelected);
connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorageVisual::exitShareMode);
layout->insertWidget(0, shareBar);
shareBar->setVisible(false);
connect(client, &AbstractClient::statusChanged, this, &TabDeckStorageVisual::handleConnectionChanged);
shareDeckAvailable = (client->getStatus() == StatusLoggedIn);
visualDeckStorageWidget->setShareAvailable(shareDeckAvailable);
shareTimeoutTimer->setSingleShot(true);
shareTimeoutTimer->setInterval(
static_cast<int>((static_cast<qint64>(SettingsCache::instance().network().getTimeOut()) + 1) *
SettingsCache::instance().network().getKeepAlive() * 1000));
connect(shareTimeoutTimer, &QTimer::timeout, this, &TabDeckStorageVisual::onShareTimeout);
retranslateUi();
}
void TabDeckStorageVisual::retranslateUi()
{
visualDeckStorageWidget->retranslateUi();
shareBar->retranslateUi();
if (shareBar->isVisible()) {
updateShareHint();
}
}
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
@ -33,3 +86,144 @@ void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
emit openDeckEditor(deckOpt.value());
}
void TabDeckStorageVisual::enterShareMode(const QStringList &preselectFiles)
{
if (!shareDeckAvailable) {
return; // sharing is gated on being logged in
}
shareBar->setCreateEnabled(true);
visualDeckStorageWidget->setShareSelectable(true);
visualDeckStorageWidget->setShareSelectedFiles(preselectFiles);
shareBar->setName(tr("Shared decks"));
shareBar->setVisible(true);
updateShareHint();
shareBar->focusName();
}
void TabDeckStorageVisual::exitShareMode()
{
// Abandon any in-flight request: otherwise the timer keeps running and a late
// response reports the share as created after the user already backed out.
shareTimeoutTimer->stop();
shareInFlightSeq = 0;
visualDeckStorageWidget->setShareSelectable(false);
visualDeckStorageWidget->clearShareSelection();
shareBar->setVisible(false);
}
void TabDeckStorageVisual::actShareDeck(const QString &filePath)
{
if (!shareDeckAvailable) {
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
return;
}
enterShareMode({filePath});
}
void TabDeckStorageVisual::onShareSelectionChanged()
{
if (shareBar->isVisible()) {
updateShareHint();
}
}
void TabDeckStorageVisual::updateShareHint()
{
const int count = visualDeckStorageWidget->selectedFilePaths().size();
shareBar->setCountText(tr("%n deck(s)", "", count));
if (count == 0) {
shareBar->setHintText(tr("Click deck tiles to select the decks you want to share."), true);
} else if (count == 1) {
shareBar->setHintText(tr("One deck selected. Create the link to share it with other players."), true);
} else {
shareBar->setHintText(tr("%n decks selected. Create the link to share them with other players.", "", count),
true);
}
}
void TabDeckStorageVisual::actShareSelected()
{
const QStringList filePaths = visualDeckStorageWidget->selectedFilePaths();
if (filePaths.isEmpty()) {
QMessageBox::warning(this, tr("Share decks"), tr("Select at least one deck to share."));
return;
}
Command_DeckShareCreate cmd;
cmd.set_name(shareBar->name().toStdString());
if (cmd.name().empty()) {
cmd.set_name(tr("Shared decks").toStdString());
}
for (const QString &filePath : filePaths) {
std::optional<LoadedDeck> deckOpt =
DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true);
if (!deckOpt) {
QMessageBox::warning(this, tr("Share decks"), tr("Unable to load deck file %1").arg(filePath));
return;
}
DeckShareItem *item = cmd.add_items();
item->set_deck_list(deckOpt->deckList.writeToString_Native().toStdString());
item->set_color_identity(getDeckColorIdentity(deckOpt->deckList, CardDatabaseManager::query()).toStdString());
}
shareBar->setCreateEnabled(false);
const int seq = ++shareRequestSeq;
shareInFlightSeq = seq;
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, &PendingCommand::finished, this,
[this, seq](const Response &response, const CommandContainer &commandContainer) {
if (shareInFlightSeq != seq) {
return; // the user cancelled or a newer request superseded this one
}
shareInFlightSeq = 0;
shareFinished(response, commandContainer);
});
client->sendCommand(pend);
shareTimeoutTimer->start();
}
void TabDeckStorageVisual::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
{
shareTimeoutTimer->stop();
shareBar->setCreateEnabled(true);
if (response.response_code() != Response::RespOk) {
showShareNotice(tr("Failed to create the share link (server response code %1).")
.arg(QString::number(static_cast<int>(response.response_code()))),
true);
return;
}
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
showShareNotice(
tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(share.expiry)));
exitShareMode();
}
void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status)
{
shareDeckAvailable = (status == StatusLoggedIn);
visualDeckStorageWidget->setShareAvailable(shareDeckAvailable);
if (!shareDeckAvailable && shareBar->isVisible()) {
exitShareMode();
}
}
void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning)
{
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
QMessageBox::Ok, this);
box.exec();
}
void TabDeckStorageVisual::onShareTimeout()
{
if (shareInFlightSeq == 0) {
return; // share mode was left while the request was still outstanding
}
shareInFlightSeq = 0;
shareBar->setCreateEnabled(true);
showShareNotice(tr("The server did not respond in time. Try again."), true);
}

View file

@ -7,14 +7,19 @@
#ifndef TAB_DECK_STORAGE_VISUAL_H
#define TAB_DECK_STORAGE_VISUAL_H
#include "../../deck_share/share_bar_widget.h"
#include "../tab.h"
#include <QStringList>
#include <libcockatrice/network/client/abstract/abstract_client.h>
struct LoadedDeck;
class AbstractClient;
class CommandContainer;
class DeckPreviewWidget;
class QFileSystemModel;
class QGroupBox;
class QTimer;
class QToolBar;
class QTreeView;
class QTreeWidget;
@ -26,23 +31,55 @@ class TabDeckStorageVisual final : public Tab
{
Q_OBJECT
public:
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
void retranslateUi() override
{
}
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("Visual Deck Storage");
}
/**
* @brief Enters share-selection mode, optionally preselecting the given deck files.
*/
void enterShareMode(const QStringList &preselectFiles = {});
/**
* @brief Leaves share-selection mode and clears the selection.
*/
void exitShareMode();
[[nodiscard]] bool isShareModeActive() const
{
return shareBar->isVisible();
}
public slots:
void actOpenLocalDeck(const QString &filePath);
void actShareDeck(const QString &filePath);
signals:
void openDeckEditor(const LoadedDeck &deck);
private slots:
void actShareSelected();
void shareFinished(const Response &response, const CommandContainer &commandContainer);
void onShareTimeout();
void onShareSelectionChanged();
void handleConnectionChanged(ClientStatus status);
private:
void showShareNotice(const QString &message, bool warning = false);
void updateShareHint();
VisualDeckStorageWidget *visualDeckStorageWidget;
ShareBarWidget *shareBar = nullptr;
AbstractClient *client;
QTimer *shareTimeoutTimer;
int shareRequestSeq = 0;
int shareInFlightSeq = 0;
bool shareDeckAvailable = false;
};
#endif