mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[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:
parent
a289d61765
commit
ba2900dcb9
29 changed files with 1173 additions and 52 deletions
|
|
@ -50,6 +50,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_register.cpp
|
||||
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||
src/interface/widgets/dialogs/dlg_share_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||
|
|
@ -57,6 +58,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||
src/interface/widgets/dialogs/tip_of_the_day.cpp
|
||||
src/interface/widgets/deck_share/deck_share_utils.cpp
|
||||
src/interface/widgets/deck_share/share_bar_widget.cpp
|
||||
src/filters/deck_filter_string.cpp
|
||||
src/filters/filter_builder.cpp
|
||||
src/filters/filter_tree_model.cpp
|
||||
|
|
@ -163,6 +166,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/palette_editor/palette_grid_widget.cpp
|
||||
src/interface/palette_editor/palette_editor_dialog.cpp
|
||||
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
}
|
||||
lastWidth = totalWidth;
|
||||
|
||||
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
||||
const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width
|
||||
setFixedHeight(totalHeight);
|
||||
|
||||
const int count = layout->count();
|
||||
|
|
@ -97,6 +97,10 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
|||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||
|
||||
if (iconSize <= 0) {
|
||||
lastIconSize = iconSize;
|
||||
return;
|
||||
}
|
||||
if (iconSize == lastIconSize) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
#include "deck_color_identity.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||
|
||||
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db)
|
||||
{
|
||||
const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
||||
if (cardList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||
|
||||
for (const QString &cardName : cardList) {
|
||||
CardInfoPtr currentCard = db->getCardInfo(cardName);
|
||||
if (currentCard) {
|
||||
const QString colors = currentCard->getColors(); // returns something like "WUB"
|
||||
for (const QChar &color : colors) {
|
||||
colorSet.insert(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the color identity is in WUBRG order
|
||||
QString colorIdentity;
|
||||
const QString wubrgOrder = "WUBRG";
|
||||
for (const QChar &color : wubrgOrder) {
|
||||
if (colorSet.contains(color)) {
|
||||
colorIdentity.append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return colorIdentity;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||
#define COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class CardDatabaseQuerier;
|
||||
class DeckList;
|
||||
|
||||
/**
|
||||
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
|
||||
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
|
||||
*
|
||||
* Shared as a free function so the deck storage previews and the deck share
|
||||
* dialog compute identities identically.
|
||||
*
|
||||
* @param db Card database used to look up card color symbols.
|
||||
*/
|
||||
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db);
|
||||
|
||||
#endif // COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||
|
|
@ -38,7 +38,10 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
|||
{
|
||||
singleClickTimer = new QTimer(this);
|
||||
singleClickTimer->setSingleShot(true);
|
||||
connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
|
||||
connect(singleClickTimer, &QTimer::timeout, this, [this]() {
|
||||
emit imageClicked(lastMouseEvent, this);
|
||||
emit imageSingleClicked();
|
||||
});
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this,
|
||||
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public:
|
|||
|
||||
signals:
|
||||
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageSingleClicked();
|
||||
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
#include "deck_share_utils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QLocale>
|
||||
#include <QTimeZone>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||
|
||||
namespace DeckShareUtils
|
||||
{
|
||||
|
||||
QString buildShareLink(const AbstractClient *client, const QString &token)
|
||||
{
|
||||
QUrl url;
|
||||
url.setScheme(QStringLiteral("cockatrice"));
|
||||
url.setHost(QStringLiteral("opendeck"));
|
||||
|
||||
QUrlQuery query;
|
||||
query.addQueryItem(QStringLiteral("share"), token);
|
||||
query.addQueryItem(QStringLiteral("hostname"), client->serverName());
|
||||
query.addQueryItem(QStringLiteral("port"), QString::number(client->serverPort()));
|
||||
url.setQuery(query);
|
||||
|
||||
return url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
QString copyShareLinkToClipboard(const QString &link)
|
||||
{
|
||||
QGuiApplication::clipboard()->setText(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
QString formatShareExpiry(const QDateTime &expiry)
|
||||
{
|
||||
return QLocale().toString(expiry.toLocalTime(), QLocale::ShortFormat);
|
||||
}
|
||||
|
||||
ShareResponse handleShareResponse(const AbstractClient *client, const Response &response)
|
||||
{
|
||||
const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
|
||||
const QString token = QString::fromStdString(resp.token());
|
||||
|
||||
const QString link = buildShareLink(client, token);
|
||||
copyShareLinkToClipboard(link);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), QTimeZone::UTC);
|
||||
#else
|
||||
const QDateTime expiry = QDateTime::fromSecsSinceEpoch(resp.expires_at(), Qt::UTC);
|
||||
#endif
|
||||
|
||||
return {link, expiry};
|
||||
}
|
||||
|
||||
} // namespace DeckShareUtils
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @file deck_share_utils.h
|
||||
* @ingroup DeckShareWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DECK_SHARE_UTILS_H
|
||||
#define DECK_SHARE_UTILS_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
|
||||
class AbstractClient;
|
||||
class Response;
|
||||
|
||||
/**
|
||||
* @brief Shared helpers for creating temporary deck shares.
|
||||
*/
|
||||
namespace DeckShareUtils
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief The outcome of a successful share-create response.
|
||||
*/
|
||||
struct ShareResponse
|
||||
{
|
||||
QString link; ///< The share link that was copied to the clipboard.
|
||||
QDateTime expiry; ///< When the share expires (UTC).
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builds the cockatrice:// link for a freshly created deck share.
|
||||
* @param client Used to embed the target server's hostname and port.
|
||||
* @param token The share token from Response_DeckShareCreate.
|
||||
*/
|
||||
QString buildShareLink(const AbstractClient *client, const QString &token);
|
||||
|
||||
/**
|
||||
* @brief Copies the share link to the clipboard.
|
||||
* @return The link that was copied.
|
||||
*/
|
||||
QString copyShareLinkToClipboard(const QString &link);
|
||||
|
||||
/**
|
||||
* @brief Formats the expiration timestamp for a share.
|
||||
*/
|
||||
QString formatShareExpiry(const QDateTime &expiry);
|
||||
|
||||
/**
|
||||
* @brief Handles a successful Response_DeckShareCreate: builds the share link,
|
||||
* copies it to the clipboard, and derives the share expiry.
|
||||
* @param client Used to embed the target server's hostname and port.
|
||||
* @param response The successful response carrying the share token and expiry.
|
||||
*/
|
||||
ShareResponse handleShareResponse(const AbstractClient *client, const Response &response);
|
||||
|
||||
} // namespace DeckShareUtils
|
||||
|
||||
#endif // DECK_SHARE_UTILS_H
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
#include "share_bar_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
|
||||
ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
auto *layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(12, 10, 12, 10);
|
||||
layout->setSpacing(8);
|
||||
|
||||
hintLabel = new QLabel(this);
|
||||
hintLabel->setWordWrap(true);
|
||||
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setMaximumWidth(260);
|
||||
|
||||
countLabel = new QLabel(this);
|
||||
|
||||
cancelButton = new QPushButton(this);
|
||||
connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested);
|
||||
|
||||
createButton = new QPushButton(this);
|
||||
createButton->setDefault(true);
|
||||
connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested);
|
||||
|
||||
layout->addWidget(hintLabel, 1);
|
||||
layout->addWidget(nameEdit);
|
||||
layout->addWidget(countLabel);
|
||||
layout->addStretch();
|
||||
layout->addWidget(cancelButton);
|
||||
layout->addWidget(createButton);
|
||||
|
||||
setLayout(layout);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void ShareBarWidget::retranslateUi()
|
||||
{
|
||||
nameEdit->setPlaceholderText(tr("Share name"));
|
||||
cancelButton->setText(tr("Cancel"));
|
||||
createButton->setText(tr("Create share link"));
|
||||
}
|
||||
|
||||
QString ShareBarWidget::name() const
|
||||
{
|
||||
return nameEdit->text().trimmed();
|
||||
}
|
||||
|
||||
void ShareBarWidget::setName(const QString &value)
|
||||
{
|
||||
nameEdit->setText(value);
|
||||
}
|
||||
|
||||
void ShareBarWidget::setCountText(const QString &text)
|
||||
{
|
||||
countLabel->setText(text);
|
||||
}
|
||||
|
||||
void ShareBarWidget::setHintText(const QString &text, bool visible)
|
||||
{
|
||||
hintLabel->setText(text);
|
||||
hintLabel->setVisible(visible);
|
||||
}
|
||||
|
||||
void ShareBarWidget::setCreateEnabled(bool enabled)
|
||||
{
|
||||
createButton->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void ShareBarWidget::focusName()
|
||||
{
|
||||
nameEdit->setFocus();
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* @file share_bar_widget.h
|
||||
* @ingroup DeckShareWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef SHARE_BAR_WIDGET_H
|
||||
#define SHARE_BAR_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
* @brief The activated toolbar used to create a temporary deck share.
|
||||
*
|
||||
* A single reusable component shared by the local visual deck storage and the
|
||||
* remote server deck storage tabs, so the share workflow renders identically in
|
||||
* both places. It owns its own widgets, strings, and layout; the owning tab only
|
||||
* sets the count/hint text and reacts to the create/cancel signals.
|
||||
*/
|
||||
class ShareBarWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ShareBarWidget(QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
/** @return The trimmed name entered by the user. */
|
||||
[[nodiscard]] QString name() const;
|
||||
|
||||
/** @brief Resets the name field to the given default. */
|
||||
void setName(const QString &name);
|
||||
|
||||
/** @brief Sets the selected-count summary label text. */
|
||||
void setCountText(const QString &text);
|
||||
|
||||
/** @brief Sets the explainer hint text, showing it when @p visible is true. */
|
||||
void setHintText(const QString &text, bool visible);
|
||||
|
||||
/** @brief Enables or disables the create-share-link button (guards double submission). */
|
||||
void setCreateEnabled(bool enabled);
|
||||
|
||||
/** @brief Moves keyboard focus to the name field. */
|
||||
void focusName();
|
||||
|
||||
signals:
|
||||
void createRequested();
|
||||
void cancelRequested();
|
||||
|
||||
private:
|
||||
QLabel *hintLabel;
|
||||
QLineEdit *nameEdit;
|
||||
QLabel *countLabel;
|
||||
QPushButton *cancelButton;
|
||||
QPushButton *createButton;
|
||||
};
|
||||
|
||||
#endif // SHARE_BAR_WIDGET_H
|
||||
96
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
Normal file
96
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#include "dlg_share_deck.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
#include "../deck_share/deck_share_utils.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#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>
|
||||
|
||||
DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *_parent)
|
||||
: QDialog(_parent), client(_client), deck(_deck), shareTimeoutTimer(new QTimer(this))
|
||||
{
|
||||
setWindowTitle(tr("Share deck"));
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
nameEdit = new QLineEdit(this);
|
||||
nameEdit->setText(tr("Shared deck"));
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
form->addRow(tr("Share name:"), nameEdit);
|
||||
layout->addLayout(form);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link"));
|
||||
buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject);
|
||||
this->buttonBox = buttonBox;
|
||||
layout->addWidget(buttonBox);
|
||||
|
||||
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, &DlgShareDeck::onShareTimeout);
|
||||
}
|
||||
|
||||
void DlgShareDeck::actShare()
|
||||
{
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
|
||||
Command_DeckShareCreate cmd;
|
||||
cmd.set_name(nameEdit->text().trimmed().toStdString());
|
||||
if (cmd.name().empty()) {
|
||||
cmd.set_name(tr("Shared deck").toStdString());
|
||||
}
|
||||
|
||||
DeckShareItem *item = cmd.add_items();
|
||||
item->set_deck_list(deck->writeToString_Native().toStdString());
|
||||
item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished);
|
||||
client->sendCommand(pend);
|
||||
shareTimeoutTimer->start();
|
||||
}
|
||||
|
||||
void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
shareTimeoutTimer->stop();
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||
QMessageBox::critical(this, tr("Share deck"),
|
||||
tr("Failed to create the share link (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const DeckShareUtils::ShareResponse share = DeckShareUtils::handleShareResponse(client, response);
|
||||
|
||||
QMessageBox::information(this, tr("Share deck"),
|
||||
tr("Share link created and copied to the clipboard:\n\n%1\n\n"
|
||||
"The share expires on %2.")
|
||||
.arg(share.link, DeckShareUtils::formatShareExpiry(share.expiry)));
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgShareDeck::onShareTimeout()
|
||||
{
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||
QMessageBox::warning(this, tr("Share deck"), tr("The server did not respond in time. Try again."));
|
||||
}
|
||||
46
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
Normal file
46
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @file dlg_share_deck.h
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_SHARE_DECK_H
|
||||
#define DLG_SHARE_DECK_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QSharedPointer>
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckList;
|
||||
class QDialogButtonBox;
|
||||
class QLineEdit;
|
||||
class QTimer;
|
||||
class Response;
|
||||
|
||||
/**
|
||||
* @brief Slim dialog to create a temporary share for the deck open in the editor.
|
||||
*
|
||||
* Asks for a share name, sends Command_DeckShareCreate for the single inline
|
||||
* deck, and copies the resulting link to the clipboard.
|
||||
*/
|
||||
class DlgShareDeck : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void actShare();
|
||||
void shareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void onShareTimeout();
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
QSharedPointer<DeckList> deck;
|
||||
QLineEdit *nameEdit;
|
||||
QDialogButtonBox *buttonBox;
|
||||
QTimer *shareTimeoutTimer;
|
||||
};
|
||||
|
||||
#endif // DLG_SHARE_DECK_H
|
||||
|
|
@ -28,6 +28,9 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
|
|||
aSaveDeckAs = new QAction(QString(), this);
|
||||
connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs);
|
||||
|
||||
aShareDeck = new QAction(QString(), this);
|
||||
connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck);
|
||||
|
||||
aLoadDeckFromClipboard = new QAction(QString(), this);
|
||||
connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard);
|
||||
|
||||
|
|
@ -96,6 +99,7 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
|
|||
addMenu(loadRecentDeckMenu);
|
||||
addAction(aSaveDeck);
|
||||
addAction(aSaveDeckAs);
|
||||
addAction(aShareDeck);
|
||||
addSeparator();
|
||||
addAction(aLoadDeckFromClipboard);
|
||||
addMenu(editDeckInClipboardMenu);
|
||||
|
|
@ -120,6 +124,7 @@ void DeckEditorMenu::setSaveStatus(bool newStatus)
|
|||
{
|
||||
aSaveDeck->setEnabled(newStatus);
|
||||
aSaveDeckAs->setEnabled(newStatus);
|
||||
aShareDeck->setEnabled(newStatus);
|
||||
aSaveDeckToClipboard->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
|
||||
aSaveDeckToClipboardRaw->setEnabled(newStatus);
|
||||
|
|
@ -157,6 +162,7 @@ void DeckEditorMenu::retranslateUi()
|
|||
aClearRecents->setText(tr("Clear"));
|
||||
aSaveDeck->setText(tr("&Save deck"));
|
||||
aSaveDeckAs->setText(tr("Save deck &as..."));
|
||||
aShareDeck->setText(tr("Share deck..."));
|
||||
|
||||
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ public:
|
|||
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
|
||||
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
|
||||
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
|
||||
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
|
||||
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck,
|
||||
*aClose;
|
||||
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
|
||||
|
||||
void setSaveStatus(bool newStatus);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,10 @@ public:
|
|||
return userInfo;
|
||||
}
|
||||
[[nodiscard]] AbstractClient *getClient() const;
|
||||
[[nodiscard]] AbstractClient *getServerClient() const
|
||||
{
|
||||
return client;
|
||||
}
|
||||
[[nodiscard]] UserListManager *getUserListManager() const
|
||||
{
|
||||
return userListManager;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "deck_preview_deck_tags_display_widget.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QFrame>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
|
|
@ -27,7 +28,7 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||
VisualDeckStorageModel *_model,
|
||||
const QString &_filePath)
|
||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath)
|
||||
: QWidget(_parent), filePath(_filePath), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model)
|
||||
{
|
||||
layout = new QVBoxLayout(this);
|
||||
setLayout(layout);
|
||||
|
|
@ -36,6 +37,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
|
||||
pictureWidget->setFontSize(24);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageSingleClicked, this,
|
||||
&DeckPreviewWidget::imageSingleClicked);
|
||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&DeckPreviewWidget::imageDoubleClickedEvent);
|
||||
bannerCardDisplayWidget = pictureWidget;
|
||||
|
|
@ -99,6 +102,15 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
|||
// to keep the resize handler from searching the widget tree on every layout pass.
|
||||
fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel,
|
||||
bannerCardComboBox};
|
||||
|
||||
// Child of the banner widget so the frame tracks the banner's selection animation
|
||||
// (which animates the banner's position) instead of staying at a stale static offset.
|
||||
selectionFrame = new QFrame(bannerCardDisplayWidget);
|
||||
selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
selectionFrame->setStyleSheet(QStringLiteral(
|
||||
"QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }"));
|
||||
selectionFrame->setVisible(false);
|
||||
bannerCardDisplayWidget->installEventFilter(this);
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::retranslateUi()
|
||||
|
|
@ -122,6 +134,63 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
|||
for (QWidget *widget : fixedWidthChildren) {
|
||||
widget->setMaximumWidth(width);
|
||||
}
|
||||
updateSelectionFrameGeometry();
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == bannerCardDisplayWidget && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) {
|
||||
updateSelectionFrameGeometry();
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setShareSelectable(bool selectable)
|
||||
{
|
||||
shareSelectable = selectable;
|
||||
if (!selectable) {
|
||||
setShareSelected(false);
|
||||
}
|
||||
updateSelectionStyle();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::setShareSelected(bool selected)
|
||||
{
|
||||
if (shareSelected == selected) {
|
||||
return;
|
||||
}
|
||||
shareSelected = selected;
|
||||
updateSelectionStyle();
|
||||
emit shareSelectionToggled(selected);
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::isShareSelected() const
|
||||
{
|
||||
return shareSelected;
|
||||
}
|
||||
|
||||
bool DeckPreviewWidget::isShareSelectable() const
|
||||
{
|
||||
return shareSelectable;
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::updateSelectionFrameGeometry()
|
||||
{
|
||||
if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Frame is a child of the banner, so it is positioned in banner coordinates and
|
||||
// tracks the banner's selection animation automatically. A small inset keeps the
|
||||
// highlight visible around the card art without occluding it.
|
||||
selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1));
|
||||
selectionFrame->raise();
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::updateSelectionStyle()
|
||||
{
|
||||
if (selectionFrame != nullptr) {
|
||||
selectionFrame->setVisible(shareSelectable && isShareSelected());
|
||||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::enterEvent(QEnterEvent *event)
|
||||
|
|
@ -337,10 +406,20 @@ void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPic
|
|||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::imageSingleClicked()
|
||||
{
|
||||
if (isShareSelectable()) {
|
||||
setShareSelected(!isShareSelected());
|
||||
}
|
||||
}
|
||||
|
||||
void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
Q_UNUSED(instance);
|
||||
if (isShareSelectable()) {
|
||||
return; // in share mode a double click would just toggle a single selection
|
||||
}
|
||||
emit deckLoadRequested(filePath);
|
||||
}
|
||||
|
||||
|
|
@ -365,6 +444,9 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
|
|||
}
|
||||
});
|
||||
|
||||
connect(menu->addAction(tr("Share deck...")), &QAction::triggered, this,
|
||||
[this] { emit shareDeckRequested(filePath); });
|
||||
|
||||
connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget,
|
||||
&DeckPreviewDeckTagsDisplayWidget::openTagEditDlg);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include <QWidget>
|
||||
|
||||
class QEnterEvent;
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
class QMenu;
|
||||
class QMouseEvent;
|
||||
|
|
@ -41,9 +42,19 @@ public:
|
|||
*/
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
|
||||
/** @brief The path of the deck file backing this preview. */
|
||||
QString filePath;
|
||||
|
||||
void setShareSelectable(bool selectable);
|
||||
void setShareSelected(bool selected);
|
||||
[[nodiscard]] bool isShareSelected() const;
|
||||
[[nodiscard]] bool isShareSelectable() const;
|
||||
|
||||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
void shareDeckRequested(const QString &filePath);
|
||||
void shareSelectionToggled(bool selected);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
|
|
@ -66,6 +77,7 @@ public slots:
|
|||
protected:
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] int row() const;
|
||||
|
|
@ -76,6 +88,7 @@ private:
|
|||
QMenu *createRightClickMenu();
|
||||
void addSetBannerCardMenu(QMenu *menu);
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageSingleClicked();
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
||||
void actRenameDeck();
|
||||
|
|
@ -84,7 +97,6 @@ private:
|
|||
|
||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
VisualDeckStorageModel *model;
|
||||
QString filePath;
|
||||
QVBoxLayout *layout;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||
|
|
@ -92,6 +104,12 @@ private:
|
|||
QComboBox *bannerCardComboBox;
|
||||
QList<QWidget *> fixedWidthChildren; ///< Children clamped to the picture width on resize.
|
||||
int lastKnownBannerWidth = -1; ///< The picture width last applied to the children.
|
||||
QFrame *selectionFrame = nullptr;
|
||||
bool shareSelectable = false;
|
||||
bool shareSelected = false;
|
||||
|
||||
void updateSelectionStyle();
|
||||
void updateSelectionFrameGeometry();
|
||||
};
|
||||
|
||||
class NoScrollFilter : public QObject
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@ void VisualDeckStorageFolderDisplayWidget::continueDeckPass()
|
|||
|
||||
const bool matches = index.data(VisualDeckStorageRoles::FilterMatchRole).toBool();
|
||||
deckPreviewWidget->setVisible(matches);
|
||||
if (!matches) {
|
||||
// A deck that no longer matches the filters is dropped from the selection so its
|
||||
// highlight cannot linger on an invisible preview or be counted in the share.
|
||||
deckPreviewWidget->setShareSelected(false);
|
||||
}
|
||||
if (matches) {
|
||||
++visibleDeckCount;
|
||||
}
|
||||
|
|
@ -209,6 +214,11 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget
|
|||
&VisualDeckStorageWidget::deckLoadRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::openDeckEditor);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::shareDeckRequested, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::shareDeckRequested);
|
||||
connect(deckPreviewWidget, &DeckPreviewWidget::shareSelectionToggled, visualDeckStorageWidget,
|
||||
&VisualDeckStorageWidget::shareSelectionChanged);
|
||||
deckPreviewWidget->setShareSelectable(visualDeckStorageWidget->isShareSelectable());
|
||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
||||
|
|
@ -216,6 +226,18 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget
|
|||
return deckPreviewWidget;
|
||||
}
|
||||
|
||||
void VisualDeckStorageFolderDisplayWidget::setShareSelectable(bool selectable)
|
||||
{
|
||||
const auto previews = flowWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
preview->setShareSelectable(selectable);
|
||||
}
|
||||
const auto subFolders = findChildren<VisualDeckStorageFolderDisplayWidget *>();
|
||||
for (VisualDeckStorageFolderDisplayWidget *subFolder : subFolders) {
|
||||
subFolder->setShareSelectable(selectable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates, removes and keeps in sync the subfolder widgets of this folder.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ public slots:
|
|||
*/
|
||||
void scheduleReconcile();
|
||||
void updateShowFolders(bool enabled);
|
||||
void setShareSelectable(bool selectable);
|
||||
|
||||
signals:
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "visual_deck_storage_model.h"
|
||||
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../cards/additional_info/deck_color_identity.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
|
|
@ -119,8 +120,6 @@ DeckScanResult scanDeckDirectory(const QString &deckPath)
|
|||
}
|
||||
} // namespace
|
||||
|
||||
static QString computeColorIdentity(const LoadedDeck &deck);
|
||||
|
||||
VisualDeckStorageModel::VisualDeckStorageModel(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
|
@ -306,7 +305,7 @@ void VisualDeckStorageModel::beginLoad(int row)
|
|||
}
|
||||
// Color identity walks every card through the database, so compute it here to
|
||||
// keep the completion handler on the UI thread cheap.
|
||||
const QString colorIdentity = computeColorIdentity(*deck);
|
||||
const QString colorIdentity = getDeckColorIdentity(deck->deckList, CardDatabaseManager::query());
|
||||
return DeckLoadResult{std::move(*deck), QFileInfo(filePath).lastModified(), colorIdentity};
|
||||
}));
|
||||
}
|
||||
|
|
@ -367,40 +366,6 @@ void VisualDeckStorageModel::drainPendingLoads()
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Computes the color identity of a deck in WUBRG order.
|
||||
*/
|
||||
static QString computeColorIdentity(const LoadedDeck &deck)
|
||||
{
|
||||
QStringList cardList = deck.deckList.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
||||
if (cardList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||
|
||||
for (const QString &cardName : cardList) {
|
||||
CardInfoPtr currentCard = CardDatabaseManager::query()->getCardInfo(cardName);
|
||||
if (currentCard) {
|
||||
const QString colors = currentCard->getColors(); // Something like "WUB"
|
||||
for (const QChar &color : colors) {
|
||||
colorSet.insert(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the color identity is in WUBRG order
|
||||
QString colorIdentity;
|
||||
const QString wubrgOrder = "WUBRG";
|
||||
for (const QChar &color : wubrgOrder) {
|
||||
if (colorSet.contains(color)) {
|
||||
colorIdentity.append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return colorIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Recomputes all derived metadata of a row from its loaded deck.
|
||||
*/
|
||||
|
|
@ -414,7 +379,7 @@ void VisualDeckStorageModel::recomputeDeckMetadata(DeckPreviewData &data, bool r
|
|||
data.lastLoaded = QDateTime::fromString(deckList.getLastLoadedTimestamp());
|
||||
data.bannerCard = deckList.getBannerCard();
|
||||
if (recomputeColorIdentity) {
|
||||
data.colorIdentity = computeColorIdentity(data.deck);
|
||||
data.colorIdentity = getDeckColorIdentity(data.deck.deckList, CardDatabaseManager::query());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
refreshButton->setFixedSize(32, 32);
|
||||
connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible);
|
||||
|
||||
shareButton = new QToolButton(this);
|
||||
shareButton->setIcon(themePixmap(QStringLiteral("icons/share")));
|
||||
shareButton->setFixedSize(32, 32);
|
||||
shareButton->setVisible(false);
|
||||
connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested);
|
||||
|
||||
quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this);
|
||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showFoldersChanged, this,
|
||||
&VisualDeckStorageWidget::updateShowFolders);
|
||||
|
|
@ -58,6 +64,7 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
|||
searchAndSortLayout->addWidget(sortWidget);
|
||||
searchAndSortLayout->addWidget(searchWidget);
|
||||
searchAndSortLayout->addWidget(refreshButton);
|
||||
searchAndSortLayout->addWidget(shareButton);
|
||||
searchAndSortLayout->addWidget(quickSettingsWidget);
|
||||
|
||||
// tag filter box
|
||||
|
|
@ -158,11 +165,71 @@ void VisualDeckStorageWidget::retranslateUi()
|
|||
databaseLoadIndicator->setText(tr("Loading database ..."));
|
||||
|
||||
refreshButton->setToolTip(tr("Refresh loaded files"));
|
||||
shareButton->setToolTip(tr("Select decks to share"));
|
||||
quickSettingsWidget->setToolTip(tr("Visual Deck Storage Settings"));
|
||||
|
||||
sortWidget->retranslateUi();
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::setShareSelectable(bool selectable)
|
||||
{
|
||||
if (shareSelectable == selectable) {
|
||||
return;
|
||||
}
|
||||
shareSelectable = selectable;
|
||||
if (folderWidget != nullptr) {
|
||||
folderWidget->setShareSelectable(selectable);
|
||||
}
|
||||
emit shareSelectionChanged();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageWidget::isShareSelectable() const
|
||||
{
|
||||
return shareSelectable;
|
||||
}
|
||||
|
||||
QStringList VisualDeckStorageWidget::selectedFilePaths() const
|
||||
{
|
||||
QStringList selectedPaths;
|
||||
if (folderWidget != nullptr) {
|
||||
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
// Filtered-out previews stay alive hidden in their sorted place, so only decks the
|
||||
// user can actually see are part of the share.
|
||||
if (preview->isVisible() && preview->isShareSelected()) {
|
||||
selectedPaths.append(preview->filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedPaths;
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::clearShareSelection()
|
||||
{
|
||||
if (folderWidget != nullptr) {
|
||||
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
preview->setShareSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::setShareAvailable(bool available)
|
||||
{
|
||||
shareButton->setVisible(available);
|
||||
shareButton->setEnabled(available);
|
||||
}
|
||||
|
||||
void VisualDeckStorageWidget::setShareSelectedFiles(const QStringList &paths)
|
||||
{
|
||||
if (folderWidget != nullptr) {
|
||||
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||
for (DeckPreviewWidget *preview : previews) {
|
||||
preview->setShareSelected(paths.contains(preview->filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a const pointer to the quick settings so that the values can be accessed.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ public:
|
|||
explicit VisualDeckStorageWidget(QWidget *parent);
|
||||
void refreshIfPossible();
|
||||
void retranslateUi();
|
||||
void setShareSelectable(bool selectable);
|
||||
[[nodiscard]] bool isShareSelectable() const;
|
||||
[[nodiscard]] QStringList selectedFilePaths() const;
|
||||
void setShareSelectedFiles(const QStringList &paths);
|
||||
void clearShareSelection();
|
||||
void setShareAvailable(bool available);
|
||||
|
||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||
bool deckPreviewSelectionAnimationEnabled;
|
||||
|
|
@ -63,6 +69,9 @@ public slots:
|
|||
signals:
|
||||
void deckLoadRequested(const QString &filePath);
|
||||
void openDeckEditor(const LoadedDeck &deck);
|
||||
void shareDeckRequested(const QString &filePath);
|
||||
void shareSelectionChanged();
|
||||
void shareRequested();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
|
@ -81,12 +90,14 @@ private:
|
|||
VisualDeckStorageSearchWidget *searchWidget;
|
||||
DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget;
|
||||
QToolButton *refreshButton;
|
||||
QToolButton *shareButton;
|
||||
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
||||
QScrollArea *scrollArea;
|
||||
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
|
||||
VisualDeckStorageModel *storageModel = nullptr;
|
||||
VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr;
|
||||
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
|
||||
bool shareSelectable = false;
|
||||
};
|
||||
|
||||
#endif // VISUAL_DECK_STORAGE_WIDGET_H
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue