mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 02:13:02 -07:00
Compare commits
3 commits
8ee45108b3
...
9ac2b41e3e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ac2b41e3e | ||
|
|
ad2fb40a2a | ||
|
|
90e11e0d63 |
47 changed files with 2653 additions and 63 deletions
|
|
@ -45,12 +45,14 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
||||
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
||||
src/interface/widgets/dialogs/dlg_login_prompt.cpp
|
||||
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
||||
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||
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_shared_decks_preview.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
|
||||
|
|
@ -59,6 +61,7 @@ set(cockatrice_SOURCES
|
|||
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/shared_deck_preview_widget.cpp
|
||||
src/interface/widgets/deck_share/share_bar_widget.cpp
|
||||
src/filters/deck_filter_string.cpp
|
||||
src/filters/filter_builder.cpp
|
||||
|
|
@ -319,6 +322,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/deck_preview/public_deck_preview_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/remote_public_decks_model.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_model.cpp
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp
|
||||
|
|
@ -378,6 +383,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/api/edhrec/display/top_tags/edhrec_top_tags_api_response_display_widget.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/tab_edhrec.cpp
|
||||
src/interface/widgets/tabs/api/edhrec/tab_edhrec_main.cpp
|
||||
src/interface/widgets/tabs/public_decks_quick_settings_widget.cpp
|
||||
src/interface/widgets/tabs/tab.cpp
|
||||
src/interface/widgets/tabs/tab_account.cpp
|
||||
src/interface/widgets/tabs/tab_admin.cpp
|
||||
|
|
@ -389,6 +395,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_logs.cpp
|
||||
src/interface/widgets/tabs/tab_message.cpp
|
||||
src/interface/widgets/tabs/tab_moderation.cpp
|
||||
src/interface/widgets/tabs/tab_public_decks.cpp
|
||||
src/interface/widgets/tabs/tab_report.cpp
|
||||
src/interface/widgets/tabs/tab_replays.cpp
|
||||
src/interface/widgets/tabs/tab_room.cpp
|
||||
|
|
@ -444,6 +451,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/intents/intent_login.h
|
||||
src/interface/intents/intent_open_server_room_by_name.cpp
|
||||
src/interface/intents/intent_open_server_room_by_name.h
|
||||
src/interface/intents/intent_open_shared_deck.cpp
|
||||
src/interface/intents/intent_open_shared_deck.h
|
||||
src/interface/intents/url_parser.cpp
|
||||
src/interface/intents/url_parser.h
|
||||
src/interface/widgets/server/user/user_info_popup.cpp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef COCKATRICE_CONTEXT_OPEN_DECK_H
|
||||
#define COCKATRICE_CONTEXT_OPEN_DECK_H
|
||||
|
||||
#include "context_connect_to_server.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct ContextOpenDeck
|
||||
{
|
||||
ContextConnectToServer serverContext;
|
||||
QString shareToken;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CONTEXT_OPEN_DECK_H
|
||||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
Intent::Intent(QObject *parent) : QObject(parent)
|
||||
{
|
||||
// An intent is done as soon as it reports success or failure. Deleting it
|
||||
// also tears down its dependency chain and disconnects any signal wiring.
|
||||
// An intent is done as soon as it reports success, failure, or cancellation.
|
||||
// Deleting it also tears down its dependency chain and disconnects any signal wiring.
|
||||
connect(this, &Intent::finished, this, &QObject::deleteLater);
|
||||
connect(this, &Intent::failed, this, &QObject::deleteLater);
|
||||
connect(this, &Intent::cancelled, this, &QObject::deleteLater);
|
||||
}
|
||||
|
||||
Intent::~Intent() = default;
|
||||
|
|
@ -46,3 +47,11 @@ void Intent::emitFailed(const QString &reason)
|
|||
emit failed(reason);
|
||||
}
|
||||
}
|
||||
|
||||
void Intent::emitCancelled()
|
||||
{
|
||||
if (!completed) {
|
||||
completed = true;
|
||||
emit cancelled();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public:
|
|||
signals:
|
||||
void finished();
|
||||
void failed(QString reason);
|
||||
void cancelled();
|
||||
|
||||
protected:
|
||||
// --- Subclasses must implement these ---
|
||||
|
|
@ -29,6 +30,7 @@ protected:
|
|||
// Emit the outcome exactly once; ignore late signals after the intent is done.
|
||||
void emitFinished();
|
||||
void emitFailed(const QString &reason);
|
||||
void emitCancelled();
|
||||
|
||||
private:
|
||||
bool completed = false;
|
||||
|
|
|
|||
|
|
@ -19,13 +19,10 @@ bool IntentJoinServerGame::checkPrecondition() const
|
|||
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
// peerPort() reflects the actual TCP peer, which may differ from the
|
||||
// configured server port (e.g. when connecting through a proxy), so only
|
||||
// the hostname is compared here.
|
||||
if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
|
||||
return false;
|
||||
}
|
||||
if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
|
||||
// serverName() reflects the server the client was configured to connect to,
|
||||
// which may differ from the actual TCP peer (e.g. when connecting through a
|
||||
// proxy), so only the hostname is compared here.
|
||||
if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
#include "intent_login.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../widgets/dialogs/dlg_login_prompt.h"
|
||||
#include "libcockatrice/settings/servers_settings.h"
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
|
||||
{
|
||||
}
|
||||
|
|
@ -29,5 +32,27 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
|
|||
|
||||
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
|
||||
{
|
||||
emitFailed(tr("No saved credentials for this server"));
|
||||
// No credentials saved for the target server: ask the user for them. They
|
||||
// opt into saving them so later links to the same server connect directly.
|
||||
const QString serverText = context->hostname + ":" + context->port;
|
||||
DlgLoginPrompt dialog(serverText);
|
||||
// ApplicationModal: the dialog has no parent (the intent is not a widget),
|
||||
// so WindowModal would not actually block any other window.
|
||||
dialog.setWindowModality(Qt::ApplicationModal);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
emitCancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
context->username = dialog.username();
|
||||
context->password = dialog.password();
|
||||
|
||||
if (dialog.savePassword() && !context->username.isEmpty()) {
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
servers.addNewServer(context->hostname, context->hostname, context->port, context->username, context->password,
|
||||
true);
|
||||
}
|
||||
|
||||
emitFinished();
|
||||
}
|
||||
|
|
|
|||
190
cockatrice/src/interface/intents/intent_open_shared_deck.cpp
Normal file
190
cockatrice/src/interface/intents/intent_open_shared_deck.cpp
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
#include "intent_open_shared_deck.h"
|
||||
|
||||
#include "../deck_loader/deck_loader.h"
|
||||
#include "../widgets/dialogs/dlg_shared_decks_preview.h"
|
||||
#include "../widgets/tabs/tab_supervisor.h"
|
||||
#include "intent_connect_to_server.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
IntentOpenSharedDeck::IntentOpenSharedDeck(TabSupervisor *_tabSupervisor,
|
||||
RemoteClient *_remoteClient,
|
||||
const CardDatabaseQuerier *_querier,
|
||||
std::unique_ptr<ContextOpenDeck> _context)
|
||||
: Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), querier(_querier),
|
||||
context(_context.release())
|
||||
{
|
||||
downloadTimer = new QTimer(this);
|
||||
downloadTimer->setSingleShot(true);
|
||||
downloadTimer->setInterval(15000);
|
||||
connect(downloadTimer, &QTimer::timeout, this, &IntentOpenSharedDeck::onDownloadTimeout);
|
||||
}
|
||||
|
||||
bool IntentOpenSharedDeck::checkPrecondition() const
|
||||
{
|
||||
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
// serverName() reflects the server the client was configured to connect to,
|
||||
// which may differ from the actual TCP peer (e.g. when connecting through a
|
||||
// proxy), so only the hostname is compared here.
|
||||
return remoteClient->serverName().compare(context->serverContext.hostname, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::onPreconditionSatisfied()
|
||||
{
|
||||
// Resolve the share token to its items first; a share can contain more than
|
||||
// one deck, and each item is downloaded by id.
|
||||
Command_DeckShareList cmd;
|
||||
cmd.set_token(context->shareToken.toStdString());
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::listShareFinished);
|
||||
remoteClient->sendCommand(pend);
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::onPreconditionNotSatisfied()
|
||||
{
|
||||
runDependency(new IntentConnectToServer(remoteClient, &context->serverContext));
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
emitFailed(tr("The shared deck could not be found or has expired"));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_DeckShareList &resp = response.GetExtension(Response_DeckShareList::ext);
|
||||
if (resp.items_size() == 0) {
|
||||
emitFailed(tr("The shared deck is empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
QList<ServerInfo_DeckShareItem> items;
|
||||
items.reserve(resp.items_size());
|
||||
for (const ServerInfo_DeckShareItem &item : resp.items()) {
|
||||
items.append(item);
|
||||
itemNames.insert(item.id(), QString::fromStdString(item.name()));
|
||||
}
|
||||
|
||||
const QString serverText = context->serverContext.hostname + ":" + context->serverContext.port;
|
||||
|
||||
// Ask the user which decks to open before downloading anything.
|
||||
previewDialog = new DlgSharedDecksPreview(tabSupervisor, querier, QString::fromStdString(resp.name()),
|
||||
resp.expires_at(), serverText, items);
|
||||
connect(previewDialog, &DlgSharedDecksPreview::openRequested, this, &IntentOpenSharedDeck::startDownloads);
|
||||
connect(previewDialog, &DlgSharedDecksPreview::cancelled, this, &IntentOpenSharedDeck::emitCancelled);
|
||||
connect(previewDialog, &DlgSharedDecksPreview::cancelled, previewDialog, &QWidget::deleteLater);
|
||||
previewDialog->show();
|
||||
previewDialog->raise();
|
||||
previewDialog->activateWindow();
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::startDownloads(const QList<int> &itemIds)
|
||||
{
|
||||
pendingItemIds = itemIds;
|
||||
totalItems = itemIds.size();
|
||||
completedItems = 0;
|
||||
loadedDecks.clear();
|
||||
downloadNextItem();
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::downloadNextItem()
|
||||
{
|
||||
if (pendingItemIds.isEmpty()) {
|
||||
finishAll();
|
||||
return;
|
||||
}
|
||||
|
||||
currentItemId = pendingItemIds.takeFirst();
|
||||
downloadTimer->start();
|
||||
|
||||
Command_DeckShareDownload cmd;
|
||||
cmd.set_token(context->shareToken.toStdString());
|
||||
cmd.set_item_id(currentItemId);
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &IntentOpenSharedDeck::downloadShareFinished);
|
||||
remoteClient->sendCommand(pend);
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::downloadShareFinished(const Response &response,
|
||||
const CommandContainer & /* commandContainer */)
|
||||
{
|
||||
downloadTimer->stop();
|
||||
|
||||
QString failureReason;
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
failureReason = tr("Failed to download the shared deck");
|
||||
} else {
|
||||
const Response_DeckShareDownload &resp = response.GetExtension(Response_DeckShareDownload::ext);
|
||||
const QString deckString = QString::fromStdString(resp.deck());
|
||||
if (deckString.isEmpty()) {
|
||||
failureReason = tr("The shared deck is empty");
|
||||
} else {
|
||||
std::optional<LoadedDeck> deckOpt =
|
||||
DeckLoader::loadFromRemote(deckString, LoadedDeck::LoadInfo::NON_REMOTE_ID);
|
||||
if (!deckOpt) {
|
||||
failureReason = tr("The shared deck could not be loaded");
|
||||
} else {
|
||||
loadedDecks.append(deckOpt.value());
|
||||
++completedItems;
|
||||
previewDialog->setDownloadProgress(completedItems, totalItems,
|
||||
itemNames.value(currentItemId, tr("Unknown deck")));
|
||||
downloadNextItem();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onItemFailure(failureReason);
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::onItemFailure(const QString &reason)
|
||||
{
|
||||
downloadTimer->stop();
|
||||
|
||||
if (loadedDecks.isEmpty()) {
|
||||
previewDialog->deleteLater();
|
||||
emitFailed(reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const int downloadedCount = loadedDecks.size();
|
||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
||||
previewDialog, tr("Open shared decks"),
|
||||
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount)
|
||||
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||
|
||||
if (answer == QMessageBox::Yes) {
|
||||
finishAll();
|
||||
} else {
|
||||
previewDialog->deleteLater();
|
||||
emitCancelled();
|
||||
}
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::onDownloadTimeout()
|
||||
{
|
||||
onItemFailure(tr("Timed out while downloading the shared deck"));
|
||||
}
|
||||
|
||||
void IntentOpenSharedDeck::finishAll()
|
||||
{
|
||||
previewDialog->deleteLater();
|
||||
for (const LoadedDeck &deck : loadedDecks) {
|
||||
tabSupervisor->openDeckInNewTab(deck);
|
||||
}
|
||||
emitFinished();
|
||||
}
|
||||
59
cockatrice/src/interface/intents/intent_open_shared_deck.h
Normal file
59
cockatrice/src/interface/intents/intent_open_shared_deck.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H
|
||||
#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H
|
||||
|
||||
#include "contexts/context_open_deck.h"
|
||||
#include "intent.h"
|
||||
#include "remote_client.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QScopedPointer>
|
||||
#include <memory>
|
||||
|
||||
class TabSupervisor;
|
||||
struct LoadedDeck;
|
||||
class CardDatabaseQuerier;
|
||||
class DlgSharedDecksPreview;
|
||||
class QTimer;
|
||||
|
||||
class IntentOpenSharedDeck : public Intent
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
IntentOpenSharedDeck(TabSupervisor *_tabSupervisor,
|
||||
RemoteClient *_remoteClient,
|
||||
const CardDatabaseQuerier *_querier,
|
||||
std::unique_ptr<ContextOpenDeck> _context);
|
||||
|
||||
protected:
|
||||
bool checkPrecondition() const override;
|
||||
void onPreconditionSatisfied() override;
|
||||
void onPreconditionNotSatisfied() override;
|
||||
|
||||
private slots:
|
||||
void listShareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void downloadShareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void onDownloadTimeout();
|
||||
|
||||
private:
|
||||
void startDownloads(const QList<int> &itemIds);
|
||||
void downloadNextItem();
|
||||
void onItemFailure(const QString &reason);
|
||||
void finishAll();
|
||||
|
||||
TabSupervisor *tabSupervisor;
|
||||
RemoteClient *remoteClient;
|
||||
const CardDatabaseQuerier *querier;
|
||||
QScopedPointer<ContextOpenDeck> context;
|
||||
DlgSharedDecksPreview *previewDialog = nullptr;
|
||||
QTimer *downloadTimer;
|
||||
QMap<int, QString> itemNames;
|
||||
QList<int> pendingItemIds;
|
||||
QList<LoadedDeck> loadedDecks;
|
||||
int currentItemId = 0;
|
||||
int totalItems = 0;
|
||||
int completedItems = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTENT_OPEN_SHARED_DECK_H
|
||||
|
|
@ -1,19 +1,28 @@
|
|||
#include "url_parser.h"
|
||||
|
||||
#include "../../client/settings/cache_settings.h"
|
||||
#include "../widgets/tabs/tab_room.h"
|
||||
#include "../widgets/tabs/tab_supervisor.h"
|
||||
#include "../window_main.h"
|
||||
#include "contexts/context_join_game.h"
|
||||
#include "contexts/context_open_deck.h"
|
||||
#include "intent.h"
|
||||
#include "intent_join_server_game.h"
|
||||
#include "intent_login.h"
|
||||
#include "intent_open_shared_deck.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMessageBox>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/settings/servers_settings.h>
|
||||
#include <memory>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(UrlParserLog, "url_parser");
|
||||
|
||||
IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
|
||||
{
|
||||
}
|
||||
|
|
@ -29,16 +38,33 @@ void IntentUrlParser::handle(const QString &urlStr)
|
|||
const QString action = url.host();
|
||||
QUrlQuery query(url);
|
||||
|
||||
qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action;
|
||||
|
||||
QList<Intent *> chain;
|
||||
Intent *firstIntent = nullptr;
|
||||
if (action == "joingame") {
|
||||
handleJoinGame(query);
|
||||
firstIntent = createJoinGameIntent(query, chain);
|
||||
} else if (action == "opendeck") {
|
||||
// handleOpenDeck(query);
|
||||
firstIntent = createOpenDeckIntent(query, chain);
|
||||
} else {
|
||||
qWarning() << "Unknown intent:" << action;
|
||||
}
|
||||
|
||||
if (firstIntent == nullptr) {
|
||||
// The link was invalid or the user declined the confirm: nothing runs.
|
||||
// Report the idle state when no other chain is queued so that a startup
|
||||
// launch (which skipped its own connection for this URL) falls back to it.
|
||||
if (!chainRunning && pendingChains.isEmpty()) {
|
||||
emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pendingChains.append(chain);
|
||||
startNextChain();
|
||||
}
|
||||
|
||||
void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
||||
Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, QList<Intent *> &chain)
|
||||
{
|
||||
auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); };
|
||||
|
||||
|
|
@ -49,21 +75,21 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
|||
|
||||
if (ctx->roomContext.serverContext.hostname.isEmpty()) {
|
||||
showError(tr("Missing or empty hostname in the game link"));
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
ctx->roomContext.serverContext.port.toUShort(&ok);
|
||||
if (!ok) {
|
||||
showError(tr("Invalid or missing port in the game link"));
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
|
||||
|
||||
if (!ok) {
|
||||
showError(tr("Invalid or missing room id in the game link"));
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ok = false;
|
||||
|
|
@ -71,7 +97,7 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
|||
|
||||
if (!ok) {
|
||||
showError(tr("Invalid or missing game id in the game link"));
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
|
||||
|
|
@ -80,24 +106,32 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
|||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
||||
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RemoteClient *client = mainWindow->getRemoteClient();
|
||||
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
|
||||
|
||||
// The join game intent owns the context and the credential lookup; once the
|
||||
// chain finishes (or fails) it deletes the whole tree.
|
||||
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
|
||||
auto joinGameIntent =
|
||||
new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx));
|
||||
auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), client, std::move(ctx));
|
||||
joinGameIntent->setParent(this);
|
||||
|
||||
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
|
||||
getLoginCredentialsIntent->setParent(joinGameIntent);
|
||||
|
||||
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
|
||||
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
|
||||
chain.append(joinGameIntent);
|
||||
connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
|
||||
|
||||
getLoginCredentialsIntent->execute();
|
||||
Intent *firstIntent = joinGameIntent;
|
||||
if (!isConnectedTo(serverContext->hostname, serverContext->port)) {
|
||||
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
|
||||
getLoginCredentialsIntent->setParent(joinGameIntent);
|
||||
chain.insert(0, getLoginCredentialsIntent);
|
||||
|
||||
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
|
||||
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
|
||||
connect(getLoginCredentialsIntent, &Intent::cancelled, joinGameIntent, &Intent::cancelled);
|
||||
firstIntent = getLoginCredentialsIntent;
|
||||
}
|
||||
|
||||
return firstIntent;
|
||||
}
|
||||
|
||||
QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription)
|
||||
|
|
@ -134,3 +168,221 @@ QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context,
|
|||
.arg(gameDescription, gameIdStr, roomTab->getRoomName(), server)
|
||||
: tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server);
|
||||
}
|
||||
|
||||
Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, QList<Intent *> &chain)
|
||||
{
|
||||
auto showError = [this](const QString &message) {
|
||||
QMessageBox::warning(mainWindow, tr("Open shared deck"), message);
|
||||
};
|
||||
|
||||
auto ctx = std::make_unique<ContextOpenDeck>();
|
||||
|
||||
ctx->serverContext.hostname = query.queryItemValue("hostname");
|
||||
ctx->serverContext.port = query.queryItemValue("port");
|
||||
ctx->shareToken = query.queryItemValue("share");
|
||||
|
||||
qCDebug(UrlParserLog) << "Open-deck intent: host" << ctx->serverContext.hostname << "port"
|
||||
<< ctx->serverContext.port << "token length" << ctx->shareToken.length();
|
||||
|
||||
if (ctx->serverContext.hostname.isEmpty()) {
|
||||
showError(tr("Missing or empty hostname in the share link"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const quint16 port = ctx->serverContext.port.toUShort(&ok);
|
||||
if (!ok || port == 0) {
|
||||
showError(tr("Invalid or missing port in the share link"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (ctx->shareToken.isEmpty()) {
|
||||
showError(tr("Missing or empty share value in the share link"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RemoteClient *client = mainWindow->getRemoteClient();
|
||||
|
||||
// When the link would move us away from a live session, ask first — the
|
||||
// open deck download needs the connection the user already has. Remember
|
||||
// the current session so a failed or cancelled chain can restore it.
|
||||
const bool migrating =
|
||||
client->getStatus() == StatusLoggedIn && !isConnectedTo(ctx->serverContext.hostname, ctx->serverContext.port);
|
||||
if (migrating) {
|
||||
const QString target = QStringLiteral("%1:%2").arg(ctx->serverContext.hostname, ctx->serverContext.port);
|
||||
const QString current =
|
||||
QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort()));
|
||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
||||
mainWindow, tr("Open shared deck"),
|
||||
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return nullptr;
|
||||
}
|
||||
migrationTargetHost = ctx->serverContext.hostname;
|
||||
migrationTargetPort = ctx->serverContext.port;
|
||||
previousServerHost = client->serverName();
|
||||
previousServerPort = QString::number(client->serverPort());
|
||||
pendingRestore = true;
|
||||
}
|
||||
|
||||
ContextConnectToServer *serverContext = &ctx->serverContext;
|
||||
|
||||
// The open deck intent owns the context and the credential lookup; once
|
||||
// the chain finishes (or fails) it deletes the whole tree.
|
||||
auto openDeckIntent =
|
||||
new IntentOpenSharedDeck(mainWindow->getTabSupervisor(), client, CardDatabaseManager::query(), std::move(ctx));
|
||||
openDeckIntent->setParent(this);
|
||||
chain.append(openDeckIntent);
|
||||
connect(openDeckIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
|
||||
|
||||
Intent *firstIntent = openDeckIntent;
|
||||
if (!isConnectedTo(serverContext->hostname, serverContext->port)) {
|
||||
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
|
||||
getLoginCredentialsIntent->setParent(openDeckIntent);
|
||||
chain.insert(0, getLoginCredentialsIntent);
|
||||
|
||||
connect(getLoginCredentialsIntent, &Intent::finished, openDeckIntent, &Intent::execute);
|
||||
connect(getLoginCredentialsIntent, &Intent::failed, openDeckIntent, &Intent::failed);
|
||||
connect(getLoginCredentialsIntent, &Intent::cancelled, openDeckIntent, &Intent::cancelled);
|
||||
firstIntent = getLoginCredentialsIntent;
|
||||
}
|
||||
|
||||
return firstIntent;
|
||||
}
|
||||
|
||||
bool IntentUrlParser::isConnectedTo(const QString &hostname, const QString &port) const
|
||||
{
|
||||
Q_UNUSED(port);
|
||||
// Deliberately hostname-only (no port): the intents' preconditions apply the
|
||||
// same rule, so a link to the same host on another port still connects
|
||||
// rather than silently reusing an existing session on a different server.
|
||||
RemoteClient *client = mainWindow->getRemoteClient();
|
||||
return client->getStatus() == StatusLoggedIn && client->serverName().compare(hostname, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
void IntentUrlParser::startNextChain()
|
||||
{
|
||||
if (chainRunning || pendingChains.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
chainRunning = true;
|
||||
currentChainSucceeded = false;
|
||||
|
||||
const QList<Intent *> chain = pendingChains.takeFirst();
|
||||
if (chain.isEmpty()) {
|
||||
chainRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the last intent completes the chain; its terminal signal ends the
|
||||
// whole run. Cancellation of an intermediate intent (e.g. declined login
|
||||
// prompt) is forwarded onto the last intent in the chain builders above.
|
||||
Intent *finalIntent = chain.last();
|
||||
connect(finalIntent, &Intent::finished, this, [this]() {
|
||||
currentChainSucceeded = true;
|
||||
chainEnded();
|
||||
});
|
||||
connect(finalIntent, &Intent::failed, this, &IntentUrlParser::chainEnded);
|
||||
connect(finalIntent, &Intent::cancelled, this, &IntentUrlParser::chainEnded);
|
||||
|
||||
chain.first()->execute();
|
||||
}
|
||||
|
||||
void IntentUrlParser::chainEnded()
|
||||
{
|
||||
chainRunning = false;
|
||||
|
||||
// Only a failed or cancelled chain restores the session the link migrated
|
||||
// away from; a successful one leaves the user where they are.
|
||||
if (pendingRestore && !currentChainSucceeded) {
|
||||
restorePreviousServer();
|
||||
}
|
||||
pendingRestore = false;
|
||||
|
||||
startNextChain();
|
||||
|
||||
// Only report the terminal state once the queue has fully drained, so a
|
||||
// queued follow-up link keeps the startup fallback out of the picture.
|
||||
if (!chainRunning && pendingChains.isEmpty()) {
|
||||
emit urlChainFinished(mainWindow->getRemoteClient()->getStatus() == StatusLoggedIn);
|
||||
}
|
||||
}
|
||||
|
||||
void IntentUrlParser::restorePreviousServer()
|
||||
{
|
||||
if (previousServerHost.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RemoteClient *client = mainWindow->getRemoteClient();
|
||||
const ClientStatus status = client->getStatus();
|
||||
|
||||
// A failed/cancelled chain can fire while the client is still settling the
|
||||
// in-flight connection attempt (wrong password, connect timeout). Only
|
||||
// decide once the client has settled into logged-in or disconnected;
|
||||
// deciding mid-connect would strand the user offline from their previous
|
||||
// server.
|
||||
if (status == StatusDisconnected || status == StatusLoggedIn) {
|
||||
restoreToPreviousServer();
|
||||
return;
|
||||
}
|
||||
auto waitConnection = std::make_shared<QMetaObject::Connection>();
|
||||
*waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, client, waitConnection]() {
|
||||
const ClientStatus settled = client->getStatus();
|
||||
if (settled == StatusDisconnected || settled == StatusLoggedIn) {
|
||||
QObject::disconnect(*waitConnection);
|
||||
restoreToPreviousServer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IntentUrlParser::restoreToPreviousServer()
|
||||
{
|
||||
RemoteClient *client = mainWindow->getRemoteClient();
|
||||
|
||||
// Back on the previous server already → nothing to undo.
|
||||
if (client->serverName().compare(previousServerHost, Qt::CaseInsensitive) == 0 &&
|
||||
QString::number(client->serverPort()) == previousServerPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When logged in somewhere, only intervene if that somewhere is the server
|
||||
// the link moved us to; if the user went elsewhere on their own, leave them.
|
||||
if (client->getStatus() == StatusLoggedIn) {
|
||||
const bool onMigrationTarget = client->serverName().compare(migrationTargetHost, Qt::CaseInsensitive) == 0 &&
|
||||
QString::number(client->serverPort()) == migrationTargetPort;
|
||||
if (!onMigrationTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
const int index = servers.findServerIndex(previousServerHost, previousServerPort);
|
||||
if (index >= 0 && servers.hasLoginData(previousServerHost, previousServerPort)) {
|
||||
const QString username =
|
||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
||||
const QString password =
|
||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
||||
client->connectToServer(previousServerHost, previousServerPort.toUInt(), username, password);
|
||||
return;
|
||||
}
|
||||
client->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->getStatus() != StatusDisconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The link's connection attempt failed: reconnect to the previous server
|
||||
// when credentials are saved, otherwise stay offline.
|
||||
ServersSettings &servers = SettingsCache::instance().servers();
|
||||
const int index = servers.findServerIndex(previousServerHost, previousServerPort);
|
||||
if (index >= 0 && servers.hasLoginData(previousServerHost, previousServerPort)) {
|
||||
const QString username =
|
||||
servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
||||
const QString password =
|
||||
servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
||||
client->connectToServer(previousServerHost, previousServerPort.toUInt(), username, password);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
#ifndef COCKATRICE_URL_PARSER_H
|
||||
#define COCKATRICE_URL_PARSER_H
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QUrlQuery>
|
||||
|
||||
class Intent;
|
||||
class MainWindow;
|
||||
struct ContextJoinGame;
|
||||
|
||||
/**
|
||||
* @brief Parses cockatrice:// links and runs them as serialized intent chains.
|
||||
*
|
||||
* Links are parsed by action (joingame/opendeck) and translated into an intent
|
||||
* chain. Chains are queued and run one at a time: a document can hand multiple
|
||||
* links to the window while an earlier chain still connects, and running two
|
||||
* connect chains concurrently tears the connection down. urlChainFinished is
|
||||
* emitted once the queue has fully drained.
|
||||
*/
|
||||
class IntentUrlParser : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -12,12 +24,34 @@ class IntentUrlParser : public QObject
|
|||
public:
|
||||
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
|
||||
void handle(const QString &urlStr);
|
||||
void handleJoinGame(const QUrlQuery &query);
|
||||
|
||||
signals:
|
||||
/** @brief Emitted when the last queued chain ended; carries whether the client is logged in. */
|
||||
void urlChainFinished(bool connected);
|
||||
|
||||
private:
|
||||
Intent *createJoinGameIntent(const QUrlQuery &query, QList<Intent *> &chain);
|
||||
Intent *createOpenDeckIntent(const QUrlQuery &query, QList<Intent *> &chain);
|
||||
QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription);
|
||||
[[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const;
|
||||
void startNextChain();
|
||||
void chainEnded();
|
||||
void restorePreviousServer();
|
||||
void restoreToPreviousServer();
|
||||
|
||||
MainWindow *mainWindow;
|
||||
QList<QList<Intent *>> pendingChains;
|
||||
bool chainRunning = false;
|
||||
bool currentChainSucceeded = false;
|
||||
|
||||
// Set when an open-deck link migrates the session to another server. If the
|
||||
// chain then fails or is cancelled while still on that server, the previous
|
||||
// session is restored (reconnect if credentials are saved, else disconnect).
|
||||
QString migrationTargetHost;
|
||||
QString migrationTargetPort;
|
||||
QString previousServerHost;
|
||||
QString previousServerPort;
|
||||
bool pendingRestore = false;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_URL_PARSER_H
|
||||
|
|
|
|||
|
|
@ -133,12 +133,14 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
|||
path.addRoundedRect(glowRect, radius, radius);
|
||||
|
||||
// Soft outer glow
|
||||
QColor glowColor(0, 150, 255, 80); // subtle blu
|
||||
QColor glowColor = palette().color(QPalette::Highlight);
|
||||
glowColor.setAlpha(80);
|
||||
painter.setPen(QPen(glowColor, 6));
|
||||
painter.drawPath(path);
|
||||
|
||||
// Thin inner border for crispness
|
||||
QColor borderColor(0, 150, 255, 200);
|
||||
QColor borderColor = palette().color(QPalette::Highlight);
|
||||
borderColor.setAlpha(200);
|
||||
painter.setPen(QPen(borderColor, 2));
|
||||
painter.drawRoundedRect(pixmapRect, radius, radius);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,14 +27,16 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
|||
const QColor &textColor,
|
||||
const QColor &outlineColor,
|
||||
const int fontSize,
|
||||
const Qt::Alignment alignment)
|
||||
const Qt::Alignment alignment,
|
||||
const bool _emitClickImmediately)
|
||||
: CardInfoPictureWithTextOverlayWidget(parent,
|
||||
hoverToZoomEnabled,
|
||||
raiseOnEnter,
|
||||
textColor,
|
||||
outlineColor,
|
||||
fontSize,
|
||||
alignment)
|
||||
alignment),
|
||||
emitClickImmediately(_emitClickImmediately)
|
||||
{
|
||||
singleClickTimer = new QTimer(this);
|
||||
singleClickTimer->setSingleShot(true);
|
||||
|
|
@ -50,8 +52,13 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
|||
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
lastMouseEvent = event;
|
||||
singleClickTimer->start(QApplication::doubleClickInterval());
|
||||
if (emitClickImmediately) {
|
||||
emit imageClicked(event, this);
|
||||
emit imageSingleClicked();
|
||||
} else {
|
||||
lastMouseEvent = event;
|
||||
singleClickTimer->start(QApplication::doubleClickInterval());
|
||||
}
|
||||
} else {
|
||||
emit imageClicked(event, this);
|
||||
event->accept();
|
||||
|
|
@ -61,7 +68,14 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
|||
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
singleClickTimer->stop(); // Prevent single-click logic
|
||||
emit imageDoubleClicked(lastMouseEvent, this);
|
||||
if (emitClickImmediately) {
|
||||
// Do not report a second single click for the second press of the
|
||||
// double-click; the consumer maps the double-click to select+open.
|
||||
lastMouseEvent = event;
|
||||
emit imageDoubleClicked(event, this);
|
||||
} else {
|
||||
singleClickTimer->stop(); // Prevent single-click logic
|
||||
emit imageDoubleClicked(lastMouseEvent, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,28 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a DeckPreviewCardPictureWidget.
|
||||
* @param parent The parent widget.
|
||||
* @param hoverToZoomEnabled If this widget will spawn a larger widget when hovered over.
|
||||
* @param raiseOnEnter If the widget raises its border when the mouse enters.
|
||||
* @param textColor The color of the overlay text.
|
||||
* @param outlineColor The color of the outline around the text.
|
||||
* @param fontSize The font size of the overlay text.
|
||||
* @param alignment The alignment of the text within the overlay.
|
||||
* @param emitClickImmediately If true, a left click is reported immediately on click
|
||||
* instead of after the double-click interval. Use this for selection surfaces
|
||||
* where reacting to a double-click (select-and-open) would needlessly delay the
|
||||
* single-click feedback. The double-click signal is still emitted.
|
||||
*/
|
||||
explicit DeckPreviewCardPictureWidget(QWidget *parent,
|
||||
bool hoverToZoomEnabled = false,
|
||||
bool raiseOnEnter = false,
|
||||
const QColor &textColor = Qt::white,
|
||||
const QColor &outlineColor = Qt::black,
|
||||
int fontSize = 12,
|
||||
Qt::Alignment alignment = Qt::AlignCenter);
|
||||
Qt::Alignment alignment = Qt::AlignCenter,
|
||||
bool _emitClickImmediately = false);
|
||||
|
||||
signals:
|
||||
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
|
|
@ -36,6 +51,7 @@ signals:
|
|||
private:
|
||||
QTimer *singleClickTimer;
|
||||
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
|
||||
bool emitClickImmediately;
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
#include "shared_deck_preview_widget.h"
|
||||
|
||||
#include "../cards/additional_info/color_identity_widget.h"
|
||||
#include "../cards/deck_preview_card_picture_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
|
||||
SharedDeckPreviewWidget::SharedDeckPreviewWidget(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &deckName,
|
||||
const QString &bannerCardName,
|
||||
const QString &colorIdentity,
|
||||
const QString &gameFormat,
|
||||
const QString &deckToolTip)
|
||||
: QWidget(parent)
|
||||
{
|
||||
bannerCardDisplayWidget =
|
||||
new DeckPreviewCardPictureWidget(this, false, false, Qt::white, Qt::black, 12, Qt::AlignCenter, true);
|
||||
bannerCardDisplayWidget->setScaleFactor(100);
|
||||
const ExactCard bannerCard = bannerCardName.isEmpty() ? ExactCard() : querier->getCard(CardRef{bannerCardName, {}});
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
bannerCardDisplayWidget->setOverlayText(deckName);
|
||||
setToolTip(deckToolTip.isEmpty() ? deckName : deckToolTip);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setBaseAccessibleName(deckName);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this, colorIdentity);
|
||||
colorIdentityWidget->setVisible(!colorIdentity.isEmpty());
|
||||
|
||||
gameFormatLabel = new QLabel(gameFormat, this);
|
||||
gameFormatLabel->setAlignment(Qt::AlignCenter);
|
||||
gameFormatLabel->setVisible(!gameFormat.isEmpty());
|
||||
|
||||
selectionCheckBox = new QCheckBox(this);
|
||||
selectionCheckBox->setToolTip(tr("Select this deck"));
|
||||
// The tile itself is focusable (Space/Enter toggles); keep the checkbox
|
||||
// from creating a second tab stop per tile.
|
||||
selectionCheckBox->setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
// Selection frame reused from the deck-preview selection covenant: a
|
||||
// palette(highlight) border around the banner card, shown while selected.
|
||||
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);
|
||||
|
||||
auto *selectionRow = new QHBoxLayout;
|
||||
selectionRow->addWidget(selectionCheckBox);
|
||||
selectionRow->addStretch(1);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addLayout(selectionRow);
|
||||
layout->addWidget(bannerCardDisplayWidget, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(colorIdentityWidget, 0, Qt::AlignHCenter);
|
||||
layout->addWidget(gameFormatLabel, 0, Qt::AlignHCenter);
|
||||
setLayout(layout);
|
||||
|
||||
connect(selectionCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
|
||||
updateSelectionVisual(checked);
|
||||
emit selectionToggled(checked);
|
||||
});
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
|
||||
&SharedDeckPreviewWidget::toggleSelection);
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&SharedDeckPreviewWidget::activate);
|
||||
}
|
||||
|
||||
bool SharedDeckPreviewWidget::isSelected() const
|
||||
{
|
||||
return selectionCheckBox->isChecked();
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::setSelected(bool selected)
|
||||
{
|
||||
if (isSelected() == selected) {
|
||||
return;
|
||||
}
|
||||
selectionCheckBox->setChecked(selected);
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::updateSelectionVisual(bool selected)
|
||||
{
|
||||
selectionFrame->setVisible(selected);
|
||||
selectionFrame->raise();
|
||||
if (selected) {
|
||||
setAccessibleName(baseAccessibleName + tr(" (selected)"));
|
||||
} else {
|
||||
setAccessibleName(baseAccessibleName);
|
||||
}
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::setBaseAccessibleName(const QString &name)
|
||||
{
|
||||
baseAccessibleName = name;
|
||||
setAccessibleName(name);
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::toggleSelection()
|
||||
{
|
||||
setSelected(!isSelected());
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::activate()
|
||||
{
|
||||
setSelected(true);
|
||||
emit activated();
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
updateSelectionFrameGeometry();
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::updateSelectionFrameGeometry()
|
||||
{
|
||||
if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1));
|
||||
}
|
||||
|
||||
void SharedDeckPreviewWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
|
||||
toggleSelection();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* @file shared_deck_preview_widget.h
|
||||
* @ingroup DeckShareWidgets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef SHARED_DECK_PREVIEW_WIDGET_H
|
||||
#define SHARED_DECK_PREVIEW_WIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class ColorIdentityWidget;
|
||||
class DeckPreviewCardPictureWidget;
|
||||
class QCheckBox;
|
||||
class QFrame;
|
||||
class QKeyEvent;
|
||||
class QLabel;
|
||||
class QResizeEvent;
|
||||
class CardDatabaseQuerier;
|
||||
|
||||
/**
|
||||
* @brief A selectable preview tile for a deck that has no local file.
|
||||
*
|
||||
* Renders a banner card picture (looked up by name in the card database), the
|
||||
* deck name, color identity and game format. Used to preview decks shared via a
|
||||
* cockatrice:// link (metadata from Command_DeckShareList) and the deck
|
||||
* currently open in the deck editor.
|
||||
*
|
||||
* Selection follows the deck-preview covenant: the tile reports its click
|
||||
* immediately (no double-click interval delay), a palette(highlight) frame
|
||||
* marks the selected tile, and Space/Enter toggles selection from the keyboard.
|
||||
* A double click selects the tile and emits activated() so the caller can open
|
||||
* just that deck.
|
||||
*/
|
||||
class SharedDeckPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SharedDeckPreviewWidget(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &deckName,
|
||||
const QString &bannerCardName,
|
||||
const QString &colorIdentity,
|
||||
const QString &gameFormat = QString(),
|
||||
const QString &deckToolTip = QString());
|
||||
|
||||
[[nodiscard]] bool isSelected() const;
|
||||
void setSelected(bool selected);
|
||||
|
||||
void setBaseAccessibleName(const QString &name);
|
||||
|
||||
signals:
|
||||
void selectionToggled(bool selected);
|
||||
void activated();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void toggleSelection();
|
||||
void activate();
|
||||
|
||||
private:
|
||||
void updateSelectionVisual(bool selected);
|
||||
void updateSelectionFrameGeometry();
|
||||
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
QLabel *gameFormatLabel;
|
||||
QCheckBox *selectionCheckBox;
|
||||
QFrame *selectionFrame;
|
||||
QString baseAccessibleName;
|
||||
};
|
||||
|
||||
#endif // SHARED_DECK_PREVIEW_WIDGET_H
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
#include "dlg_login_prompt.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
DlgLoginPrompt::DlgLoginPrompt(const QString &serverText, QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Sign in"));
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->addWidget(
|
||||
new QLabel(tr("This link requires you to be signed in.\nSign in to %1:").arg(serverText), this));
|
||||
|
||||
auto *formLayout = new QFormLayout;
|
||||
usernameEdit = new QLineEdit(this);
|
||||
passwordEdit = new QLineEdit(this);
|
||||
passwordEdit->setEchoMode(QLineEdit::Password);
|
||||
formLayout->addRow(tr("Username:"), usernameEdit);
|
||||
formLayout->addRow(tr("Password:"), passwordEdit);
|
||||
mainLayout->addLayout(formLayout);
|
||||
|
||||
savePasswordCheckBox = new QCheckBox(tr("Save password for this server"), this);
|
||||
mainLayout->addWidget(savePasswordCheckBox);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
|
||||
usernameEdit->setFocus();
|
||||
}
|
||||
|
||||
QString DlgLoginPrompt::username() const
|
||||
{
|
||||
return usernameEdit->text().trimmed();
|
||||
}
|
||||
|
||||
QString DlgLoginPrompt::password() const
|
||||
{
|
||||
return passwordEdit->text();
|
||||
}
|
||||
|
||||
bool DlgLoginPrompt::savePassword() const
|
||||
{
|
||||
return savePasswordCheckBox->isChecked();
|
||||
}
|
||||
40
cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h
Normal file
40
cockatrice/src/interface/widgets/dialogs/dlg_login_prompt.h
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @file dlg_login_prompt.h
|
||||
* @ingroup ConnectionDialogs
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef DLG_LOGIN_PROMPT_H
|
||||
#define DLG_LOGIN_PROMPT_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QCheckBox;
|
||||
class QLineEdit;
|
||||
|
||||
/**
|
||||
* @brief Small sign-in dialog used when a cockatrice:// link needs credentials
|
||||
* that are not saved for the target server.
|
||||
*
|
||||
* The entered name and password are handed to the intent chain; when the user
|
||||
* opts to save them, they are stored in the server settings so that later links
|
||||
* to the same server connect seamlessly.
|
||||
*/
|
||||
class DlgLoginPrompt : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DlgLoginPrompt(const QString &serverText, QWidget *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString username() const;
|
||||
[[nodiscard]] QString password() const;
|
||||
[[nodiscard]] bool savePassword() const;
|
||||
|
||||
private:
|
||||
QLineEdit *usernameEdit;
|
||||
QLineEdit *passwordEdit;
|
||||
QCheckBox *savePasswordCheckBox;
|
||||
};
|
||||
|
||||
#endif // DLG_LOGIN_PROMPT_H
|
||||
|
|
@ -72,7 +72,11 @@ void DlgShareDeck::shareFinished(const Response &response, const CommandContaine
|
|||
|
||||
const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
|
||||
const QString token = QString::fromStdString(resp.token());
|
||||
#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
|
||||
|
||||
const QString link = DeckShareUtils::buildShareLink(client, token);
|
||||
DeckShareUtils::copyShareLinkToClipboard(link);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
#include "dlg_shared_decks_preview.h"
|
||||
|
||||
#include "../deck_share/shared_deck_preview_widget.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
|
||||
#include <QCloseEvent>
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_querier.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
|
||||
|
||||
DlgSharedDecksPreview::DlgSharedDecksPreview(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &shareName,
|
||||
qint64 expiresAt,
|
||||
const QString &serverText,
|
||||
const QList<ServerInfo_DeckShareItem> &items)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Open shared decks"));
|
||||
resize(700, 500);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
|
||||
auto *titleLabel = new QLabel(tr("Share: %1").arg(shareName.isEmpty() ? tr("Untitled") : shareName), this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setBold(true);
|
||||
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||
titleLabel->setFont(titleFont);
|
||||
mainLayout->addWidget(titleLabel);
|
||||
|
||||
if (!serverText.isEmpty()) {
|
||||
mainLayout->addWidget(new QLabel(tr("From %1").arg(serverText), this));
|
||||
}
|
||||
|
||||
if (expiresAt > 0) {
|
||||
const QString expiryText = QDateTime::fromSecsSinceEpoch(expiresAt).toLocalTime().toString(Qt::TextDate);
|
||||
mainLayout->addWidget(new QLabel(tr("This share link expires on %1").arg(expiryText), this));
|
||||
}
|
||||
|
||||
downloadStatusLabel = new QLabel(this);
|
||||
downloadStatusLabel->setVisible(false);
|
||||
mainLayout->addWidget(downloadStatusLabel);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
mainLayout->addWidget(flowWidget, 1);
|
||||
|
||||
for (const ServerInfo_DeckShareItem &item : items) {
|
||||
QStringList tags;
|
||||
for (const auto &tag : item.tags()) {
|
||||
tags.append(QString::fromStdString(tag));
|
||||
}
|
||||
|
||||
auto *tile = new SharedDeckPreviewWidget(
|
||||
this, querier, QString::fromStdString(item.name()), QString::fromStdString(item.banner_card()),
|
||||
QString::fromStdString(item.color_identity()), QString::fromStdString(item.game_format()), tags.join(", "));
|
||||
flowWidget->addWidget(tile);
|
||||
tiles.append(tile);
|
||||
itemIds.append(item.id());
|
||||
}
|
||||
|
||||
if (tiles.size() == 1) {
|
||||
tiles.first()->setSelected(true);
|
||||
}
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(this);
|
||||
openSelectedButton = buttonBox->addButton(tr("Open selected"), QDialogButtonBox::AcceptRole);
|
||||
openAllButton = buttonBox->addButton(tr("Open all"), QDialogButtonBox::ActionRole);
|
||||
buttonBox->addButton(tr("Cancel"), QDialogButtonBox::RejectRole);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, [this]() {
|
||||
onCancel();
|
||||
close();
|
||||
});
|
||||
|
||||
// Esc calls QDialog::reject() directly (which hides the dialog without a
|
||||
// close event), so route it through the same guarded cancel as the button.
|
||||
connect(this, &QDialog::rejected, this, [this]() {
|
||||
onCancel();
|
||||
close();
|
||||
});
|
||||
|
||||
connect(openSelectedButton, &QPushButton::clicked, this, &DlgSharedDecksPreview::openSelected);
|
||||
connect(buttonBox, &QDialogButtonBox::clicked, this, [this, buttonBox](QAbstractButton *button) {
|
||||
if (buttonBox->buttonRole(button) == QDialogButtonBox::ActionRole) {
|
||||
openAll();
|
||||
}
|
||||
});
|
||||
|
||||
for (SharedDeckPreviewWidget *tile : tiles) {
|
||||
connect(tile, &SharedDeckPreviewWidget::selectionToggled, this,
|
||||
&DlgSharedDecksPreview::updateOpenSelectedEnabled);
|
||||
}
|
||||
for (int i = 0; i < tiles.size(); ++i) {
|
||||
const int itemId = itemIds.at(i);
|
||||
// Double-clicking a tile selects it and opens just that deck.
|
||||
connect(tiles.at(i), &SharedDeckPreviewWidget::activated, this, [this, itemId]() {
|
||||
resultEmitted = true;
|
||||
setDownloading(true);
|
||||
emit openRequested(QList<int>{itemId});
|
||||
});
|
||||
}
|
||||
updateOpenSelectedEnabled();
|
||||
}
|
||||
|
||||
QList<int> DlgSharedDecksPreview::selectedItemIds() const
|
||||
{
|
||||
QList<int> selectedIds;
|
||||
for (int i = 0; i < tiles.size(); ++i) {
|
||||
if (tiles.at(i)->isSelected()) {
|
||||
selectedIds.append(itemIds.at(i));
|
||||
}
|
||||
}
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::openSelected()
|
||||
{
|
||||
const QList<int> selectedIds = selectedItemIds();
|
||||
if (selectedIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
resultEmitted = true;
|
||||
setDownloading(true);
|
||||
emit openRequested(selectedIds);
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::openAll()
|
||||
{
|
||||
resultEmitted = true;
|
||||
setDownloading(true);
|
||||
emit openRequested(itemIds);
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::setDownloading(bool downloading)
|
||||
{
|
||||
if (downloadInProgress == downloading) {
|
||||
return;
|
||||
}
|
||||
downloadInProgress = downloading;
|
||||
downloadStatusLabel->setVisible(downloading);
|
||||
for (SharedDeckPreviewWidget *tile : tiles) {
|
||||
tile->setEnabled(!downloading);
|
||||
}
|
||||
openSelectedButton->setEnabled(!downloading);
|
||||
openAllButton->setEnabled(!downloading);
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::setDownloadProgress(int done, int total, const QString ¤tDeckName)
|
||||
{
|
||||
if (!downloadInProgress) {
|
||||
return;
|
||||
}
|
||||
downloadStatusLabel->setText(tr("Downloading deck %1 of %2: %3").arg(done).arg(total).arg(currentDeckName));
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::updateOpenSelectedEnabled()
|
||||
{
|
||||
openSelectedButton->setEnabled(!selectedItemIds().isEmpty());
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::onCancel()
|
||||
{
|
||||
if (!resultEmitted || downloadInProgress) {
|
||||
resultEmitted = true;
|
||||
emit cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
void DlgSharedDecksPreview::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
onCancel();
|
||||
QDialog::closeEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#ifndef COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
|
||||
#define COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
class FlowWidget;
|
||||
class QCloseEvent;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class ServerInfo_DeckShareItem;
|
||||
class SharedDeckPreviewWidget;
|
||||
class CardDatabaseQuerier;
|
||||
|
||||
/**
|
||||
* @brief Non-modal preview of the decks contained in a shared-deck link.
|
||||
*
|
||||
* Lets the user pick which of the shared decks to open before anything is
|
||||
* downloaded. Emits openRequested with the ids of the chosen decks, or
|
||||
* cancelled when the user closes the dialog without choosing. Once the user
|
||||
* picks, the dialog switches into a "downloading" state: the tiles and open
|
||||
* buttons are disabled, a progress label shows the current download and Cancel
|
||||
* stays functional so the download can be aborted.
|
||||
*/
|
||||
class DlgSharedDecksPreview : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DlgSharedDecksPreview(QWidget *parent,
|
||||
const CardDatabaseQuerier *querier,
|
||||
const QString &shareName,
|
||||
qint64 expiresAt,
|
||||
const QString &serverText,
|
||||
const QList<ServerInfo_DeckShareItem> &items);
|
||||
|
||||
void setDownloadProgress(int done, int total, const QString ¤tDeckName);
|
||||
|
||||
public slots:
|
||||
void setDownloading(bool downloading);
|
||||
|
||||
signals:
|
||||
void openRequested(const QList<int> &itemIds);
|
||||
void cancelled();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void openSelected();
|
||||
void openAll();
|
||||
void updateOpenSelectedEnabled();
|
||||
void onCancel();
|
||||
|
||||
private:
|
||||
QList<int> selectedItemIds() const;
|
||||
|
||||
FlowWidget *flowWidget;
|
||||
QList<SharedDeckPreviewWidget *> tiles;
|
||||
QList<int> itemIds;
|
||||
QPushButton *openSelectedButton;
|
||||
QPushButton *openAllButton;
|
||||
QLabel *downloadStatusLabel;
|
||||
bool resultEmitted = false;
|
||||
bool downloadInProgress = false;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DLG_SHARED_DECKS_PREVIEW_H
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "flow_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QSizePolicy>
|
||||
|
|
@ -177,6 +178,50 @@ QLayoutItem *FlowWidget::itemAt(int index) const
|
|||
return flowLayout->itemAt(index);
|
||||
}
|
||||
|
||||
void FlowWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
// Keyboard navigation between the flow items: arrow keys move focus just
|
||||
// like clicking the sibling tiles would. Only items that can take keyboard
|
||||
// focus (e.g. the deck-preview tiles in shared-deck links) are visited.
|
||||
const bool moveForward = event->key() == Qt::Key_Right || event->key() == Qt::Key_Down;
|
||||
const bool moveBackward = event->key() == Qt::Key_Left || event->key() == Qt::Key_Up;
|
||||
if (!moveForward && !moveBackward) {
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QWidget *> focusableItems;
|
||||
for (int i = 0; i < flowLayout->count(); ++i) {
|
||||
QWidget *item = flowLayout->itemAt(i)->widget();
|
||||
if (item != nullptr && (item->focusPolicy() & Qt::TabFocus)) {
|
||||
focusableItems.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (focusableItems.isEmpty()) {
|
||||
QWidget::keyPressEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < focusableItems.size(); ++i) {
|
||||
if (focusableItems.at(i)->hasFocus()) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const int delta = moveForward ? 1 : -1;
|
||||
int nextIndex;
|
||||
if (currentIndex < 0) {
|
||||
nextIndex = moveForward ? 0 : focusableItems.size() - 1;
|
||||
} else {
|
||||
nextIndex = (currentIndex + delta + focusableItems.size()) % focusableItems.size();
|
||||
}
|
||||
focusableItems.value(nextIndex)->setFocus();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
int FlowWidget::count() const
|
||||
{
|
||||
return flowLayout->count();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "../../../layouts/flow_layout.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLoggingCategory>
|
||||
#include <QScrollArea>
|
||||
#include <QWidget>
|
||||
|
|
@ -44,6 +45,7 @@ public slots:
|
|||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private:
|
||||
Qt::Orientation flowDirection;
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ int RemoteDeckList_TreeModel::rowCount(const QModelIndex &parent) const
|
|||
|
||||
int RemoteDeckList_TreeModel::columnCount(const QModelIndex & /*parent*/) const
|
||||
{
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) const
|
||||
|
|
@ -121,7 +121,7 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
if (index.column() >= 3) {
|
||||
if (index.column() >= 4) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
|
|
@ -134,12 +134,20 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
switch (index.column()) {
|
||||
case 0:
|
||||
return node->getName();
|
||||
case 3:
|
||||
return isEffectivelyPublic(node) ? tr("Public") : tr("Private");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
case Qt::DecorationRole:
|
||||
return index.column() == 0 ? dirIcon : QVariant();
|
||||
case Qt::ToolTipRole:
|
||||
if (index.column() == 3) {
|
||||
return isEffectivelyPublic(node) ? tr("This folder is visible to other users")
|
||||
: tr("This folder is only visible to you");
|
||||
}
|
||||
return QVariant();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -153,6 +161,8 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
return file->getId();
|
||||
case 2:
|
||||
return file->getUploadTime();
|
||||
case 3:
|
||||
return isEffectivelyPublic(file) ? tr("Public") : tr("Private");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -161,6 +171,12 @@ QVariant RemoteDeckList_TreeModel::data(const QModelIndex &index, int role) cons
|
|||
return index.column() == 0 ? fileIcon : QVariant();
|
||||
case Qt::TextAlignmentRole:
|
||||
return index.column() == 1 ? Qt::AlignRight : Qt::AlignLeft;
|
||||
case Qt::ToolTipRole:
|
||||
if (index.column() == 3) {
|
||||
return isEffectivelyPublic(file) ? tr("This deck is visible to other users")
|
||||
: tr("This deck is only visible to you");
|
||||
}
|
||||
return QVariant();
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -183,6 +199,8 @@ QVariant RemoteDeckList_TreeModel::headerData(int section, Qt::Orientation orien
|
|||
return tr("ID");
|
||||
case 2:
|
||||
return tr("Upload time");
|
||||
case 3:
|
||||
return tr("Visibility");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
|
@ -239,13 +257,14 @@ void RemoteDeckList_TreeModel::addFileToTree(const ServerInfo_DeckStorage_TreeIt
|
|||
time.setSecsSinceEpoch(fileInfo.creation_time());
|
||||
|
||||
beginInsertRows(nodeToIndex(parent), parent->size(), parent->size());
|
||||
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent));
|
||||
parent->append(new FileNode(QString::fromStdString(file.name()), file.id(), time, parent, fileInfo.is_public()));
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void RemoteDeckList_TreeModel::addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent)
|
||||
{
|
||||
DirectoryNode *newItem = addNamedFolderToTree(QString::fromStdString(folder.name()), parent);
|
||||
newItem->setIsPublic(folder.folder().is_public());
|
||||
const ServerInfo_DeckStorage_Folder &folderInfo = folder.folder();
|
||||
const int folderItemsSize = folderInfo.items_size();
|
||||
for (int i = 0; i < folderItemsSize; ++i) {
|
||||
|
|
@ -285,6 +304,21 @@ void RemoteDeckList_TreeModel::refreshTree()
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
bool RemoteDeckList_TreeModel::isEffectivelyPublic(const Node *node) const
|
||||
{
|
||||
if (node == nullptr || node == root) {
|
||||
return false;
|
||||
}
|
||||
const Node *current = node;
|
||||
while (current != nullptr) {
|
||||
if (current->isPublic()) {
|
||||
return true;
|
||||
}
|
||||
current = current->getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RemoteDeckList_TreeModel::clearTree()
|
||||
{
|
||||
beginResetModel();
|
||||
|
|
|
|||
|
|
@ -27,9 +27,11 @@ public:
|
|||
protected:
|
||||
DirectoryNode *parent;
|
||||
QString name;
|
||||
bool publicFlag;
|
||||
|
||||
public:
|
||||
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr) : parent(_parent), name(_name)
|
||||
explicit Node(const QString &_name, DirectoryNode *_parent = nullptr)
|
||||
: parent(_parent), name(_name), publicFlag(false)
|
||||
{
|
||||
}
|
||||
virtual ~Node() = default;
|
||||
|
|
@ -41,6 +43,14 @@ public:
|
|||
{
|
||||
return name;
|
||||
}
|
||||
[[nodiscard]] bool isPublic() const
|
||||
{
|
||||
return publicFlag;
|
||||
}
|
||||
void setIsPublic(bool _public)
|
||||
{
|
||||
publicFlag = _public;
|
||||
}
|
||||
};
|
||||
class DirectoryNode : public Node, public QList<Node *>
|
||||
{
|
||||
|
|
@ -59,9 +69,14 @@ public:
|
|||
QDateTime uploadTime;
|
||||
|
||||
public:
|
||||
FileNode(const QString &_name, int _id, const QDateTime &_uploadTime, DirectoryNode *_parent = nullptr)
|
||||
FileNode(const QString &_name,
|
||||
int _id,
|
||||
const QDateTime &_uploadTime,
|
||||
DirectoryNode *_parent = nullptr,
|
||||
bool _isPublic = false)
|
||||
: Node(_name, _parent), id(_id), uploadTime(_uploadTime)
|
||||
{
|
||||
setIsPublic(_isPublic);
|
||||
}
|
||||
[[nodiscard]] int getId() const
|
||||
{
|
||||
|
|
@ -109,6 +124,11 @@ public:
|
|||
{
|
||||
return root;
|
||||
}
|
||||
/**
|
||||
* @brief Whether a node is visible to other users (own flag or inherited
|
||||
* from any ancestor folder).
|
||||
*/
|
||||
[[nodiscard]] bool isEffectivelyPublic(const Node *node) const;
|
||||
void addFileToTree(const ServerInfo_DeckStorage_TreeItem &file, DirectoryNode *parent);
|
||||
void addFolderToTree(const ServerInfo_DeckStorage_TreeItem &folder, DirectoryNode *parent);
|
||||
DirectoryNode *addNamedFolderToTree(const QString &name, DirectoryNode *parent);
|
||||
|
|
@ -125,6 +145,10 @@ private:
|
|||
|
||||
public:
|
||||
explicit RemoteDeckList_TreeWidget(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
[[nodiscard]] RemoteDeckList_TreeModel *getModel() const
|
||||
{
|
||||
return treeModel;
|
||||
}
|
||||
[[nodiscard]] RemoteDeckList_TreeModel::Node *getNode(const QModelIndex &ind) const;
|
||||
[[nodiscard]] RemoteDeckList_TreeModel::Node *getCurrentItem() const;
|
||||
[[nodiscard]] QList<RemoteDeckList_TreeModel::Node *> getCurrentSelection() const;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aDetails = new QAction(QString(), this);
|
||||
aChat = new QAction(QString(), this);
|
||||
aShowGames = new QAction(QString(), this);
|
||||
aViewPublicDecks = new QAction(QString(), this);
|
||||
aAddToBuddyList = new QAction(QString(), this);
|
||||
aRemoveFromBuddyList = new QAction(QString(), this);
|
||||
aAddToIgnoreList = new QAction(QString(), this);
|
||||
|
|
@ -62,6 +63,7 @@ void UserContextMenu::retranslateUi()
|
|||
aDetails->setText(tr("User &details"));
|
||||
aChat->setText(tr("Private &chat"));
|
||||
aShowGames->setText(tr("Show this user's &games"));
|
||||
aViewPublicDecks->setText(tr("View this user's &public decks"));
|
||||
aAddToBuddyList->setText(tr("Add to &buddy list"));
|
||||
aRemoveFromBuddyList->setText(tr("Remove from &buddy list"));
|
||||
aAddToIgnoreList->setText(tr("Add to &ignore list"));
|
||||
|
|
@ -372,6 +374,9 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
}
|
||||
menu->addAction(aDetails);
|
||||
menu->addAction(aShowGames);
|
||||
if (userLevel.testFlag(ServerInfo_User::IsRegistered)) {
|
||||
menu->addAction(aViewPublicDecks);
|
||||
}
|
||||
menu->addAction(aChat);
|
||||
const QList<GameInviteOption> inviteOptions = inviteOptionsForUser(userName);
|
||||
if (!inviteOptions.isEmpty()) {
|
||||
|
|
@ -442,6 +447,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aChat->setEnabled(anotherUser && online);
|
||||
aShowGames->setEnabled(online);
|
||||
aReport->setEnabled(anotherUser);
|
||||
aViewPublicDecks->setEnabled(anotherUser);
|
||||
aAddToBuddyList->setEnabled(anotherUser);
|
||||
aRemoveFromBuddyList->setEnabled(anotherUser);
|
||||
aAddToIgnoreList->setEnabled(anotherUser);
|
||||
|
|
@ -464,6 +470,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execChat(userName);
|
||||
} else if (actionClicked == aShowGames) {
|
||||
execShowGames(userName);
|
||||
} else if (actionClicked == aViewPublicDecks) {
|
||||
execViewPublicDecks(userName);
|
||||
} else if (actionClicked == aAddToBuddyList) {
|
||||
execAddToBuddy(userName);
|
||||
} else if (actionClicked == aRemoveFromBuddyList) {
|
||||
|
|
@ -585,6 +593,11 @@ void UserContextMenu::execShowGames(const QString &userName)
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserContextMenu::execViewPublicDecks(const QString &userName)
|
||||
{
|
||||
tabSupervisor->openTabPublicDecks(userName);
|
||||
}
|
||||
|
||||
void UserContextMenu::execAddToBuddy(const QString &userName)
|
||||
{
|
||||
Command_AddToList cmd;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ private:
|
|||
QAction *aUserName;
|
||||
QAction *aDetails;
|
||||
QAction *aShowGames;
|
||||
QAction *aViewPublicDecks;
|
||||
QAction *aChat;
|
||||
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
|
||||
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
|
||||
|
|
@ -110,6 +111,7 @@ public:
|
|||
void execInvite(const QString &userName);
|
||||
void execDetails(const QString &userName);
|
||||
void execShowGames(const QString &userName);
|
||||
void execViewPublicDecks(const QString &userName);
|
||||
void execAddToBuddy(const QString &userName);
|
||||
void execRemoveFromBuddy(const QString &userName);
|
||||
void execAddToIgnore(const QString &userName);
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ bool AbstractTabDeckEditor::actSaveDeck()
|
|||
Command_DeckUpload cmd;
|
||||
cmd.set_deck_id(static_cast<google::protobuf::uint32>(loadedDeck.lastLoadInfo.remoteDeckId));
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
cmd.set_tags(loadedDeck.deckList.getTags().join(QStringLiteral(",")).toStdString());
|
||||
|
||||
PendingCommand *pend = AbstractClient::prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &AbstractTabDeckEditor::saveDeckRemoteFinished);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
#include "public_decks_quick_settings_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../cards/card_size_widget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QSpinBox>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
PublicDecksQuickSettingsWidget::PublicDecksQuickSettingsWidget(QWidget *parent) : SettingsButtonWidget(parent)
|
||||
{
|
||||
// show color identity on preview tiles checkbox
|
||||
showColorIdentityCheckBox = new QCheckBox(this);
|
||||
showColorIdentityCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity());
|
||||
connect(showColorIdentityCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showColorIdentityChanged);
|
||||
connect(showColorIdentityCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowColorIdentity);
|
||||
|
||||
// show tags on preview tiles checkbox
|
||||
showTagsOnDeckPreviewsCheckBox = new QCheckBox(this);
|
||||
showTagsOnDeckPreviewsCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews());
|
||||
connect(showTagsOnDeckPreviewsCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showTagsOnDeckPreviewsChanged);
|
||||
connect(showTagsOnDeckPreviewsCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews);
|
||||
|
||||
// show the last modified / upload time on preview tiles checkbox
|
||||
showUploadTimeCheckBox = new QCheckBox(this);
|
||||
showUploadTimeCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime());
|
||||
connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showUploadTimeChanged);
|
||||
connect(showUploadTimeCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime);
|
||||
|
||||
// show tag filter box checkbox
|
||||
showTagFilterCheckBox = new QCheckBox(this);
|
||||
showTagFilterCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter());
|
||||
connect(showTagFilterCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::showTagFilterChanged);
|
||||
connect(showTagFilterCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageShowTagFilter);
|
||||
|
||||
// draw unused color identities checkbox
|
||||
drawUnusedColorIdentitiesCheckBox = new QCheckBox(this);
|
||||
drawUnusedColorIdentitiesCheckBox->setChecked(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities());
|
||||
connect(drawUnusedColorIdentitiesCheckBox, &QCheckBox::QT_STATE_CHANGED, this,
|
||||
&PublicDecksQuickSettingsWidget::drawUnusedColorIdentitiesChanged);
|
||||
connect(drawUnusedColorIdentitiesCheckBox, &QCheckBox::QT_STATE_CHANGED,
|
||||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities);
|
||||
|
||||
// unused color identities opacity selector
|
||||
auto unusedColorIdentityOpacityWidget = new QWidget(this);
|
||||
|
||||
unusedColorIdentitiesOpacityLabel = new QLabel(unusedColorIdentityOpacityWidget);
|
||||
unusedColorIdentitiesOpacitySpinBox = new QSpinBox(unusedColorIdentityOpacityWidget);
|
||||
|
||||
unusedColorIdentitiesOpacitySpinBox->setMinimum(0);
|
||||
unusedColorIdentitiesOpacitySpinBox->setMaximum(100);
|
||||
unusedColorIdentitiesOpacitySpinBox->setValue(
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageUnusedColorIdentitiesOpacity());
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
&PublicDecksQuickSettingsWidget::unusedColorIdentitiesOpacityChanged);
|
||||
connect(unusedColorIdentitiesOpacitySpinBox, qOverload<int>(&QSpinBox::valueChanged),
|
||||
&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::setVisualDeckStorageUnusedColorIdentitiesOpacity);
|
||||
|
||||
unusedColorIdentitiesOpacityLabel->setBuddy(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
auto unusedColorIdentityOpacityLayout = new QHBoxLayout(unusedColorIdentityOpacityWidget);
|
||||
unusedColorIdentityOpacityLayout->setContentsMargins(11, 0, 11, 0);
|
||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacityLabel);
|
||||
unusedColorIdentityOpacityLayout->addWidget(unusedColorIdentitiesOpacitySpinBox);
|
||||
|
||||
// card size slider (kept at the bottom, like the Visual Deck Storage)
|
||||
cardSizeWidget =
|
||||
new CardSizeWidget(this, nullptr, SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize());
|
||||
connect(cardSizeWidget->getSlider(), &QSlider::valueChanged, this,
|
||||
&PublicDecksQuickSettingsWidget::cardSizeChanged);
|
||||
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance().cardsDisplay(),
|
||||
&CardsDisplaySettings::setVisualDeckStorageCardSize);
|
||||
|
||||
this->addSettingsWidget(showColorIdentityCheckBox);
|
||||
this->addSettingsWidget(showTagsOnDeckPreviewsCheckBox);
|
||||
this->addSettingsWidget(showUploadTimeCheckBox);
|
||||
this->addSettingsWidget(showTagFilterCheckBox);
|
||||
this->addSettingsWidget(drawUnusedColorIdentitiesCheckBox);
|
||||
this->addSettingsWidget(unusedColorIdentityOpacityWidget);
|
||||
this->addSettingsWidget(cardSizeWidget);
|
||||
|
||||
connect(&SettingsCache::instance().personal(), &PersonalSettings::langChanged, this,
|
||||
&PublicDecksQuickSettingsWidget::retranslateUi);
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PublicDecksQuickSettingsWidget::retranslateUi()
|
||||
{
|
||||
showColorIdentityCheckBox->setText(tr("Show Color Identity"));
|
||||
showTagsOnDeckPreviewsCheckBox->setText(tr("Show Tags On Deck Previews"));
|
||||
showUploadTimeCheckBox->setText(tr("Show Upload Time"));
|
||||
showTagFilterCheckBox->setText(tr("Show Tag Filter"));
|
||||
drawUnusedColorIdentitiesCheckBox->setText(tr("Draw unused Color Identities"));
|
||||
unusedColorIdentitiesOpacityLabel->setText(tr("Unused Color Identities Opacity"));
|
||||
unusedColorIdentitiesOpacitySpinBox->setSuffix("%");
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getDrawUnusedColorIdentities() const
|
||||
{
|
||||
return drawUnusedColorIdentitiesCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowColorIdentity() const
|
||||
{
|
||||
return showColorIdentityCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowTagFilter() const
|
||||
{
|
||||
return showTagFilterCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowTagsOnDeckPreviews() const
|
||||
{
|
||||
return showTagsOnDeckPreviewsCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool PublicDecksQuickSettingsWidget::getShowUploadTime() const
|
||||
{
|
||||
return showUploadTimeCheckBox->isChecked();
|
||||
}
|
||||
|
||||
int PublicDecksQuickSettingsWidget::getUnusedColorIdentitiesOpacity() const
|
||||
{
|
||||
return unusedColorIdentitiesOpacitySpinBox->value();
|
||||
}
|
||||
|
||||
CardSizeWidget *PublicDecksQuickSettingsWidget::getCardSizeWidget() const
|
||||
{
|
||||
return cardSizeWidget;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* @file public_decks_quick_settings_widget.h
|
||||
* @ingroup Tabs
|
||||
* @brief The quick settings menu for the public decks tab.
|
||||
* Manages the widgets in the quick settings menu dropdown of the public decks
|
||||
* tab, and syncs their values with the same SettingsCache keys the Visual Deck
|
||||
* Storage uses, so shared preview widgets (color identity, tags) behave the
|
||||
* same way in both places.
|
||||
*/
|
||||
|
||||
#ifndef PUBLIC_DECKS_QUICK_SETTINGS_WIDGET_H
|
||||
#define PUBLIC_DECKS_QUICK_SETTINGS_WIDGET_H
|
||||
|
||||
#include "../quick_settings/settings_button_widget.h"
|
||||
|
||||
class CardSizeWidget;
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QSpinBox;
|
||||
|
||||
class PublicDecksQuickSettingsWidget : public SettingsButtonWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QCheckBox *showColorIdentityCheckBox;
|
||||
QCheckBox *drawUnusedColorIdentitiesCheckBox;
|
||||
QCheckBox *showTagFilterCheckBox;
|
||||
QCheckBox *showTagsOnDeckPreviewsCheckBox;
|
||||
QCheckBox *showUploadTimeCheckBox;
|
||||
QLabel *unusedColorIdentitiesOpacityLabel;
|
||||
QSpinBox *unusedColorIdentitiesOpacitySpinBox;
|
||||
CardSizeWidget *cardSizeWidget;
|
||||
|
||||
public:
|
||||
explicit PublicDecksQuickSettingsWidget(QWidget *parent = nullptr);
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
[[nodiscard]] bool getDrawUnusedColorIdentities() const;
|
||||
[[nodiscard]] bool getShowColorIdentity() const;
|
||||
[[nodiscard]] bool getShowTagFilter() const;
|
||||
[[nodiscard]] bool getShowTagsOnDeckPreviews() const;
|
||||
[[nodiscard]] bool getShowUploadTime() const;
|
||||
[[nodiscard]] int getUnusedColorIdentitiesOpacity() const;
|
||||
[[nodiscard]] CardSizeWidget *getCardSizeWidget() const;
|
||||
|
||||
signals:
|
||||
void drawUnusedColorIdentitiesChanged(bool enabled);
|
||||
void showColorIdentityChanged(bool enabled);
|
||||
void showTagFilterChanged(bool enabled);
|
||||
void showTagsOnDeckPreviewsChanged(bool enabled);
|
||||
void showUploadTimeChanged(bool enabled);
|
||||
void unusedColorIdentitiesOpacityChanged(int opacity);
|
||||
void cardSizeChanged(int scale);
|
||||
};
|
||||
|
||||
#endif // PUBLIC_DECKS_QUICK_SETTINGS_WIDGET_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#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 "../deck_share/share_bar_widget.h"
|
||||
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
|
||||
|
|
@ -24,11 +25,13 @@
|
|||
#include <QTreeView>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_del_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_set_visibility.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
|
|
@ -159,6 +162,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
aShareDecks->setIcon(QPixmap("theme:icons/share"));
|
||||
connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks);
|
||||
|
||||
aPublishDeck = new QAction(this);
|
||||
aPublishDeck->setIcon(QPixmap("theme:icons/lock"));
|
||||
connect(aPublishDeck, &QAction::triggered, this, &TabDeckStorage::actPublishDeck);
|
||||
|
||||
// Add actions to toolbars
|
||||
leftToolBar->addAction(aOpenLocalDeck);
|
||||
leftToolBar->addAction(aRenameLocal);
|
||||
|
|
@ -171,6 +178,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
rightToolBar->addAction(aOpenRemoteDeck);
|
||||
rightToolBar->addAction(aDownload);
|
||||
rightToolBar->addAction(aShareDecks);
|
||||
rightToolBar->addAction(aPublishDeck);
|
||||
rightToolBar->addAction(aNewFolder);
|
||||
rightToolBar->addAction(aDeleteRemoteDeck);
|
||||
|
||||
|
|
@ -200,6 +208,7 @@ void TabDeckStorage::retranslateUi()
|
|||
aDeleteLocalDeck->setText(tr("Delete"));
|
||||
aDeleteRemoteDeck->setText(tr("Delete"));
|
||||
aShareDecks->setText(tr("Share decks"));
|
||||
aPublishDeck->setText(tr("Publish/unpublish deck"));
|
||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||
shareBar->retranslateUi();
|
||||
}
|
||||
|
|
@ -244,6 +253,7 @@ void TabDeckStorage::setRemoteEnabled(bool enabled)
|
|||
aOpenRemoteDeck->setEnabled(enabled);
|
||||
aDownload->setEnabled(enabled);
|
||||
aShareDecks->setEnabled(enabled);
|
||||
aPublishDeck->setEnabled(enabled);
|
||||
aNewFolder->setEnabled(enabled);
|
||||
aDeleteRemoteDeck->setEnabled(enabled);
|
||||
|
||||
|
|
@ -372,6 +382,12 @@ void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPa
|
|||
cmd.set_path(targetPath.toStdString());
|
||||
cmd.set_deck_list(deckString.toStdString());
|
||||
|
||||
const CardRef bannerCard = deck.getBannerCard();
|
||||
cmd.set_banner_card_name(bannerCard.name.toStdString());
|
||||
cmd.set_banner_card_provider(bannerCard.providerId.toStdString());
|
||||
cmd.set_color_identity(getDeckColorIdentity(deck, CardDatabaseManager::query()).toStdString());
|
||||
cmd.set_tags(deck.getTags().join(QStringLiteral(",")).toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::uploadFinished);
|
||||
client->sendCommand(pend);
|
||||
|
|
@ -779,7 +795,11 @@ void TabDeckStorage::shareFromTreeFinished(const Response &response, const Comma
|
|||
}
|
||||
const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
|
||||
const QString token = QString::fromStdString(resp.token());
|
||||
#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
|
||||
|
||||
const QString link = DeckShareUtils::buildShareLink(client, token);
|
||||
DeckShareUtils::copyShareLinkToClipboard(link);
|
||||
|
|
@ -791,7 +811,49 @@ void TabDeckStorage::shareFromTreeFinished(const Response &response, const Comma
|
|||
|
||||
void TabDeckStorage::showShareNotice(const QString &message, bool warning)
|
||||
{
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message,
|
||||
QMessageBox::Ok, this);
|
||||
box.exec();
|
||||
}
|
||||
|
||||
void TabDeckStorage::actPublishDeck()
|
||||
{
|
||||
const auto selection = serverDirView->getCurrentSelection();
|
||||
for (const auto *node : selection) {
|
||||
Command_DeckSetVisibility cmd;
|
||||
if (const auto *fileNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||
cmd.set_deck_id(fileNode->getId());
|
||||
} else if (const auto *dirNode = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||
const QString path = dirNode->getPath();
|
||||
if (path.isEmpty()) {
|
||||
continue; // the root folder cannot be published
|
||||
}
|
||||
cmd.set_folder_path(path.toStdString());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
// Toggle the node's own visibility bit (what the server persists); the
|
||||
// effective visibility shown by the column may additionally be inherited
|
||||
// from a parent folder.
|
||||
cmd.set_is_public(!node->isPublic());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::setVisibilityFinished);
|
||||
client->sendCommand(pend);
|
||||
++pendingVisibilityChanges;
|
||||
}
|
||||
}
|
||||
|
||||
void TabDeckStorage::setVisibilityFinished(const Response &r, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (r.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Failed to change deck visibility on server (response code %1).")
|
||||
.arg(QString::number(static_cast<int>(r.response_code()))));
|
||||
}
|
||||
// Refresh once the last in-flight change has been acknowledged so the
|
||||
// Public/Private column reflects every selected node.
|
||||
if (--pendingVisibilityChanges == 0) {
|
||||
serverDirView->refreshTree();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ private:
|
|||
|
||||
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
||||
QAction *aOpenDecksFolder;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aNewFolder, *aDeleteRemoteDeck;
|
||||
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck;
|
||||
int pendingVisibilityChanges = 0;
|
||||
QString getTargetPath() const;
|
||||
|
||||
void setRemoteEnabled(bool enabled);
|
||||
|
|
@ -87,6 +88,9 @@ private slots:
|
|||
void onServerSelectionChanged();
|
||||
void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
void actPublishDeck();
|
||||
void setVisibilityFinished(const Response &r, const CommandContainer &commandContainer);
|
||||
|
||||
void actDeleteRemoteDeck();
|
||||
void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
|
|
|
|||
230
cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp
Normal file
230
cockatrice/src/interface/widgets/tabs/tab_public_decks.cpp
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
#include "tab_public_decks.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../deck_loader/deck_loader.h"
|
||||
#include "../general/layout_containers/flow_widget.h"
|
||||
#include "../visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.h"
|
||||
#include "../visual_deck_storage/deck_preview/public_deck_preview_widget.h"
|
||||
#include "../visual_deck_storage/remote_public_decks_model.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_search_widget.h"
|
||||
#include "../visual_deck_storage/visual_deck_storage_tag_filter_widget.h"
|
||||
#include "public_decks_quick_settings_widget.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QStringList>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/cards_display_settings.h>
|
||||
#include <optional>
|
||||
|
||||
TabPublicDecks::TabPublicDecks(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &_userName)
|
||||
: Tab(_tabSupervisor), client(_client), userName(_userName)
|
||||
{
|
||||
model = new RemotePublicDecksModel(client, this);
|
||||
cardSize = SettingsCache::instance().cardsDisplay().getVisualDeckStorageCardSize();
|
||||
|
||||
titleLabel = new QLabel(tr("Public decks of %1").arg(userName), this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setBold(true);
|
||||
titleLabel->setFont(titleFont);
|
||||
|
||||
auto *headerLayout = new QHBoxLayout;
|
||||
headerLayout->addWidget(titleLabel);
|
||||
headerLayout->addStretch(1);
|
||||
|
||||
// Filter/toolbar row, matching the Visual Deck Storage: color identity filter
|
||||
// first, the search bar stretching in the middle, and the quick settings
|
||||
// cogwheel at the end. The card size slider lives inside the cogwheel popup.
|
||||
emptyLabel = new QLabel(tr("This user has not published any decks."), this);
|
||||
emptyLabel->setAlignment(Qt::AlignCenter);
|
||||
emptyLabel->setVisible(false);
|
||||
|
||||
statusLabel = new QLabel(this);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
statusLabel->setVisible(false);
|
||||
|
||||
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
flowWidget->setSpacing(8, 8);
|
||||
|
||||
colorIdentityFilter = new DeckPreviewColorIdentityFilterWidget(this);
|
||||
searchWidget = new VisualDeckStorageSearchWidget(this);
|
||||
searchWidget->setPlaceholderText(tr("Search by deck name"));
|
||||
refreshButton = new QToolButton(this);
|
||||
refreshButton->setIcon(QPixmap("theme:icons/reload"));
|
||||
refreshButton->setFixedSize(32, 32);
|
||||
quickSettingsWidget = new PublicDecksQuickSettingsWidget(this);
|
||||
|
||||
auto *filterLayout = new QHBoxLayout;
|
||||
filterLayout->addWidget(colorIdentityFilter);
|
||||
filterLayout->addWidget(searchWidget, 1);
|
||||
filterLayout->addWidget(refreshButton);
|
||||
filterLayout->addWidget(quickSettingsWidget);
|
||||
|
||||
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
||||
tagFilterWidget->setAllTagsProvider([this] { return model->allTags(); });
|
||||
updateTagsVisibility(quickSettingsWidget->getShowTagFilter());
|
||||
|
||||
auto *layout = new QVBoxLayout;
|
||||
layout->addLayout(headerLayout);
|
||||
layout->addLayout(filterLayout);
|
||||
layout->addWidget(tagFilterWidget);
|
||||
layout->addWidget(statusLabel);
|
||||
layout->addWidget(emptyLabel);
|
||||
layout->addWidget(flowWidget, 1);
|
||||
|
||||
auto *mainWidget = new QWidget(this);
|
||||
mainWidget->setLayout(layout);
|
||||
setCentralWidget(mainWidget);
|
||||
|
||||
connect(refreshButton, &QToolButton::clicked, this, [this] { model->refresh(userName); });
|
||||
connect(model, &QAbstractItemModel::modelReset, this, &TabPublicDecks::rebuildGrid);
|
||||
connect(model, &RemotePublicDecksModel::loadingChanged, this, &TabPublicDecks::updateLoadingState);
|
||||
connect(model, &RemotePublicDecksModel::loadFailed, this, [this](const QString &message) {
|
||||
statusLabel->setText(message);
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
});
|
||||
connect(searchWidget, &VisualDeckStorageSearchWidget::searchTextChanged, this,
|
||||
[this](const QString &text) { model->setSearchText(text); });
|
||||
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::activeColorsChanged, this,
|
||||
&TabPublicDecks::updateColorFilter);
|
||||
connect(colorIdentityFilter, &DeckPreviewColorIdentityFilterWidget::filterModeChanged, this,
|
||||
&TabPublicDecks::updateColorFilter);
|
||||
connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this, &TabPublicDecks::updateTagFilter);
|
||||
connect(quickSettingsWidget, &PublicDecksQuickSettingsWidget::cardSizeChanged, this,
|
||||
&TabPublicDecks::updateCardSize);
|
||||
connect(quickSettingsWidget, &PublicDecksQuickSettingsWidget::showTagFilterChanged, this,
|
||||
&TabPublicDecks::updateTagsVisibility);
|
||||
|
||||
model->refresh(userName);
|
||||
}
|
||||
|
||||
QString TabPublicDecks::getTabText() const
|
||||
{
|
||||
return tr("Public decks of %1").arg(userName);
|
||||
}
|
||||
|
||||
void TabPublicDecks::retranslateUi()
|
||||
{
|
||||
titleLabel->setText(tr("Public decks of %1").arg(userName));
|
||||
searchWidget->setPlaceholderText(tr("Search by deck name"));
|
||||
emptyLabel->setText(tr("This user has not published any decks."));
|
||||
refreshButton->setToolTip(tr("Refresh"));
|
||||
quickSettingsWidget->setToolTip(tr("Public Decks Settings"));
|
||||
emit tabTextChanged(this, getTabText());
|
||||
}
|
||||
|
||||
bool TabPublicDecks::closeRequest()
|
||||
{
|
||||
emit closing(this);
|
||||
return Tab::closeRequest();
|
||||
}
|
||||
|
||||
void TabPublicDecks::rebuildGrid()
|
||||
{
|
||||
flowWidget->clearLayout();
|
||||
|
||||
const int count = model->rowCount();
|
||||
if (count == 0) {
|
||||
emptyLabel->setText(model->totalCount() > 0 ? tr("No decks match your filters.")
|
||||
: tr("This user has not published any decks."));
|
||||
}
|
||||
emptyLabel->setVisible(count == 0);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto *tile = new PublicDeckPreviewWidget(flowWidget, model->entryAt(i));
|
||||
tile->setScaleFactor(cardSize);
|
||||
connect(tile, &PublicDeckPreviewWidget::openDeckRequested, this, &TabPublicDecks::openDeck);
|
||||
flowWidget->addWidget(tile);
|
||||
}
|
||||
|
||||
// The deck set changed, so the tag filter chips are re-gathered from it.
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateColorFilter()
|
||||
{
|
||||
model->setColorFilter(colorIdentityFilter->getFilterMode(), colorIdentityFilter->getActiveColors());
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateTagFilter()
|
||||
{
|
||||
const QStringList selectedTags = tagFilterWidget->selectedTags();
|
||||
const QStringList excludedTags = tagFilterWidget->excludedTags();
|
||||
model->setTagFilter(QSet<QString>(selectedTags.cbegin(), selectedTags.cend()),
|
||||
QSet<QString>(excludedTags.cbegin(), excludedTags.cend()));
|
||||
tagFilterWidget->refreshTags();
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateTagsVisibility(bool visible)
|
||||
{
|
||||
tagFilterWidget->setVisible(visible);
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateLoadingState(bool loading)
|
||||
{
|
||||
if (loading) {
|
||||
statusLabel->setText(tr("Loading public decks…"));
|
||||
statusLabel->setVisible(true);
|
||||
flowWidget->setVisible(false);
|
||||
emptyLabel->setVisible(false);
|
||||
} else {
|
||||
statusLabel->setVisible(false);
|
||||
flowWidget->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TabPublicDecks::updateCardSize(int scale)
|
||||
{
|
||||
cardSize = scale;
|
||||
applyCardSize(scale);
|
||||
}
|
||||
|
||||
void TabPublicDecks::applyCardSize(int scale)
|
||||
{
|
||||
const auto tiles = flowWidget->findChildren<PublicDeckPreviewWidget *>();
|
||||
for (PublicDeckPreviewWidget *tile : tiles) {
|
||||
tile->setScaleFactor(scale);
|
||||
}
|
||||
flowWidget->setMinimumSizeToMaxSizeHint();
|
||||
}
|
||||
|
||||
void TabPublicDecks::openDeck(int deckId)
|
||||
{
|
||||
Command_DeckDownloadPublic cmd;
|
||||
cmd.set_deck_id(deckId);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabPublicDecks::openDeckFinished);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabPublicDecks::openDeckFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::warning(this, tr("Open public deck"),
|
||||
tr("Failed to open the public deck (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_DeckDownload &resp = response.GetExtension(Response_DeckDownload::ext);
|
||||
std::optional<LoadedDeck> deckOpt =
|
||||
DeckLoader::loadFromRemote(QString::fromStdString(resp.deck()), LoadedDeck::LoadInfo::NON_REMOTE_ID);
|
||||
if (!deckOpt) {
|
||||
QMessageBox::warning(this, tr("Open public deck"), tr("The public deck could not be parsed."));
|
||||
return;
|
||||
}
|
||||
|
||||
tabSupervisor->openDeckInNewTab(deckOpt.value());
|
||||
}
|
||||
79
cockatrice/src/interface/widgets/tabs/tab_public_decks.h
Normal file
79
cockatrice/src/interface/widgets/tabs/tab_public_decks.h
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* @file tab_public_decks.h
|
||||
* @ingroup Tabs
|
||||
*/
|
||||
|
||||
#ifndef TAB_PUBLIC_DECKS_H
|
||||
#define TAB_PUBLIC_DECKS_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class DeckPreviewColorIdentityFilterWidget;
|
||||
class FlowWidget;
|
||||
class PublicDeckPreviewWidget;
|
||||
class PublicDecksQuickSettingsWidget;
|
||||
class QLabel;
|
||||
class QToolButton;
|
||||
class RemotePublicDecksModel;
|
||||
class Response;
|
||||
class VisualDeckStorageSearchWidget;
|
||||
class VisualDeckStorageTagFilterWidget;
|
||||
|
||||
/**
|
||||
* @brief A visual grid of the public decks published by another user.
|
||||
*
|
||||
* The grid is rendered from the preview metadata the server stores for the
|
||||
* decks, so browsing costs no downloads; the deck list is fetched via
|
||||
* Command_DeckDownloadPublic only when the user opens a deck. Multiple users
|
||||
* can be browsed simultaneously; each gets its own tab.
|
||||
*/
|
||||
class TabPublicDecks final : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TabPublicDecks(TabSupervisor *tabSupervisor, AbstractClient *client, const QString &userName);
|
||||
|
||||
[[nodiscard]] QString getTabText() const override;
|
||||
void retranslateUi() override;
|
||||
bool closeRequest() override;
|
||||
|
||||
[[nodiscard]] QString getUserName() const
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
signals:
|
||||
void closing(TabPublicDecks *tab);
|
||||
|
||||
private slots:
|
||||
void openDeck(int deckId);
|
||||
void openDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||
void updateColorFilter();
|
||||
void updateTagFilter();
|
||||
void updateCardSize(int scale);
|
||||
void updateTagsVisibility(bool visible);
|
||||
void updateLoadingState(bool loading);
|
||||
|
||||
private:
|
||||
void rebuildGrid();
|
||||
void applyCardSize(int scale);
|
||||
|
||||
AbstractClient *client;
|
||||
QString userName;
|
||||
RemotePublicDecksModel *model;
|
||||
FlowWidget *flowWidget;
|
||||
VisualDeckStorageSearchWidget *searchWidget;
|
||||
DeckPreviewColorIdentityFilterWidget *colorIdentityFilter;
|
||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||
QToolButton *refreshButton;
|
||||
PublicDecksQuickSettingsWidget *quickSettingsWidget;
|
||||
QLabel *titleLabel;
|
||||
QLabel *statusLabel;
|
||||
QLabel *emptyLabel;
|
||||
int cardSize = 100;
|
||||
};
|
||||
|
||||
#endif // TAB_PUBLIC_DECKS_H
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
#include "tab_logs.h"
|
||||
#include "tab_message.h"
|
||||
#include "tab_moderation.h"
|
||||
#include "tab_public_decks.h"
|
||||
#include "tab_replays.h"
|
||||
#include "tab_report.h"
|
||||
#include "tab_room.h"
|
||||
|
|
@ -267,6 +268,10 @@ void TabSupervisor::retranslateUi()
|
|||
while (gameIterator.hasNext()) {
|
||||
tabs.append(gameIterator.next().value());
|
||||
}
|
||||
QMapIterator<QString, TabPublicDecks *> publicDecksIterator(publicDecksTabs);
|
||||
while (publicDecksIterator.hasNext()) {
|
||||
tabs.append(publicDecksIterator.next().value());
|
||||
}
|
||||
QListIterator<TabGame *> replayIterator(replayTabs);
|
||||
while (replayIterator.hasNext()) {
|
||||
tabs.append(replayIterator.next());
|
||||
|
|
@ -986,6 +991,30 @@ void TabSupervisor::roomLeft(TabRoom *tab)
|
|||
removeTab(indexOf(tab));
|
||||
}
|
||||
|
||||
void TabSupervisor::openTabPublicDecks(const QString &userName)
|
||||
{
|
||||
if (auto *existing = publicDecksTabs.value(userName, nullptr)) {
|
||||
setCurrentWidget(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
auto *tab = new TabPublicDecks(this, client, userName);
|
||||
connect(tab, &TabPublicDecks::closing, this, &TabSupervisor::publicDecksClosed);
|
||||
myAddTab(tab);
|
||||
publicDecksTabs.insert(userName, tab);
|
||||
setCurrentWidget(tab);
|
||||
}
|
||||
|
||||
void TabSupervisor::publicDecksClosed(TabPublicDecks *tab)
|
||||
{
|
||||
if (tab == currentWidget()) {
|
||||
emit setMenu();
|
||||
}
|
||||
|
||||
publicDecksTabs.remove(tab->getUserName());
|
||||
removeTab(indexOf(tab));
|
||||
}
|
||||
|
||||
void TabSupervisor::switchToFirstAvailableNetworkTab()
|
||||
{
|
||||
if (!roomTabs.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class TabModeration;
|
|||
class TabAccount;
|
||||
class TabDeckEditor;
|
||||
class TabLog;
|
||||
class TabPublicDecks;
|
||||
class RoomEvent;
|
||||
class GameEventContainer;
|
||||
class Event_GameJoined;
|
||||
|
|
@ -112,6 +113,7 @@ private:
|
|||
QMap<int, TabGame *> gameTabs;
|
||||
QList<TabGame *> replayTabs;
|
||||
QMap<QString, TabMessage *> messageTabs;
|
||||
QMap<QString, TabPublicDecks *> publicDecksTabs;
|
||||
QList<AbstractTabDeckEditor *> deckEditorTabs;
|
||||
bool isLocalGame;
|
||||
|
||||
|
|
@ -196,6 +198,7 @@ public slots:
|
|||
void actTabReplays(bool checked);
|
||||
void openTabServer();
|
||||
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
|
||||
void openTabPublicDecks(const QString &userName);
|
||||
private slots:
|
||||
void refreshShortcuts();
|
||||
|
||||
|
|
@ -226,6 +229,7 @@ private slots:
|
|||
void localGameJoined(const Event_GameJoined &event);
|
||||
void gameLeft(TabGame *tab);
|
||||
void roomLeft(TabRoom *tab);
|
||||
void publicDecksClosed(TabPublicDecks *tab);
|
||||
TabMessage *addMessageTab(const QString &userName, bool focus);
|
||||
void replayLeft(TabGame *tab);
|
||||
void processUserLeft(const QString &userName);
|
||||
|
|
|
|||
|
|
@ -169,7 +169,11 @@ void TabDeckStorageVisual::shareFinished(const Response &response, const Command
|
|||
|
||||
const Response_DeckShareCreate &resp = response.GetExtension(Response_DeckShareCreate::ext);
|
||||
const QString token = QString::fromStdString(resp.token());
|
||||
#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
|
||||
|
||||
const QString link = DeckShareUtils::buildShareLink(tabSupervisor->getClient(), token);
|
||||
DeckShareUtils::copyShareLinkToClipboard(link);
|
||||
|
|
@ -190,7 +194,7 @@ void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status)
|
|||
|
||||
void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning)
|
||||
{
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
|
||||
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Share link"), message,
|
||||
QMessageBox::Ok, this);
|
||||
box.exec();
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
#include "public_deck_preview_widget.h"
|
||||
|
||||
#include "../../../../client/settings/cache_settings.h"
|
||||
#include "../../cards/additional_info/color_identity_widget.h"
|
||||
#include "../../cards/deck_preview_card_picture_widget.h"
|
||||
#include "../../general/layout_containers/flow_widget.h"
|
||||
#include "deck_preview_tag_display_widget.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
|
||||
PublicDeckPreviewWidget::PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry)
|
||||
: QWidget(parent)
|
||||
{
|
||||
bannerCardDisplayWidget = new DeckPreviewCardPictureWidget(this);
|
||||
bannerCardDisplayWidget->setFontSize(24);
|
||||
|
||||
// The whole tile is a single focusable, keyboard-operable control: Tab lands
|
||||
// on it and Space/Enter opens the deck, mirroring the shared-deck preview tile.
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
uploadTimeLabel = new QLabel(this);
|
||||
uploadTimeLabel->setAlignment(Qt::AlignHCenter);
|
||||
|
||||
colorIdentityWidget = new ColorIdentityWidget(this);
|
||||
|
||||
tagsFlowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
|
||||
tagsFlowWidget->setSpacing(3, 3);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(bannerCardDisplayWidget);
|
||||
layout->addWidget(uploadTimeLabel);
|
||||
layout->addWidget(colorIdentityWidget);
|
||||
layout->addWidget(tagsFlowWidget);
|
||||
setLayout(layout);
|
||||
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowColorIdentityChanged, this,
|
||||
&PublicDeckPreviewWidget::updateColorIdentityVisibility);
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowTagsOnDeckPreviewsChanged, this,
|
||||
&PublicDeckPreviewWidget::updateTagsVisibility);
|
||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||
&VisualDeckStorageSettings::visualDeckStorageShowUploadTimeChanged, this,
|
||||
&PublicDeckPreviewWidget::updateUploadTimeVisibility);
|
||||
|
||||
setEntry(entry);
|
||||
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageClicked, this,
|
||||
&PublicDeckPreviewWidget::imageClickedEvent);
|
||||
connect(bannerCardDisplayWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||
&PublicDeckPreviewWidget::imageDoubleClickedEvent);
|
||||
|
||||
// resizeEvent clamps every child to the banner picture's width, so collect them
|
||||
// once here to keep the resize handler from searching the widget tree on every pass.
|
||||
fixedWidthChildren = {bannerCardDisplayWidget, uploadTimeLabel, colorIdentityWidget, tagsFlowWidget};
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
if (bannerCardDisplayWidget == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int width = bannerCardDisplayWidget->width();
|
||||
if (width == lastKnownBannerWidth) {
|
||||
return;
|
||||
}
|
||||
lastKnownBannerWidth = width;
|
||||
|
||||
for (QWidget *widget : fixedWidthChildren) {
|
||||
widget->setMaximumWidth(width);
|
||||
}
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::setEntry(const RemotePublicDecksModel::DeckEntry &entry)
|
||||
{
|
||||
deckId = entry.id;
|
||||
|
||||
hasColorIdentity = !entry.colorIdentity.isEmpty();
|
||||
colorIdentityWidget->setColorIdentity(entry.colorIdentity);
|
||||
updateColorIdentityVisibility();
|
||||
|
||||
const ExactCard bannerCard =
|
||||
entry.bannerCardName.isEmpty()
|
||||
? ExactCard()
|
||||
: CardDatabaseManager::query()->getCard(CardRef{entry.bannerCardName, entry.bannerCardProvider});
|
||||
bannerCardDisplayWidget->setCard(bannerCard);
|
||||
|
||||
// The deck name is the overlay text on the banner, like the local preview.
|
||||
bannerCardDisplayWidget->setOverlayText(entry.name);
|
||||
setToolTip(entry.name);
|
||||
setBaseAccessibleName(entry.name);
|
||||
|
||||
tagsFlowWidget->clearLayout();
|
||||
for (const QString &tag : entry.tags) {
|
||||
auto *chip = new DeckPreviewTagDisplayWidget(tagsFlowWidget, tag);
|
||||
chip->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
tagsFlowWidget->addWidget(chip);
|
||||
}
|
||||
hasTags = !entry.tags.isEmpty();
|
||||
updateTagsVisibility();
|
||||
|
||||
uploadTimeLabel->setText(tr("Uploaded %1").arg(entry.uploadTime.toString(Qt::TextDate)));
|
||||
hasUploadTime = !entry.uploadTime.isNull();
|
||||
updateUploadTimeVisibility();
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::updateColorIdentityVisibility()
|
||||
{
|
||||
colorIdentityWidget->setVisible(
|
||||
hasColorIdentity && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowColorIdentity());
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::updateTagsVisibility()
|
||||
{
|
||||
tagsFlowWidget->setVisible(
|
||||
hasTags && SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagsOnDeckPreviews());
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::updateUploadTimeVisibility()
|
||||
{
|
||||
uploadTimeLabel->setVisible(hasUploadTime &&
|
||||
SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowUploadTime());
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
|
||||
event->accept();
|
||||
emit openDeckRequested(deckId);
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::setBaseAccessibleName(const QString &name)
|
||||
{
|
||||
baseAccessibleName = name;
|
||||
setAccessibleName(name);
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::setScaleFactor(int scale)
|
||||
{
|
||||
bannerCardDisplayWidget->setScaleFactor(scale);
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::imageClickedEvent(QMouseEvent * /*event*/, DeckPreviewCardPictureWidget * /*instance*/)
|
||||
{
|
||||
// Reserved: clicking could show a card popup for the banner card.
|
||||
}
|
||||
|
||||
void PublicDeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent * /*event*/,
|
||||
DeckPreviewCardPictureWidget * /*instance*/)
|
||||
{
|
||||
emit openDeckRequested(deckId);
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file public_deck_preview_widget.h
|
||||
* @ingroup VisualDeckPreviewWidgets
|
||||
*/
|
||||
|
||||
#ifndef PUBLIC_DECK_PREVIEW_WIDGET_H
|
||||
#define PUBLIC_DECK_PREVIEW_WIDGET_H
|
||||
|
||||
#include "../remote_public_decks_model.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class ColorIdentityWidget;
|
||||
class DeckPreviewCardPictureWidget;
|
||||
class FlowWidget;
|
||||
class QKeyEvent;
|
||||
class QLabel;
|
||||
class QMouseEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
/**
|
||||
* @brief A preview tile for a public deck published by another user.
|
||||
*
|
||||
* Renders the banner card picture (looked up by name/provider in the card
|
||||
* database) with the deck name overlaid, the color identity, the deck's tags
|
||||
* (read-only) and its upload time, all from the metadata the server stores for
|
||||
* the deck, so no deck list is downloaded until the user actually opens the
|
||||
* deck. Double-clicking the banner requests opening it.
|
||||
*/
|
||||
class PublicDeckPreviewWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PublicDeckPreviewWidget(QWidget *parent, const RemotePublicDecksModel::DeckEntry &entry);
|
||||
|
||||
void setEntry(const RemotePublicDecksModel::DeckEntry &entry);
|
||||
|
||||
/** @brief Sets the accessible name announced to assistive technologies. */
|
||||
void setBaseAccessibleName(const QString &name);
|
||||
|
||||
/** @brief Scales the banner card picture, mirroring the Visual Deck Storage. */
|
||||
void setScaleFactor(int scale);
|
||||
|
||||
signals:
|
||||
void openDeckRequested(int deckId);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||
void updateColorIdentityVisibility();
|
||||
void updateTagsVisibility();
|
||||
void updateUploadTimeVisibility();
|
||||
|
||||
private:
|
||||
int deckId = 0;
|
||||
QString baseAccessibleName;
|
||||
bool hasColorIdentity = false;
|
||||
bool hasTags = false;
|
||||
bool hasUploadTime = false;
|
||||
int lastKnownBannerWidth = 0;
|
||||
QList<QWidget *> fixedWidthChildren;
|
||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||
ColorIdentityWidget *colorIdentityWidget;
|
||||
FlowWidget *tagsFlowWidget;
|
||||
QLabel *uploadTimeLabel;
|
||||
};
|
||||
|
||||
#endif // PUBLIC_DECK_PREVIEW_WIDGET_H
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
#include "remote_public_decks_model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_list_other_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
RemotePublicDecksModel::RemotePublicDecksModel(AbstractClient *_client, QObject *parent)
|
||||
: QAbstractListModel(parent), client(_client)
|
||||
{
|
||||
}
|
||||
|
||||
int RemotePublicDecksModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : visibleIndices.size();
|
||||
}
|
||||
|
||||
QVariant RemotePublicDecksModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= visibleIndices.size()) {
|
||||
return QVariant();
|
||||
}
|
||||
if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
|
||||
return decks.at(visibleIndices.at(index.row())).name;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
RemotePublicDecksModel::DeckEntry RemotePublicDecksModel::entryAt(int row) const
|
||||
{
|
||||
if (row < 0 || row >= visibleIndices.size()) {
|
||||
return DeckEntry{};
|
||||
}
|
||||
return decks.at(visibleIndices.at(row));
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setSearchText(const QString &text)
|
||||
{
|
||||
searchText = text.trimmed();
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setColorFilter(VisualDeckStorageSortFilterProxyModel::FilterMode mode,
|
||||
const QSet<QChar> &colors)
|
||||
{
|
||||
colorFilterMode = mode;
|
||||
activeColors = colors;
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setTagFilter(const QSet<QString> &selected, const QSet<QString> &excluded)
|
||||
{
|
||||
includedTags = selected;
|
||||
excludedTags = excluded;
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
QSet<QString> RemotePublicDecksModel::allTags() const
|
||||
{
|
||||
QSet<QString> all;
|
||||
for (const DeckEntry &entry : decks) {
|
||||
all.unite(QSet<QString>(entry.tags.cbegin(), entry.tags.cend()));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::rebuildVisibleIndices()
|
||||
{
|
||||
QList<int> newIndices;
|
||||
newIndices.reserve(decks.size());
|
||||
for (int row = 0; row < decks.size(); ++row) {
|
||||
const DeckEntry &entry = decks.at(row);
|
||||
|
||||
if (!searchText.isEmpty() && !entry.name.contains(searchText, Qt::CaseInsensitive)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!activeColors.isEmpty()) {
|
||||
const QString &identity = entry.colorIdentity;
|
||||
bool colorMatch = true;
|
||||
switch (colorFilterMode) {
|
||||
case VisualDeckStorageSortFilterProxyModel::ExactMatch: {
|
||||
QSet<QChar> activeSet;
|
||||
for (const QChar &color : activeColors) {
|
||||
activeSet.insert(color.toUpper());
|
||||
}
|
||||
QSet<QChar> identitySet;
|
||||
for (const QChar &color : identity) {
|
||||
identitySet.insert(color.toUpper());
|
||||
}
|
||||
colorMatch = activeSet == identitySet;
|
||||
break;
|
||||
}
|
||||
case VisualDeckStorageSortFilterProxyModel::Includes:
|
||||
colorMatch = std::all_of(activeColors.begin(), activeColors.end(),
|
||||
[&identity](const QChar &color) { return identity.contains(color); });
|
||||
break;
|
||||
case VisualDeckStorageSortFilterProxyModel::Excludes:
|
||||
colorMatch = std::none_of(activeColors.begin(), activeColors.end(),
|
||||
[&identity](const QChar &color) { return identity.contains(color); });
|
||||
break;
|
||||
}
|
||||
if (!colorMatch) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!includedTags.isEmpty()) {
|
||||
const QSet<QString> entryTags(entry.tags.cbegin(), entry.tags.cend());
|
||||
bool hasAll = std::all_of(includedTags.begin(), includedTags.end(),
|
||||
[&entryTags](const QString &tag) { return entryTags.contains(tag); });
|
||||
if (!hasAll) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!excludedTags.isEmpty() && std::any_of(excludedTags.begin(), excludedTags.end(),
|
||||
[&entry](const QString &tag) { return entry.tags.contains(tag); })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newIndices.append(row);
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
visibleIndices = newIndices;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::refresh(const QString &userName)
|
||||
{
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
Command_DeckListOtherUser cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &RemotePublicDecksModel::decksReceived);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::clear()
|
||||
{
|
||||
decks.clear();
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::setLoading(bool value)
|
||||
{
|
||||
if (loading == value) {
|
||||
return;
|
||||
}
|
||||
loading = value;
|
||||
emit loadingChanged(loading);
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::decksReceived(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||
{
|
||||
setLoading(false);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
emit loadFailed(tr("Failed to load the user's public decks (server response code %1).")
|
||||
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_DeckList &resp = response.GetExtension(Response_DeckList::ext);
|
||||
decks.clear();
|
||||
addFolder(resp.root());
|
||||
rebuildVisibleIndices();
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::addFolder(const ServerInfo_DeckStorage_Folder &folder)
|
||||
{
|
||||
const int itemCount = folder.items_size();
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
addTreeItem(folder.items(i));
|
||||
}
|
||||
}
|
||||
|
||||
void RemotePublicDecksModel::addTreeItem(const ServerInfo_DeckStorage_TreeItem &item)
|
||||
{
|
||||
if (item.has_folder()) {
|
||||
addFolder(item.folder());
|
||||
return;
|
||||
}
|
||||
|
||||
const ServerInfo_DeckStorage_File &file = item.file();
|
||||
DeckEntry entry;
|
||||
entry.id = item.id();
|
||||
entry.name = QString::fromStdString(item.name());
|
||||
entry.uploadTime = QDateTime::fromSecsSinceEpoch(file.creation_time());
|
||||
entry.bannerCardName = QString::fromStdString(file.banner_card_name());
|
||||
entry.bannerCardProvider = QString::fromStdString(file.banner_card_provider());
|
||||
entry.colorIdentity = QString::fromStdString(file.color_identity());
|
||||
const QString tagsString = QString::fromStdString(file.tags());
|
||||
entry.tags = tagsString.split(QLatin1Char(','), Qt::SkipEmptyParts);
|
||||
decks.append(entry);
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* @file remote_public_decks_model.h
|
||||
* @ingroup DeckStorageWidgets
|
||||
*/
|
||||
|
||||
#ifndef REMOTE_PUBLIC_DECKS_MODEL_H
|
||||
#define REMOTE_PUBLIC_DECKS_MODEL_H
|
||||
|
||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
class AbstractClient;
|
||||
class CommandContainer;
|
||||
class Response;
|
||||
class ServerInfo_DeckStorage_Folder;
|
||||
class ServerInfo_DeckStorage_TreeItem;
|
||||
|
||||
/**
|
||||
* @brief Flat, read-only list of the public decks published by another user.
|
||||
*
|
||||
* Fetches the target user's public decks via Command_DeckListOtherUser and
|
||||
* flattens the response tree into entries carrying the preview metadata stored
|
||||
* on the server (banner card name/provider and color identity). No deck list is
|
||||
* downloaded until the user actually opens a deck.
|
||||
*
|
||||
* Name and color-identity filtering is applied against this metadata, mirroring
|
||||
* the Visual Deck Storage's filter semantics, so the grid can be narrowed like
|
||||
* the local deck storage.
|
||||
*/
|
||||
class RemotePublicDecksModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct DeckEntry
|
||||
{
|
||||
int id = 0;
|
||||
QString name;
|
||||
QDateTime uploadTime;
|
||||
QString bannerCardName;
|
||||
QString bannerCardProvider;
|
||||
QString colorIdentity;
|
||||
QStringList tags;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The color identity filter mode, shared with the Visual Deck Storage.
|
||||
*/
|
||||
using FilterMode = VisualDeckStorageSortFilterProxyModel::FilterMode;
|
||||
|
||||
explicit RemotePublicDecksModel(AbstractClient *client, QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
[[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
/** @brief Fetches the public decks of another user, replacing the current contents. */
|
||||
void refresh(const QString &userName);
|
||||
void clear();
|
||||
|
||||
/** @brief Sets a case-insensitive substring filter on the deck name. */
|
||||
void setSearchText(const QString &text);
|
||||
|
||||
/** @brief Sets the active color identity filter and mode. */
|
||||
void setColorFilter(FilterMode mode, const QSet<QChar> &colors);
|
||||
|
||||
/** @brief Filters decks by required (`selected`) and forbidden (`excluded`) tags. */
|
||||
void setTagFilter(const QSet<QString> &selected, const QSet<QString> &excluded);
|
||||
|
||||
/** @brief All tags present across all loaded decks, for building filter chips. */
|
||||
[[nodiscard]] QSet<QString> allTags() const;
|
||||
|
||||
/** @brief The number of decks after filtering. */
|
||||
[[nodiscard]] int filteredCount() const
|
||||
{
|
||||
return visibleIndices.size();
|
||||
}
|
||||
|
||||
/** @brief The number of decks before filtering. */
|
||||
[[nodiscard]] int totalCount() const
|
||||
{
|
||||
return decks.size();
|
||||
}
|
||||
|
||||
/** @brief True while a refresh request is in flight and the grid has no data yet. */
|
||||
[[nodiscard]] bool isLoading() const
|
||||
{
|
||||
return loading;
|
||||
}
|
||||
|
||||
[[nodiscard]] DeckEntry entryAt(int row) const;
|
||||
|
||||
signals:
|
||||
/** @brief Emitted when a refresh starts, completes, or fails (see loading()). */
|
||||
void loadingChanged(bool loading);
|
||||
|
||||
/** @brief Emitted when the last refresh failed; contains a user-facing message. */
|
||||
void loadFailed(const QString &message);
|
||||
|
||||
private slots:
|
||||
void decksReceived(const Response &response, const CommandContainer &commandContainer);
|
||||
|
||||
private:
|
||||
void addFolder(const ServerInfo_DeckStorage_Folder &folder);
|
||||
void addTreeItem(const ServerInfo_DeckStorage_TreeItem &item);
|
||||
void rebuildVisibleIndices();
|
||||
void setLoading(bool value);
|
||||
|
||||
AbstractClient *client;
|
||||
QList<DeckEntry> decks;
|
||||
QList<int> visibleIndices; ///< Row indices into `decks` that pass the current filters.
|
||||
bool loading = false;
|
||||
|
||||
QString searchText;
|
||||
VisualDeckStorageSortFilterProxyModel::FilterMode colorFilterMode = VisualDeckStorageSortFilterProxyModel::Includes;
|
||||
QSet<QChar> activeColors;
|
||||
QSet<QString> includedTags;
|
||||
QSet<QString> excludedTags;
|
||||
};
|
||||
|
||||
#endif // REMOTE_PUBLIC_DECKS_MODEL_H
|
||||
|
|
@ -507,6 +507,7 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
|
||||
connectionController = new ConnectionController(this, this);
|
||||
urlParser = new IntentUrlParser(this, this);
|
||||
connect(urlParser, &IntentUrlParser::urlChainFinished, this, &MainWindow::onUrlChainFinished);
|
||||
|
||||
createActions();
|
||||
createMenus();
|
||||
|
|
@ -722,6 +723,7 @@ void MainWindow::applyStartupDestination()
|
|||
|
||||
connect(credentials, &Intent::finished, connector, &Intent::execute);
|
||||
connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed);
|
||||
connect(credentials, &Intent::cancelled, this, [this]() { startupDestinationFailed(tr("Sign-in cancelled")); });
|
||||
connect(connector, &Intent::finished, this,
|
||||
[this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); });
|
||||
connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed);
|
||||
|
|
@ -862,18 +864,7 @@ void MainWindow::changeEvent(QEvent *event)
|
|||
} else if (event->type() == QEvent::ActivationChange) {
|
||||
if (isActiveWindow() && !bHasActivated) {
|
||||
bHasActivated = true;
|
||||
if (!connectTo.isEmpty()) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo;
|
||||
connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(),
|
||||
connectTo.password());
|
||||
} else if (SettingsCache::instance().servers().getAutoConnect() &&
|
||||
!SettingsCache::instance().debug().getLocalGameOnStartup() &&
|
||||
!startupDestinationConnectsToServer()) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
|
||||
DlgConnect dlg(this);
|
||||
connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
|
||||
dlg.getPlayerName(), dlg.getPassword());
|
||||
}
|
||||
attemptStartupAutoConnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -899,6 +890,40 @@ void MainWindow::handleCockatriceLink(const QString &url)
|
|||
urlParser->handle(url);
|
||||
}
|
||||
|
||||
void MainWindow::attemptStartupAutoConnect()
|
||||
{
|
||||
if (startupAutoConnectAttempted || skipStartupAutoConnect) {
|
||||
return;
|
||||
}
|
||||
startupAutoConnectAttempted = true;
|
||||
|
||||
if (!connectTo.isEmpty()) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo;
|
||||
connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(),
|
||||
connectTo.password());
|
||||
} else if (SettingsCache::instance().servers().getAutoConnect() &&
|
||||
!SettingsCache::instance().debug().getLocalGameOnStartup() && !startupDestinationConnectsToServer()) {
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
|
||||
DlgConnect dlg(this);
|
||||
connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
|
||||
dlg.getPlayerName(), dlg.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onUrlChainFinished(bool connected)
|
||||
{
|
||||
// A cockatrice:// link owns the startup connection while it runs. When its
|
||||
// chain ended without connecting (declined, invalid, offline), fall back to
|
||||
// the startup connection so the activation launch still behaves like a
|
||||
// normal launch.
|
||||
if (connected || !skipStartupAutoConnect || getRemoteClient()->getStatus() != StatusDisconnected) {
|
||||
return;
|
||||
}
|
||||
qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup connect";
|
||||
skipStartupAutoConnect = false;
|
||||
attemptStartupAutoConnect();
|
||||
}
|
||||
|
||||
void MainWindow::cardDatabaseLoadingFailed()
|
||||
{
|
||||
if (askedForDbUpdater) {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public slots:
|
|||
void actCheckClientUpdates();
|
||||
void actConnect();
|
||||
void actExit();
|
||||
void handleCockatriceLink(const QString &url);
|
||||
private slots:
|
||||
void updateTabMenu(const QList<QMenu *> &newMenuList);
|
||||
void statusChanged(ClientStatus _status);
|
||||
|
|
@ -92,7 +93,7 @@ private slots:
|
|||
void actOpenSettingsFolder();
|
||||
void actShow();
|
||||
void showWindowIfHidden();
|
||||
void handleCockatriceLink(const QString &url);
|
||||
void onUrlChainFinished(bool connected);
|
||||
|
||||
void cardUpdateError(QProcess::ProcessError err);
|
||||
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
|
|
@ -120,6 +121,8 @@ private slots:
|
|||
void startupDestinationFailed(const QString &reason);
|
||||
[[nodiscard]] bool startupDestinationConnectsToServer() const;
|
||||
|
||||
void attemptStartupAutoConnect();
|
||||
|
||||
private:
|
||||
static const QString appName;
|
||||
static const QStringList fileNameFilters;
|
||||
|
|
@ -158,6 +161,8 @@ private:
|
|||
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
||||
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
|
||||
bool bHasActivated, askedForDbUpdater;
|
||||
bool skipStartupAutoConnect = false;
|
||||
bool startupAutoConnectAttempted = false;
|
||||
QProcess *cardUpdateProcess;
|
||||
DlgViewLog *logviewDialog;
|
||||
GameReplay *replay;
|
||||
|
|
@ -170,6 +175,16 @@ public:
|
|||
{
|
||||
connectTo = QUrl(QString("cockatrice://%1").arg(url));
|
||||
}
|
||||
// When set, the window's own startup connection (--connect or auto-connect
|
||||
// on first activation) is skipped. Used for activation launches: the intent
|
||||
// chain triggered by a cockatrice:// URL owns the connection, and letting
|
||||
// auto-connect race against it caused two connectToServer calls to tear
|
||||
// each other down. onUrlChainFinished() clears this and retries the startup
|
||||
// connection when the link's chain ended without connecting.
|
||||
void setSkipStartupAutoConnect(bool skip)
|
||||
{
|
||||
skipStartupAutoConnect = skip;
|
||||
}
|
||||
~MainWindow() override;
|
||||
|
||||
RemoteClient *getRemoteClient() const
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
#include "client/url_scheme_event_filter.h"
|
||||
#include "database/interface/settings_card_preference_provider.h"
|
||||
#include "interface/intents/intent_open_local_deck.h"
|
||||
#include "interface/intents/url_parser.h"
|
||||
#include "interface/logger.h"
|
||||
#include "interface/pixel_map_generator.h"
|
||||
#include "interface/theme_manager.h"
|
||||
|
|
@ -45,6 +44,7 @@
|
|||
#include <QMessageBox>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QTranslator>
|
||||
#include <algorithm>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/rng/rng_sfmt.h>
|
||||
#include <libcockatrice/settings/appearance_settings.h>
|
||||
|
|
@ -273,10 +273,12 @@ int main(int argc, char *argv[])
|
|||
SingleInstanceManager instance;
|
||||
|
||||
if (hasActivationFiles) {
|
||||
qInfo() << "Activation launch, files:" << startupFiles;
|
||||
// Activation launch: hand off to the primary instance if one is
|
||||
// running, otherwise become the primary ourselves. Do this before
|
||||
// constructing the main window so a hand-off exits cheaply.
|
||||
if (!instance.tryRun(startupFiles)) {
|
||||
qInfo() << "Handed off to a running instance, exiting";
|
||||
// Sent successfully → exit
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -325,10 +327,22 @@ int main(int argc, char *argv[])
|
|||
|
||||
MainWindow ui;
|
||||
|
||||
// A URL launch must own the connection: the intent chain triggered by the
|
||||
// URL connects to the server named in the URL, so the window's own startup
|
||||
// auto-connect must not race against it (two connectToServer calls tear
|
||||
// each other down via doDisconnectFromServer).
|
||||
const bool hasUrlActivation = std::any_of(startupFiles.begin(), startupFiles.end(), [](const QString &file) {
|
||||
return file.startsWith(QStringLiteral("cockatrice://"));
|
||||
});
|
||||
ui.setSkipStartupAutoConnect(hasUrlActivation);
|
||||
|
||||
auto handleActivation = [&ui](const QString &file) {
|
||||
if (file.startsWith("cockatrice://")) {
|
||||
auto urlParser = new IntentUrlParser(&ui, &ui);
|
||||
urlParser->handle(file);
|
||||
qInfo() << "Handling URL activation:" << file;
|
||||
// Route through the window's persistent url parser: it serializes
|
||||
// link chains so activations handed over while another chain is
|
||||
// still connecting do not connect concurrently.
|
||||
ui.handleCockatriceLink(file);
|
||||
} else if (QFileInfo(file).exists()) {
|
||||
auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
|
||||
QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
#include <QDir>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Sent by the primary instance after it has read a forwarded payload. Without
|
||||
// an acknowledgment, a second instance cannot tell a live primary apart from a
|
||||
// stale socket left behind by a process that is still shutting down.
|
||||
const QByteArray ACK_MESSAGE = QByteArrayLiteral("COCKATRICE_ACK");
|
||||
} // namespace
|
||||
|
||||
SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
|
@ -72,7 +80,14 @@ bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend)
|
|||
socket.flush();
|
||||
socket.waitForBytesWritten(1000);
|
||||
|
||||
return true;
|
||||
// Only report a successful hand-off once the primary has acknowledged that
|
||||
// it actually read the payload. A socket that connects but never answers
|
||||
// belongs to a process that is dying, so the caller must not treat this as
|
||||
// a hand-off (otherwise it would exit without anyone handling the files).
|
||||
if (!socket.waitForReadyRead(1000)) {
|
||||
return false;
|
||||
}
|
||||
return socket.readAll() == ACK_MESSAGE;
|
||||
}
|
||||
|
||||
void SingleInstanceManager::handleNewConnection()
|
||||
|
|
@ -111,6 +126,14 @@ void SingleInstanceManager::handleNewConnection()
|
|||
QStringList files;
|
||||
payloadStream >> files;
|
||||
|
||||
// Acknowledge receipt as soon as the payload is parsed, before the
|
||||
// primary starts handling it. The handlers run synchronously and can
|
||||
// take longer than the sender's readiness timeout (e.g. a modal
|
||||
// confirmation box), which would otherwise make a live primary look
|
||||
// dead and cause duplicate handling.
|
||||
socket->write(ACK_MESSAGE);
|
||||
socket->flush();
|
||||
|
||||
emit filesReceived(files);
|
||||
|
||||
// Reset buffer (single message use-case)
|
||||
|
|
|
|||
|
|
@ -130,6 +130,11 @@ bool VisualDeckStorageSettings::getVisualDeckStorageShowTagsOnDeckPreviews() con
|
|||
return getValue("showTagsOnDeckPreviews", "interface", "visualDeckStorage", true).toBool();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageSettings::getVisualDeckStorageShowUploadTime() const
|
||||
{
|
||||
return getValue("showUploadTime", "interface", "visualDeckStorage", true).toBool();
|
||||
}
|
||||
|
||||
bool VisualDeckStorageSettings::getVisualDeckStorageDrawUnusedColorIdentities() const
|
||||
{
|
||||
return getValue("drawUnusedColorIdentities", "interface", "visualDeckStorage", true).toBool();
|
||||
|
|
@ -220,6 +225,12 @@ void VisualDeckStorageSettings::setVisualDeckStorageShowTagsOnDeckPreviews(bool
|
|||
emit visualDeckStorageShowTagsOnDeckPreviewsChanged(_showTags);
|
||||
}
|
||||
|
||||
void VisualDeckStorageSettings::setVisualDeckStorageShowUploadTime(bool value)
|
||||
{
|
||||
setValue(value, "showUploadTime", "interface", "visualDeckStorage");
|
||||
emit visualDeckStorageShowUploadTimeChanged(value);
|
||||
}
|
||||
|
||||
void VisualDeckStorageSettings::setVisualDeckStorageDrawUnusedColorIdentities(bool _draw)
|
||||
{
|
||||
setValue(_draw, "drawUnusedColorIdentities", "interface", "visualDeckStorage");
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public:
|
|||
[[nodiscard]] bool getVisualDeckStorageShowColorIdentity() const override;
|
||||
[[nodiscard]] bool getVisualDeckStorageShowBannerCardComboBox() const override;
|
||||
[[nodiscard]] bool getVisualDeckStorageShowTagsOnDeckPreviews() const override;
|
||||
[[nodiscard]] bool getVisualDeckStorageShowUploadTime() const;
|
||||
[[nodiscard]] bool getVisualDeckStorageDrawUnusedColorIdentities() const override;
|
||||
[[nodiscard]] int getVisualDeckStorageUnusedColorIdentitiesOpacity() const override;
|
||||
[[nodiscard]] int getVisualDeckStorageTooltipType() const override;
|
||||
|
|
@ -38,6 +39,7 @@ public:
|
|||
void setVisualDeckStorageShowColorIdentity(bool value);
|
||||
void setVisualDeckStorageShowBannerCardComboBox(bool _showBannerCardComboBox);
|
||||
void setVisualDeckStorageShowTagsOnDeckPreviews(bool _showTags);
|
||||
void setVisualDeckStorageShowUploadTime(bool value);
|
||||
void setVisualDeckStorageDrawUnusedColorIdentities(bool _draw);
|
||||
void setVisualDeckStorageUnusedColorIdentitiesOpacity(int _opacity);
|
||||
void setVisualDeckStorageTooltipType(int value);
|
||||
|
|
@ -54,6 +56,7 @@ signals:
|
|||
void visualDeckStorageShowColorIdentityChanged(bool _visible);
|
||||
void visualDeckStorageShowBannerCardComboBoxChanged(bool _visible);
|
||||
void visualDeckStorageShowTagsOnDeckPreviewsChanged(bool _visible);
|
||||
void visualDeckStorageShowUploadTimeChanged(bool _visible);
|
||||
void visualDeckStorageDrawUnusedColorIdentitiesChanged(bool _visible);
|
||||
void visualDeckStorageUnusedColorIdentitiesOpacityChanged(bool value);
|
||||
void visualDeckStorageInGameChanged(bool enabled);
|
||||
|
|
|
|||
|
|
@ -571,6 +571,19 @@ TEST_F(SettingsDefaultsTest, VisualDeckStorage_DefaultTagsList_SetAndGet)
|
|||
ASSERT_EQ(s.getVisualDeckStorageDefaultTagsList(), custom);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_Default)
|
||||
{
|
||||
VisualDeckStorageSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, VisualDeckStorage_ShowUploadTime_SetAndGet)
|
||||
{
|
||||
VisualDeckStorageSettings s(settingsPath, nullptr);
|
||||
s.setVisualDeckStorageShowUploadTime(false);
|
||||
ASSERT_EQ(s.getVisualDeckStorageShowUploadTime(), false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue