mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
Compare commits
6 commits
fcaf94553d
...
7faa2fca13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7faa2fca13 | ||
|
|
812f2a88ea | ||
|
|
0bfa89b5d3 | ||
|
|
ad6d33c1e2 | ||
|
|
71febfa21d | ||
|
|
d18e28a65b |
78 changed files with 3361 additions and 177 deletions
|
|
@ -45,11 +45,14 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
||||||
src/interface/widgets/dialogs/dlg_local_game_options.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_manage_sets.cpp
|
||||||
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||||
src/interface/widgets/dialogs/dlg_register.cpp
|
src/interface/widgets/dialogs/dlg_register.cpp
|
||||||
src/interface/widgets/dialogs/dlg_report_user.cpp
|
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.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_settings.cpp
|
||||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||||
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
src/interface/widgets/dialogs/dlg_tip_of_the_day.cpp
|
||||||
|
|
@ -57,6 +60,9 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_view_log.cpp
|
src/interface/widgets/dialogs/dlg_view_log.cpp
|
||||||
src/interface/widgets/dialogs/override_printing_warning.cpp
|
src/interface/widgets/dialogs/override_printing_warning.cpp
|
||||||
src/interface/widgets/dialogs/tip_of_the_day.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/deck_filter_string.cpp
|
||||||
src/filters/filter_builder.cpp
|
src/filters/filter_builder.cpp
|
||||||
src/filters/filter_tree_model.cpp
|
src/filters/filter_tree_model.cpp
|
||||||
|
|
@ -163,6 +169,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/palette_editor/palette_grid_widget.cpp
|
src/interface/palette_editor/palette_grid_widget.cpp
|
||||||
src/interface/palette_editor/palette_editor_dialog.cpp
|
src/interface/palette_editor/palette_editor_dialog.cpp
|
||||||
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
src/interface/widgets/cards/additional_info/color_identity_widget.cpp
|
||||||
|
src/interface/widgets/cards/additional_info/deck_color_identity.cpp
|
||||||
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_cost_widget.cpp
|
||||||
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
src/interface/widgets/cards/additional_info/mana_symbol_widget.cpp
|
||||||
src/interface/widgets/cards/art_crop_attribution.cpp
|
src/interface/widgets/cards/art_crop_attribution.cpp
|
||||||
|
|
@ -441,6 +448,8 @@ set(cockatrice_SOURCES
|
||||||
src/interface/intents/intent_login.h
|
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.cpp
|
||||||
src/interface/intents/intent_open_server_room_by_name.h
|
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.cpp
|
||||||
src/interface/intents/url_parser.h
|
src/interface/intents/url_parser.h
|
||||||
src/interface/widgets/server/user/user_info_popup.cpp
|
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)
|
Intent::Intent(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
// An intent is done as soon as it reports success or failure. Deleting it
|
// An intent is done as soon as it reports success, failure, or cancellation.
|
||||||
// also tears down its dependency chain and disconnects any signal wiring.
|
// Deleting it also tears down its dependency chain and disconnects any signal wiring.
|
||||||
connect(this, &Intent::finished, this, &QObject::deleteLater);
|
connect(this, &Intent::finished, this, &QObject::deleteLater);
|
||||||
connect(this, &Intent::failed, this, &QObject::deleteLater);
|
connect(this, &Intent::failed, this, &QObject::deleteLater);
|
||||||
|
connect(this, &Intent::cancelled, this, &QObject::deleteLater);
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent::~Intent() = default;
|
Intent::~Intent() = default;
|
||||||
|
|
@ -46,3 +47,11 @@ void Intent::emitFailed(const QString &reason)
|
||||||
emit failed(reason);
|
emit failed(reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Intent::emitCancelled()
|
||||||
|
{
|
||||||
|
if (!completed) {
|
||||||
|
completed = true;
|
||||||
|
emit cancelled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public:
|
||||||
signals:
|
signals:
|
||||||
void finished();
|
void finished();
|
||||||
void failed(QString reason);
|
void failed(QString reason);
|
||||||
|
void cancelled();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// --- Subclasses must implement these ---
|
// --- Subclasses must implement these ---
|
||||||
|
|
@ -29,6 +30,7 @@ protected:
|
||||||
// Emit the outcome exactly once; ignore late signals after the intent is done.
|
// Emit the outcome exactly once; ignore late signals after the intent is done.
|
||||||
void emitFinished();
|
void emitFinished();
|
||||||
void emitFailed(const QString &reason);
|
void emitFailed(const QString &reason);
|
||||||
|
void emitCancelled();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool completed = false;
|
bool completed = false;
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,10 @@ bool IntentJoinServerGame::checkPrecondition() const
|
||||||
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// peerPort() reflects the actual TCP peer, which may differ from the
|
// serverName() reflects the server the client was configured to connect to,
|
||||||
// configured server port (e.g. when connecting through a proxy), so only
|
// which may differ from the actual TCP peer (e.g. when connecting through a
|
||||||
// the hostname is compared here.
|
// proxy), so only the hostname is compared here.
|
||||||
if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
|
if (remoteClient->serverName().compare(context->roomContext.serverContext.hostname, Qt::CaseInsensitive) != 0) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
#include "intent_login.h"
|
#include "intent_login.h"
|
||||||
|
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
|
#include "../widgets/dialogs/dlg_login_prompt.h"
|
||||||
#include "libcockatrice/settings/servers_settings.h"
|
#include "libcockatrice/settings/servers_settings.h"
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
|
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -29,5 +32,27 @@ void IntentGetLoginCredentials::onPreconditionSatisfied()
|
||||||
|
|
||||||
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
|
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 "url_parser.h"
|
||||||
|
|
||||||
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "../widgets/tabs/tab_room.h"
|
#include "../widgets/tabs/tab_room.h"
|
||||||
#include "../widgets/tabs/tab_supervisor.h"
|
#include "../widgets/tabs/tab_supervisor.h"
|
||||||
#include "../window_main.h"
|
#include "../window_main.h"
|
||||||
#include "contexts/context_join_game.h"
|
#include "contexts/context_join_game.h"
|
||||||
|
#include "contexts/context_open_deck.h"
|
||||||
|
#include "intent.h"
|
||||||
#include "intent_join_server_game.h"
|
#include "intent_join_server_game.h"
|
||||||
#include "intent_login.h"
|
#include "intent_login.h"
|
||||||
|
#include "intent_open_shared_deck.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/settings/servers_settings.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
inline Q_LOGGING_CATEGORY(UrlParserLog, "url_parser");
|
||||||
|
|
||||||
IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
|
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();
|
const QString action = url.host();
|
||||||
QUrlQuery query(url);
|
QUrlQuery query(url);
|
||||||
|
|
||||||
|
qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action;
|
||||||
|
|
||||||
|
QList<Intent *> chain;
|
||||||
|
Intent *firstIntent = nullptr;
|
||||||
if (action == "joingame") {
|
if (action == "joingame") {
|
||||||
handleJoinGame(query);
|
firstIntent = createJoinGameIntent(query, chain);
|
||||||
} else if (action == "opendeck") {
|
} else if (action == "opendeck") {
|
||||||
// handleOpenDeck(query);
|
firstIntent = createOpenDeckIntent(query, chain);
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Unknown intent:" << action;
|
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); };
|
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()) {
|
if (ctx->roomContext.serverContext.hostname.isEmpty()) {
|
||||||
showError(tr("Missing or empty hostname in the game link"));
|
showError(tr("Missing or empty hostname in the game link"));
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
ctx->roomContext.serverContext.port.toUShort(&ok);
|
ctx->roomContext.serverContext.port.toUShort(&ok);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showError(tr("Invalid or missing port in the game link"));
|
showError(tr("Invalid or missing port in the game link"));
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
|
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showError(tr("Invalid or missing room id in the game link"));
|
showError(tr("Invalid or missing room id in the game link"));
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = false;
|
ok = false;
|
||||||
|
|
@ -71,7 +97,7 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
showError(tr("Invalid or missing game id in the game link"));
|
showError(tr("Invalid or missing game id in the game link"));
|
||||||
return;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
|
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
|
||||||
|
|
@ -80,24 +106,32 @@ void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
|
||||||
const QMessageBox::StandardButton answer = QMessageBox::question(
|
const QMessageBox::StandardButton answer = QMessageBox::question(
|
||||||
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
|
||||||
if (answer != 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
|
// The join game intent owns the context and the credential lookup; once the
|
||||||
// chain finishes (or fails) it deletes the whole tree.
|
// chain finishes (or fails) it deletes the whole tree.
|
||||||
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
|
auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), client, std::move(ctx));
|
||||||
auto joinGameIntent =
|
|
||||||
new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx));
|
|
||||||
joinGameIntent->setParent(this);
|
joinGameIntent->setParent(this);
|
||||||
|
chain.append(joinGameIntent);
|
||||||
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
|
|
||||||
getLoginCredentialsIntent->setParent(joinGameIntent);
|
|
||||||
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
|
|
||||||
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
|
|
||||||
connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
|
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)
|
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)
|
.arg(gameDescription, gameIdStr, roomTab->getRoomName(), server)
|
||||||
: tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, 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
|
#ifndef COCKATRICE_URL_PARSER_H
|
||||||
#define COCKATRICE_URL_PARSER_H
|
#define COCKATRICE_URL_PARSER_H
|
||||||
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
class Intent;
|
||||||
class MainWindow;
|
class MainWindow;
|
||||||
struct ContextJoinGame;
|
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
|
class IntentUrlParser : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -12,12 +24,34 @@ class IntentUrlParser : public QObject
|
||||||
public:
|
public:
|
||||||
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
|
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
|
||||||
void handle(const QString &urlStr);
|
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:
|
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);
|
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;
|
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
|
#endif // COCKATRICE_URL_PARSER_H
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||||
}
|
}
|
||||||
lastWidth = totalWidth;
|
lastWidth = totalWidth;
|
||||||
|
|
||||||
const int totalHeight = totalWidth / 6; // Set height to 1/4 of the width
|
const int totalHeight = qMax(0, totalWidth / 6); // Set height to 1/4 of the width
|
||||||
setFixedHeight(totalHeight);
|
setFixedHeight(totalHeight);
|
||||||
|
|
||||||
const int count = layout->count();
|
const int count = layout->count();
|
||||||
|
|
@ -97,6 +97,10 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
|
||||||
const int availableWidth = totalWidth - (spacing * (count - 1));
|
const int availableWidth = totalWidth - (spacing * (count - 1));
|
||||||
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
const int iconSize = qMin(availableWidth / count, totalHeight); // Ensure icons fit within the new height
|
||||||
|
|
||||||
|
if (iconSize <= 0) {
|
||||||
|
lastIconSize = iconSize;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (iconSize == lastIconSize) {
|
if (iconSize == lastIconSize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
#include "deck_color_identity.h"
|
||||||
|
|
||||||
|
#include <QSet>
|
||||||
|
#include <libcockatrice/card/database/card_database_querier.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
|
||||||
|
|
||||||
|
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db)
|
||||||
|
{
|
||||||
|
const QStringList cardList = deck.getCardList({DECK_ZONE_MAIN, DECK_ZONE_SIDE});
|
||||||
|
if (cardList.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
|
||||||
|
|
||||||
|
for (const QString &cardName : cardList) {
|
||||||
|
CardInfoPtr currentCard = db->getCardInfo(cardName);
|
||||||
|
if (currentCard) {
|
||||||
|
const QString colors = currentCard->getColors(); // returns something like "WUB"
|
||||||
|
for (const QChar &color : colors) {
|
||||||
|
colorSet.insert(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the color identity is in WUBRG order
|
||||||
|
QString colorIdentity;
|
||||||
|
const QString wubrgOrder = "WUBRG";
|
||||||
|
for (const QChar &color : wubrgOrder) {
|
||||||
|
if (colorSet.contains(color)) {
|
||||||
|
colorIdentity.append(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorIdentity;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
#ifndef COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||||
|
#define COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class CardDatabaseQuerier;
|
||||||
|
class DeckList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
|
||||||
|
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
|
||||||
|
*
|
||||||
|
* Shared as a free function so the deck storage previews and the deck share
|
||||||
|
* dialog compute identities identically.
|
||||||
|
*
|
||||||
|
* @param db Card database used to look up card color symbols.
|
||||||
|
*/
|
||||||
|
QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db);
|
||||||
|
|
||||||
|
#endif // COCKATRICE_DECK_COLOR_IDENTITY_H
|
||||||
|
|
@ -133,12 +133,14 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
|
||||||
path.addRoundedRect(glowRect, radius, radius);
|
path.addRoundedRect(glowRect, radius, radius);
|
||||||
|
|
||||||
// Soft outer glow
|
// 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.setPen(QPen(glowColor, 6));
|
||||||
painter.drawPath(path);
|
painter.drawPath(path);
|
||||||
|
|
||||||
// Thin inner border for crispness
|
// 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.setPen(QPen(borderColor, 2));
|
||||||
painter.drawRoundedRect(pixmapRect, radius, radius);
|
painter.drawRoundedRect(pixmapRect, radius, radius);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,23 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
||||||
const QColor &textColor,
|
const QColor &textColor,
|
||||||
const QColor &outlineColor,
|
const QColor &outlineColor,
|
||||||
const int fontSize,
|
const int fontSize,
|
||||||
const Qt::Alignment alignment)
|
const Qt::Alignment alignment,
|
||||||
|
const bool _emitClickImmediately)
|
||||||
: CardInfoPictureWithTextOverlayWidget(parent,
|
: CardInfoPictureWithTextOverlayWidget(parent,
|
||||||
hoverToZoomEnabled,
|
hoverToZoomEnabled,
|
||||||
raiseOnEnter,
|
raiseOnEnter,
|
||||||
textColor,
|
textColor,
|
||||||
outlineColor,
|
outlineColor,
|
||||||
fontSize,
|
fontSize,
|
||||||
alignment)
|
alignment),
|
||||||
|
emitClickImmediately(_emitClickImmediately)
|
||||||
{
|
{
|
||||||
singleClickTimer = new QTimer(this);
|
singleClickTimer = new QTimer(this);
|
||||||
singleClickTimer->setSingleShot(true);
|
singleClickTimer->setSingleShot(true);
|
||||||
connect(singleClickTimer, &QTimer::timeout, this, [this]() { emit imageClicked(lastMouseEvent, this); });
|
connect(singleClickTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
emit imageClicked(lastMouseEvent, this);
|
||||||
|
emit imageSingleClicked();
|
||||||
|
});
|
||||||
connect(&SettingsCache::instance().visualDeckStorage(),
|
connect(&SettingsCache::instance().visualDeckStorage(),
|
||||||
&VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this,
|
&VisualDeckStorageSettings::visualDeckStorageSelectionAnimationChanged, this,
|
||||||
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
|
&CardInfoPictureWidget::setRaiseOnEnterEnabled);
|
||||||
|
|
@ -47,8 +52,13 @@ DeckPreviewCardPictureWidget::DeckPreviewCardPictureWidget(QWidget *parent,
|
||||||
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton) {
|
||||||
lastMouseEvent = event;
|
if (emitClickImmediately) {
|
||||||
singleClickTimer->start(QApplication::doubleClickInterval());
|
emit imageClicked(event, this);
|
||||||
|
emit imageSingleClicked();
|
||||||
|
} else {
|
||||||
|
lastMouseEvent = event;
|
||||||
|
singleClickTimer->start(QApplication::doubleClickInterval());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emit imageClicked(event, this);
|
emit imageClicked(event, this);
|
||||||
event->accept();
|
event->accept();
|
||||||
|
|
@ -58,7 +68,14 @@ void DeckPreviewCardPictureWidget::mousePressEvent(QMouseEvent *event)
|
||||||
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
void DeckPreviewCardPictureWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
if (event->button() == Qt::LeftButton) {
|
if (event->button() == Qt::LeftButton) {
|
||||||
singleClickTimer->stop(); // Prevent single-click logic
|
if (emitClickImmediately) {
|
||||||
emit imageDoubleClicked(lastMouseEvent, this);
|
// 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,21 +20,38 @@ class DeckPreviewCardPictureWidget final : public CardInfoPictureWithTextOverlay
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
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,
|
explicit DeckPreviewCardPictureWidget(QWidget *parent,
|
||||||
bool hoverToZoomEnabled = false,
|
bool hoverToZoomEnabled = false,
|
||||||
bool raiseOnEnter = false,
|
bool raiseOnEnter = false,
|
||||||
const QColor &textColor = Qt::white,
|
const QColor &textColor = Qt::white,
|
||||||
const QColor &outlineColor = Qt::black,
|
const QColor &outlineColor = Qt::black,
|
||||||
int fontSize = 12,
|
int fontSize = 12,
|
||||||
Qt::Alignment alignment = Qt::AlignCenter);
|
Qt::Alignment alignment = Qt::AlignCenter,
|
||||||
|
bool _emitClickImmediately = false);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
void imageClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||||
|
void imageSingleClicked();
|
||||||
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
void imageDoubleClicked(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTimer *singleClickTimer;
|
QTimer *singleClickTimer;
|
||||||
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
|
QMouseEvent *lastMouseEvent = nullptr; // Store the last mouse event
|
||||||
|
bool emitClickImmediately;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#include "deck_share_utils.h"
|
||||||
|
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QTimeZone>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
|
||||||
|
namespace DeckShareUtils
|
||||||
|
{
|
||||||
|
|
||||||
|
QString buildShareLink(const AbstractClient *client, const QString &token)
|
||||||
|
{
|
||||||
|
return QString("cockatrice://opendeck?share=%1&hostname=%2&port=%3")
|
||||||
|
.arg(token, client->serverName(), QString::number(client->serverPort()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString copyShareLinkToClipboard(const QString &link)
|
||||||
|
{
|
||||||
|
QGuiApplication::clipboard()->setText(link);
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString formatShareExpiry(const QDateTime &expiry)
|
||||||
|
{
|
||||||
|
return expiry.toLocalTime().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace DeckShareUtils
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @file deck_share_utils.h
|
||||||
|
* @ingroup DeckShareWidgets
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef DECK_SHARE_UTILS_H
|
||||||
|
#define DECK_SHARE_UTILS_H
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class AbstractClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shared helpers for creating temporary deck shares.
|
||||||
|
*/
|
||||||
|
namespace DeckShareUtils
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Builds the cockatrice:// link for a freshly created deck share.
|
||||||
|
* @param client Used to embed the target server's hostname and port.
|
||||||
|
* @param token The share token from Response_DeckShareCreate.
|
||||||
|
*/
|
||||||
|
QString buildShareLink(const AbstractClient *client, const QString &token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Copies the share link to the clipboard.
|
||||||
|
* @return The link that was copied.
|
||||||
|
*/
|
||||||
|
QString copyShareLinkToClipboard(const QString &link);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Formats the expiration timestamp for a share.
|
||||||
|
*/
|
||||||
|
QString formatShareExpiry(const QDateTime &expiry);
|
||||||
|
|
||||||
|
} // namespace DeckShareUtils
|
||||||
|
|
||||||
|
#endif // DECK_SHARE_UTILS_H
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
#include "share_bar_widget.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
|
ShareBarWidget::ShareBarWidget(QWidget *parent) : QWidget(parent)
|
||||||
|
{
|
||||||
|
auto *layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(12, 10, 12, 10);
|
||||||
|
layout->setSpacing(8);
|
||||||
|
|
||||||
|
hintLabel = new QLabel(this);
|
||||||
|
hintLabel->setWordWrap(true);
|
||||||
|
|
||||||
|
nameEdit = new QLineEdit(this);
|
||||||
|
nameEdit->setMaximumWidth(260);
|
||||||
|
|
||||||
|
countLabel = new QLabel(this);
|
||||||
|
|
||||||
|
cancelButton = new QPushButton(this);
|
||||||
|
connect(cancelButton, &QPushButton::clicked, this, &ShareBarWidget::cancelRequested);
|
||||||
|
|
||||||
|
createButton = new QPushButton(this);
|
||||||
|
createButton->setDefault(true);
|
||||||
|
connect(createButton, &QPushButton::clicked, this, &ShareBarWidget::createRequested);
|
||||||
|
|
||||||
|
layout->addWidget(hintLabel, 1);
|
||||||
|
layout->addWidget(nameEdit);
|
||||||
|
layout->addWidget(countLabel);
|
||||||
|
layout->addStretch();
|
||||||
|
layout->addWidget(cancelButton);
|
||||||
|
layout->addWidget(createButton);
|
||||||
|
|
||||||
|
setLayout(layout);
|
||||||
|
|
||||||
|
retranslateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareBarWidget::retranslateUi()
|
||||||
|
{
|
||||||
|
nameEdit->setPlaceholderText(tr("Share name"));
|
||||||
|
cancelButton->setText(tr("Cancel"));
|
||||||
|
createButton->setText(tr("Create share link"));
|
||||||
|
hintLabel->setText(tr("Click deck tiles to select the decks you want to share."));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ShareBarWidget::name() const
|
||||||
|
{
|
||||||
|
return nameEdit->text().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareBarWidget::setName(const QString &value)
|
||||||
|
{
|
||||||
|
nameEdit->setText(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareBarWidget::setCountText(const QString &text)
|
||||||
|
{
|
||||||
|
countLabel->setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareBarWidget::setHintText(const QString &text, bool visible)
|
||||||
|
{
|
||||||
|
hintLabel->setText(text);
|
||||||
|
hintLabel->setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareBarWidget::setCreateEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
createButton->setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShareBarWidget::focusName()
|
||||||
|
{
|
||||||
|
nameEdit->setFocus();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* @file share_bar_widget.h
|
||||||
|
* @ingroup DeckShareWidgets
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef SHARE_BAR_WIDGET_H
|
||||||
|
#define SHARE_BAR_WIDGET_H
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QLineEdit;
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The activated toolbar used to create a temporary deck share.
|
||||||
|
*
|
||||||
|
* A single reusable component shared by the local visual deck storage and the
|
||||||
|
* remote server deck storage tabs, so the share workflow renders identically in
|
||||||
|
* both places. It owns its own widgets, strings, and layout; the owning tab only
|
||||||
|
* sets the count/hint text and reacts to the create/cancel signals.
|
||||||
|
*/
|
||||||
|
class ShareBarWidget final : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ShareBarWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void retranslateUi();
|
||||||
|
|
||||||
|
/** @return The trimmed name entered by the user. */
|
||||||
|
[[nodiscard]] QString name() const;
|
||||||
|
|
||||||
|
/** @brief Resets the name field to the given default. */
|
||||||
|
void setName(const QString &name);
|
||||||
|
|
||||||
|
/** @brief Sets the selected-count summary label text. */
|
||||||
|
void setCountText(const QString &text);
|
||||||
|
|
||||||
|
/** @brief Sets the explainer hint text, showing it when @p visible is true. */
|
||||||
|
void setHintText(const QString &text, bool visible);
|
||||||
|
|
||||||
|
/** @brief Enables or disables the create-share-link button (guards double submission). */
|
||||||
|
void setCreateEnabled(bool enabled);
|
||||||
|
|
||||||
|
/** @brief Moves keyboard focus to the name field. */
|
||||||
|
void focusName();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void createRequested();
|
||||||
|
void cancelRequested();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel *hintLabel;
|
||||||
|
QLineEdit *nameEdit;
|
||||||
|
QLabel *countLabel;
|
||||||
|
QPushButton *cancelButton;
|
||||||
|
QPushButton *createButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SHARE_BAR_WIDGET_H
|
||||||
|
|
@ -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
|
||||||
89
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
Normal file
89
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#include "dlg_share_deck.h"
|
||||||
|
|
||||||
|
#include "../cards/additional_info/deck_color_identity.h"
|
||||||
|
#include "../deck_share/deck_share_utils.h"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QTimeZone>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
|
|
||||||
|
DlgShareDeck::DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *_parent)
|
||||||
|
: QDialog(_parent), client(_client), deck(_deck)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Share deck"));
|
||||||
|
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
nameEdit = new QLineEdit(this);
|
||||||
|
nameEdit->setText(tr("Shared deck"));
|
||||||
|
|
||||||
|
auto *form = new QFormLayout;
|
||||||
|
form->addRow(tr("Share name:"), nameEdit);
|
||||||
|
layout->addLayout(form);
|
||||||
|
|
||||||
|
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Create share link"));
|
||||||
|
buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
|
||||||
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgShareDeck::actShare);
|
||||||
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &DlgShareDeck::reject);
|
||||||
|
this->buttonBox = buttonBox;
|
||||||
|
layout->addWidget(buttonBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DlgShareDeck::actShare()
|
||||||
|
{
|
||||||
|
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
|
||||||
|
Command_DeckShareCreate cmd;
|
||||||
|
cmd.set_name(nameEdit->text().trimmed().toStdString());
|
||||||
|
if (cmd.name().empty()) {
|
||||||
|
cmd.set_name(tr("Shared deck").toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckShareItem *item = cmd.add_items();
|
||||||
|
item->set_deck_list(deck->writeToString_Native().toStdString());
|
||||||
|
item->set_color_identity(getDeckColorIdentity(*deck, CardDatabaseManager::query()).toStdString());
|
||||||
|
|
||||||
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &DlgShareDeck::shareFinished);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DlgShareDeck::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||||
|
{
|
||||||
|
if (response.response_code() != Response::RespOk) {
|
||||||
|
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||||
|
QMessageBox::critical(this, tr("Share deck"),
|
||||||
|
tr("Failed to create the share link (server response code %1).")
|
||||||
|
.arg(QString::number(static_cast<int>(response.response_code()))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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);
|
||||||
|
|
||||||
|
QMessageBox::information(this, tr("Share deck"),
|
||||||
|
tr("Share link created and copied to the clipboard:\n\n%1\n\n"
|
||||||
|
"The share expires on %2.")
|
||||||
|
.arg(link, DeckShareUtils::formatShareExpiry(expiry)));
|
||||||
|
accept();
|
||||||
|
}
|
||||||
43
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
Normal file
43
cockatrice/src/interface/widgets/dialogs/dlg_share_deck.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @file dlg_share_deck.h
|
||||||
|
* @ingroup Dialogs
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef DLG_SHARE_DECK_H
|
||||||
|
#define DLG_SHARE_DECK_H
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
class AbstractClient;
|
||||||
|
class CommandContainer;
|
||||||
|
class DeckList;
|
||||||
|
class QDialogButtonBox;
|
||||||
|
class QLineEdit;
|
||||||
|
class Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Slim dialog to create a temporary share for the deck open in the editor.
|
||||||
|
*
|
||||||
|
* Asks for a share name, sends Command_DeckShareCreate for the single inline
|
||||||
|
* deck, and copies the resulting link to the clipboard.
|
||||||
|
*/
|
||||||
|
class DlgShareDeck : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
DlgShareDeck(AbstractClient *_client, const QSharedPointer<DeckList> &_deck, QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void actShare();
|
||||||
|
void shareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
AbstractClient *client;
|
||||||
|
QSharedPointer<DeckList> deck;
|
||||||
|
QLineEdit *nameEdit;
|
||||||
|
QDialogButtonBox *buttonBox;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DLG_SHARE_DECK_H
|
||||||
|
|
@ -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 "flow_widget.h"
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QKeyEvent>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QSizePolicy>
|
#include <QSizePolicy>
|
||||||
|
|
@ -177,6 +178,50 @@ QLayoutItem *FlowWidget::itemAt(int index) const
|
||||||
return flowLayout->itemAt(index);
|
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
|
int FlowWidget::count() const
|
||||||
{
|
{
|
||||||
return flowLayout->count();
|
return flowLayout->count();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "../../../layouts/flow_layout.h"
|
#include "../../../layouts/flow_layout.h"
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QKeyEvent>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
@ -44,6 +45,7 @@ public slots:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Qt::Orientation flowDirection;
|
Qt::Orientation flowDirection;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
|
||||||
aSaveDeckAs = new QAction(QString(), this);
|
aSaveDeckAs = new QAction(QString(), this);
|
||||||
connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs);
|
connect(aSaveDeckAs, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actSaveDeckAs);
|
||||||
|
|
||||||
|
aShareDeck = new QAction(QString(), this);
|
||||||
|
connect(aShareDeck, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actShareDeck);
|
||||||
|
|
||||||
aLoadDeckFromClipboard = new QAction(QString(), this);
|
aLoadDeckFromClipboard = new QAction(QString(), this);
|
||||||
connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard);
|
connect(aLoadDeckFromClipboard, &QAction::triggered, deckEditor, &AbstractTabDeckEditor::actLoadDeckFromClipboard);
|
||||||
|
|
||||||
|
|
@ -96,6 +99,7 @@ DeckEditorMenu::DeckEditorMenu(AbstractTabDeckEditor *parent) : QMenu(parent), d
|
||||||
addMenu(loadRecentDeckMenu);
|
addMenu(loadRecentDeckMenu);
|
||||||
addAction(aSaveDeck);
|
addAction(aSaveDeck);
|
||||||
addAction(aSaveDeckAs);
|
addAction(aSaveDeckAs);
|
||||||
|
addAction(aShareDeck);
|
||||||
addSeparator();
|
addSeparator();
|
||||||
addAction(aLoadDeckFromClipboard);
|
addAction(aLoadDeckFromClipboard);
|
||||||
addMenu(editDeckInClipboardMenu);
|
addMenu(editDeckInClipboardMenu);
|
||||||
|
|
@ -120,6 +124,7 @@ void DeckEditorMenu::setSaveStatus(bool newStatus)
|
||||||
{
|
{
|
||||||
aSaveDeck->setEnabled(newStatus);
|
aSaveDeck->setEnabled(newStatus);
|
||||||
aSaveDeckAs->setEnabled(newStatus);
|
aSaveDeckAs->setEnabled(newStatus);
|
||||||
|
aShareDeck->setEnabled(newStatus);
|
||||||
aSaveDeckToClipboard->setEnabled(newStatus);
|
aSaveDeckToClipboard->setEnabled(newStatus);
|
||||||
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
|
aSaveDeckToClipboardNoSetInfo->setEnabled(newStatus);
|
||||||
aSaveDeckToClipboardRaw->setEnabled(newStatus);
|
aSaveDeckToClipboardRaw->setEnabled(newStatus);
|
||||||
|
|
@ -157,6 +162,7 @@ void DeckEditorMenu::retranslateUi()
|
||||||
aClearRecents->setText(tr("Clear"));
|
aClearRecents->setText(tr("Clear"));
|
||||||
aSaveDeck->setText(tr("&Save deck"));
|
aSaveDeck->setText(tr("&Save deck"));
|
||||||
aSaveDeckAs->setText(tr("Save deck &as..."));
|
aSaveDeckAs->setText(tr("Save deck &as..."));
|
||||||
|
aShareDeck->setText(tr("Share deck..."));
|
||||||
|
|
||||||
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
|
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ public:
|
||||||
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
|
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
|
||||||
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
|
*aEditDeckInClipboard, *aEditDeckInClipboardRaw, *aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetInfo,
|
||||||
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
|
*aSaveDeckToClipboardRaw, *aSaveDeckToClipboardRawNoSetInfo, *aPrintDeck, *aLoadDeckFromWebsite,
|
||||||
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aClose;
|
*aExportDeckDecklist, *aExportDeckDecklistXyz, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout, *aShareDeck,
|
||||||
|
*aClose;
|
||||||
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
|
QMenu *loadRecentDeckMenu, *analyzeDeckMenu, *editDeckInClipboardMenu, *saveDeckToClipboardMenu;
|
||||||
|
|
||||||
void setSaveStatus(bool newStatus);
|
void setSaveStatus(bool newStatus);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
#include "../interface/widgets/dialogs/dlg_load_deck.h"
|
#include "../interface/widgets/dialogs/dlg_load_deck.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
|
#include "../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
#include "../interface/widgets/dialogs/dlg_load_deck_from_website.h"
|
||||||
|
#include "../interface/widgets/dialogs/dlg_share_deck.h"
|
||||||
#include "../utility/visibility_change_listener.h"
|
#include "../utility/visibility_change_listener.h"
|
||||||
#include "tab_supervisor.h"
|
#include "tab_supervisor.h"
|
||||||
|
|
||||||
|
|
@ -382,6 +383,25 @@ bool AbstractTabDeckEditor::actSaveDeckAs()
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opens the deck share dialog with the current deck preselected.
|
||||||
|
*/
|
||||||
|
void AbstractTabDeckEditor::actShareDeck()
|
||||||
|
{
|
||||||
|
if (tabSupervisor->getClient()->getStatus() != StatusLoggedIn) {
|
||||||
|
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QSharedPointer<DeckList> deck = deckStateManager->getDeckListShared();
|
||||||
|
if (deck->isBlankDeck()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DlgShareDeck shareDialog(tabSupervisor->getClient(), deck, this);
|
||||||
|
shareDialog.exec();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback for remote deck save completion.
|
* @brief Callback for remote deck save completion.
|
||||||
* @param response Server response.
|
* @param response Server response.
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,9 @@ protected slots:
|
||||||
/** @brief Saves the current deck under a new name. */
|
/** @brief Saves the current deck under a new name. */
|
||||||
virtual bool actSaveDeckAs();
|
virtual bool actSaveDeckAs();
|
||||||
|
|
||||||
|
/** @brief Opens the deck share dialog for the current deck. */
|
||||||
|
void actShareDeck();
|
||||||
|
|
||||||
/** @brief Loads a deck from the clipboard. */
|
/** @brief Loads a deck from the clipboard. */
|
||||||
virtual void actLoadDeckFromClipboard();
|
virtual void actLoadDeckFromClipboard();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,24 @@
|
||||||
|
|
||||||
#include "../../../client/settings/cache_settings.h"
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../deck_loader/deck_loader.h"
|
#include "../../deck_loader/deck_loader.h"
|
||||||
|
#include "../deck_share/deck_share_utils.h"
|
||||||
|
#include "../deck_share/share_bar_widget.h"
|
||||||
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
|
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.h"
|
||||||
#include "../interface/widgets/utility/get_text_with_max.h"
|
#include "../interface/widgets/utility/get_text_with_max.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QFileSystemModel>
|
#include <QFileSystemModel>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QTimeZone>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QTreeView>
|
#include <QTreeView>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
@ -23,9 +29,11 @@
|
||||||
#include <libcockatrice/protocol/pb/command_deck_del_dir.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_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pending_command.h>
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
#include <libcockatrice/settings/paths_settings.h>
|
#include <libcockatrice/settings/paths_settings.h>
|
||||||
|
|
@ -91,8 +99,17 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
serverDirView = new RemoteDeckList_TreeWidget(client);
|
serverDirView = new RemoteDeckList_TreeWidget(client);
|
||||||
|
|
||||||
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
|
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
|
||||||
|
connect(serverDirView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||||
|
[this] { onServerSelectionChanged(); });
|
||||||
|
|
||||||
|
// Share bar for creating a share link from the selected server decks/folders.
|
||||||
|
shareBar = new ShareBarWidget(this);
|
||||||
|
connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorage::actShareSelection);
|
||||||
|
connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorage::cancelShareDecks);
|
||||||
|
shareBar->setVisible(false);
|
||||||
|
|
||||||
QVBoxLayout *rightVbox = new QVBoxLayout;
|
QVBoxLayout *rightVbox = new QVBoxLayout;
|
||||||
|
rightVbox->addWidget(shareBar);
|
||||||
rightVbox->addWidget(serverDirView);
|
rightVbox->addWidget(serverDirView);
|
||||||
rightVbox->addLayout(rightToolBarLayout);
|
rightVbox->addLayout(rightToolBarLayout);
|
||||||
rightGroupBox = new QGroupBox;
|
rightGroupBox = new QGroupBox;
|
||||||
|
|
@ -138,6 +155,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
|
||||||
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
|
connect(aDeleteRemoteDeck, &QAction::triggered, this, &TabDeckStorage::actDeleteRemoteDeck);
|
||||||
|
|
||||||
|
aShareDecks = new QAction(this);
|
||||||
|
aShareDecks->setIcon(QPixmap("theme:icons/share"));
|
||||||
|
connect(aShareDecks, &QAction::triggered, this, &TabDeckStorage::actShareDecks);
|
||||||
|
|
||||||
// Add actions to toolbars
|
// Add actions to toolbars
|
||||||
leftToolBar->addAction(aOpenLocalDeck);
|
leftToolBar->addAction(aOpenLocalDeck);
|
||||||
leftToolBar->addAction(aRenameLocal);
|
leftToolBar->addAction(aRenameLocal);
|
||||||
|
|
@ -149,6 +170,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
|
|
||||||
rightToolBar->addAction(aOpenRemoteDeck);
|
rightToolBar->addAction(aOpenRemoteDeck);
|
||||||
rightToolBar->addAction(aDownload);
|
rightToolBar->addAction(aDownload);
|
||||||
|
rightToolBar->addAction(aShareDecks);
|
||||||
rightToolBar->addAction(aNewFolder);
|
rightToolBar->addAction(aNewFolder);
|
||||||
rightToolBar->addAction(aDeleteRemoteDeck);
|
rightToolBar->addAction(aDeleteRemoteDeck);
|
||||||
|
|
||||||
|
|
@ -177,7 +199,9 @@ void TabDeckStorage::retranslateUi()
|
||||||
aNewFolder->setText(tr("New folder"));
|
aNewFolder->setText(tr("New folder"));
|
||||||
aDeleteLocalDeck->setText(tr("Delete"));
|
aDeleteLocalDeck->setText(tr("Delete"));
|
||||||
aDeleteRemoteDeck->setText(tr("Delete"));
|
aDeleteRemoteDeck->setText(tr("Delete"));
|
||||||
|
aShareDecks->setText(tr("Share decks"));
|
||||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||||
|
shareBar->retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString TabDeckStorage::getTargetPath() const
|
QString TabDeckStorage::getTargetPath() const
|
||||||
|
|
@ -219,12 +243,14 @@ void TabDeckStorage::setRemoteEnabled(bool enabled)
|
||||||
aUpload->setEnabled(enabled);
|
aUpload->setEnabled(enabled);
|
||||||
aOpenRemoteDeck->setEnabled(enabled);
|
aOpenRemoteDeck->setEnabled(enabled);
|
||||||
aDownload->setEnabled(enabled);
|
aDownload->setEnabled(enabled);
|
||||||
|
aShareDecks->setEnabled(enabled);
|
||||||
aNewFolder->setEnabled(enabled);
|
aNewFolder->setEnabled(enabled);
|
||||||
aDeleteRemoteDeck->setEnabled(enabled);
|
aDeleteRemoteDeck->setEnabled(enabled);
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
serverDirView->refreshTree();
|
serverDirView->refreshTree();
|
||||||
} else {
|
} else {
|
||||||
|
setShareModeEnabled(false);
|
||||||
serverDirView->clearTree();
|
serverDirView->clearTree();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -625,3 +651,151 @@ void TabDeckStorage::deleteFolderFinished(const Response &response, const Comman
|
||||||
serverDirView->removeNode(toDelete);
|
serverDirView->removeNode(toDelete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::actShareDecks()
|
||||||
|
{
|
||||||
|
setShareModeEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::cancelShareDecks()
|
||||||
|
{
|
||||||
|
setShareModeEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::setShareModeEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
shareBar->setVisible(enabled);
|
||||||
|
if (enabled) {
|
||||||
|
shareBar->setCreateEnabled(true);
|
||||||
|
shareBar->setName(tr("Shared decks"));
|
||||||
|
onServerSelectionChanged();
|
||||||
|
shareBar->focusName();
|
||||||
|
} else {
|
||||||
|
serverDirView->clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::onServerSelectionChanged()
|
||||||
|
{
|
||||||
|
if (!shareBar->isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto selection = serverDirView->getCurrentSelection();
|
||||||
|
int folders = 0;
|
||||||
|
int files = 0;
|
||||||
|
for (const auto *node : selection) {
|
||||||
|
if (dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||||
|
++folders;
|
||||||
|
} else {
|
||||||
|
++files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString hint;
|
||||||
|
if (folders > 1) {
|
||||||
|
hint = tr("Only one folder can be shared at a time.");
|
||||||
|
} else if (folders > 0 && files > 0) {
|
||||||
|
hint = tr("Share either a folder or decks, not both.");
|
||||||
|
} else if (folders == 0 && files == 0) {
|
||||||
|
hint = tr("Select folders or decks in the tree to share.");
|
||||||
|
}
|
||||||
|
shareBar->setHintText(hint, !hint.isEmpty());
|
||||||
|
|
||||||
|
QStringList parts;
|
||||||
|
if (folders > 0) {
|
||||||
|
parts << tr("%n folder(s)", "", folders);
|
||||||
|
}
|
||||||
|
if (files > 0) {
|
||||||
|
parts << tr("%n deck(s)", "", files);
|
||||||
|
}
|
||||||
|
shareBar->setCountText(parts.isEmpty() ? tr("No decks selected") : tr("Selected: %1").arg(parts.join(tr(", "))));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::actShareSelection()
|
||||||
|
{
|
||||||
|
const auto selection = serverDirView->getCurrentSelection();
|
||||||
|
QString sharedFolder;
|
||||||
|
bool hasFile = false;
|
||||||
|
bool hasFolder = false;
|
||||||
|
for (const auto *node : selection) {
|
||||||
|
if (const auto *dirNode = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
|
||||||
|
hasFolder = true;
|
||||||
|
if (!sharedFolder.isEmpty()) {
|
||||||
|
showShareNotice(tr("Only one folder can be shared at a time."), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sharedFolder = dirNode->getPath();
|
||||||
|
} else {
|
||||||
|
hasFile = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFile && hasFolder) {
|
||||||
|
showShareNotice(tr("Share either a folder or decks, not both."), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasFolder && sharedFolder.isEmpty()) {
|
||||||
|
showShareNotice(tr("The root folder cannot be shared."), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command_DeckShareCreate cmd;
|
||||||
|
cmd.set_name(shareBar->name().toStdString());
|
||||||
|
if (cmd.name().empty()) {
|
||||||
|
cmd.set_name(tr("Shared decks").toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sharedFolder.isEmpty()) {
|
||||||
|
cmd.set_folder_path(sharedFolder.toStdString());
|
||||||
|
} else {
|
||||||
|
for (const auto *node : selection) {
|
||||||
|
if (const auto *fileNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(node)) {
|
||||||
|
DeckShareItem *item = cmd.add_items();
|
||||||
|
item->set_deck_id(fileNode->getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.items_size() == 0 && cmd.folder_path().empty()) {
|
||||||
|
showShareNotice(tr("Select decks to share."), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shareBar->setCreateEnabled(false);
|
||||||
|
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &TabDeckStorage::shareFromTreeFinished);
|
||||||
|
client->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::shareFromTreeFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||||
|
{
|
||||||
|
shareBar->setCreateEnabled(true);
|
||||||
|
if (response.response_code() != Response::RespOk) {
|
||||||
|
qWarning() << "failed to create deck share:" << response.response_code();
|
||||||
|
showShareNotice(tr("Failed to create the share link (server response code %1).")
|
||||||
|
.arg(QString::number(static_cast<int>(response.response_code()))),
|
||||||
|
true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const 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);
|
||||||
|
|
||||||
|
showShareNotice(
|
||||||
|
tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(expiry)));
|
||||||
|
setShareModeEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::showShareNotice(const QString &message, bool warning)
|
||||||
|
{
|
||||||
|
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
|
||||||
|
QMessageBox::Ok, this);
|
||||||
|
box.exec();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class QTreeWidgetItem;
|
||||||
class QGroupBox;
|
class QGroupBox;
|
||||||
class CommandContainer;
|
class CommandContainer;
|
||||||
class Response;
|
class Response;
|
||||||
|
class ShareBarWidget;
|
||||||
|
|
||||||
class TabDeckStorage : public Tab
|
class TabDeckStorage : public Tab
|
||||||
{
|
{
|
||||||
|
|
@ -35,14 +36,19 @@ private:
|
||||||
QToolBar *leftToolBar, *rightToolBar;
|
QToolBar *leftToolBar, *rightToolBar;
|
||||||
RemoteDeckList_TreeWidget *serverDirView;
|
RemoteDeckList_TreeWidget *serverDirView;
|
||||||
QGroupBox *leftGroupBox, *rightGroupBox;
|
QGroupBox *leftGroupBox, *rightGroupBox;
|
||||||
|
ShareBarWidget *shareBar;
|
||||||
|
|
||||||
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
QAction *aOpenLocalDeck, *aRenameLocal, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
|
||||||
QAction *aOpenDecksFolder;
|
QAction *aOpenDecksFolder;
|
||||||
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
|
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aNewFolder, *aDeleteRemoteDeck;
|
||||||
QString getTargetPath() const;
|
QString getTargetPath() const;
|
||||||
|
|
||||||
void setRemoteEnabled(bool enabled);
|
void setRemoteEnabled(bool enabled);
|
||||||
|
|
||||||
|
void showShareNotice(const QString &message, bool warning = false);
|
||||||
|
|
||||||
|
void setShareModeEnabled(bool enabled);
|
||||||
|
|
||||||
void uploadDeck(const QString &filePath, const QString &targetPath);
|
void uploadDeck(const QString &filePath, const QString &targetPath);
|
||||||
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
|
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
|
||||||
|
|
||||||
|
|
@ -75,6 +81,12 @@ private slots:
|
||||||
void actNewFolder();
|
void actNewFolder();
|
||||||
void newFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
void newFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
|
|
||||||
|
void actShareDecks();
|
||||||
|
void actShareSelection();
|
||||||
|
void cancelShareDecks();
|
||||||
|
void onServerSelectionChanged();
|
||||||
|
void shareFromTreeFinished(const Response &r, const CommandContainer &commandContainer);
|
||||||
|
|
||||||
void actDeleteRemoteDeck();
|
void actDeleteRemoteDeck();
|
||||||
void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
void deleteFolderFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,21 @@
|
||||||
#include "tab_deck_storage_visual.h"
|
#include "tab_deck_storage_visual.h"
|
||||||
|
|
||||||
|
#include "../../../deck_loader/deck_loader.h"
|
||||||
|
#include "../../cards/additional_info/deck_color_identity.h"
|
||||||
|
#include "../../deck_share/deck_share_utils.h"
|
||||||
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
#include "../../interface/widgets/visual_deck_storage/visual_deck_storage_widget.h"
|
||||||
#include "../tab_supervisor.h"
|
#include "../tab_supervisor.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
|
|
||||||
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
||||||
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
|
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
|
||||||
|
|
@ -14,12 +25,42 @@ TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
|
||||||
&TabDeckStorageVisual::actOpenLocalDeck);
|
&TabDeckStorageVisual::actOpenLocalDeck);
|
||||||
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this,
|
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::openDeckEditor, this,
|
||||||
&TabDeckStorageVisual::openDeckEditor);
|
&TabDeckStorageVisual::openDeckEditor);
|
||||||
|
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareDeckRequested, this,
|
||||||
|
&TabDeckStorageVisual::actShareDeck);
|
||||||
|
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareSelectionChanged, this,
|
||||||
|
&TabDeckStorageVisual::onShareSelectionChanged);
|
||||||
|
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::shareRequested, this, [this] {
|
||||||
|
if (shareDeckAvailable) {
|
||||||
|
enterShareMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AbstractClient *client = tabSupervisor->getClient();
|
||||||
|
connect(client, &AbstractClient::statusChanged, this, &TabDeckStorageVisual::handleConnectionChanged);
|
||||||
|
shareDeckAvailable = (client->getStatus() == StatusLoggedIn);
|
||||||
|
visualDeckStorageWidget->setShareAvailable(shareDeckAvailable);
|
||||||
|
|
||||||
auto *widget = new QWidget(this);
|
auto *widget = new QWidget(this);
|
||||||
auto *layout = new QVBoxLayout(widget);
|
auto *layout = new QVBoxLayout(widget);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
widget->setLayout(layout);
|
widget->setLayout(layout);
|
||||||
this->setCentralWidget(widget);
|
this->setCentralWidget(widget);
|
||||||
layout->addWidget(visualDeckStorageWidget);
|
layout->addWidget(visualDeckStorageWidget);
|
||||||
|
|
||||||
|
shareBar = new ShareBarWidget(this);
|
||||||
|
connect(shareBar, &ShareBarWidget::createRequested, this, &TabDeckStorageVisual::actShareSelected);
|
||||||
|
connect(shareBar, &ShareBarWidget::cancelRequested, this, &TabDeckStorageVisual::exitShareMode);
|
||||||
|
|
||||||
|
layout->insertWidget(0, shareBar);
|
||||||
|
shareBar->setVisible(false);
|
||||||
|
|
||||||
|
retranslateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::retranslateUi()
|
||||||
|
{
|
||||||
|
shareBar->retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
|
void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
|
||||||
|
|
@ -33,3 +74,127 @@ void TabDeckStorageVisual::actOpenLocalDeck(const QString &filePath)
|
||||||
|
|
||||||
emit openDeckEditor(deckOpt.value());
|
emit openDeckEditor(deckOpt.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::enterShareMode(const QStringList &preselectFiles)
|
||||||
|
{
|
||||||
|
if (!shareDeckAvailable) {
|
||||||
|
return; // sharing is gated on being logged in
|
||||||
|
}
|
||||||
|
shareBar->setCreateEnabled(true);
|
||||||
|
visualDeckStorageWidget->setShareSelectable(true);
|
||||||
|
visualDeckStorageWidget->setShareSelectedFiles(preselectFiles);
|
||||||
|
shareBar->setName(tr("Shared decks"));
|
||||||
|
shareBar->setVisible(true);
|
||||||
|
updateShareHint();
|
||||||
|
shareBar->focusName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::exitShareMode()
|
||||||
|
{
|
||||||
|
visualDeckStorageWidget->setShareSelectable(false);
|
||||||
|
visualDeckStorageWidget->clearShareSelection();
|
||||||
|
shareBar->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::actShareDeck(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (!shareDeckAvailable) {
|
||||||
|
QMessageBox::information(this, tr("Share deck"), tr("You must be connected to the server to share a deck."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enterShareMode({filePath});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::onShareSelectionChanged()
|
||||||
|
{
|
||||||
|
if (shareBar->isVisible()) {
|
||||||
|
updateShareHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::updateShareHint()
|
||||||
|
{
|
||||||
|
const int count = visualDeckStorageWidget->selectedFilePaths().size();
|
||||||
|
shareBar->setCountText(tr("%n deck(s)", "", count));
|
||||||
|
if (count == 0) {
|
||||||
|
shareBar->setHintText(tr("Click deck tiles to select the decks you want to share."), true);
|
||||||
|
} else {
|
||||||
|
shareBar->setHintText(tr("%n deck(s) selected. Create the link to share %1 with other players.", "", count)
|
||||||
|
.arg(count == 1 ? tr("it") : tr("them")),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::actShareSelected()
|
||||||
|
{
|
||||||
|
const QStringList filePaths = visualDeckStorageWidget->selectedFilePaths();
|
||||||
|
if (filePaths.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, tr("Share decks"), tr("Select at least one deck to share."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Command_DeckShareCreate cmd;
|
||||||
|
cmd.set_name(shareBar->name().toStdString());
|
||||||
|
if (cmd.name().empty()) {
|
||||||
|
cmd.set_name(tr("Shared decks").toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QString &filePath : filePaths) {
|
||||||
|
std::optional<LoadedDeck> deckOpt =
|
||||||
|
DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true);
|
||||||
|
if (!deckOpt) {
|
||||||
|
QMessageBox::warning(this, tr("Share decks"), tr("Unable to load deck file %1").arg(filePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DeckShareItem *item = cmd.add_items();
|
||||||
|
item->set_deck_list(deckOpt->deckList.writeToString_Native().toStdString());
|
||||||
|
item->set_color_identity(getDeckColorIdentity(deckOpt->deckList, CardDatabaseManager::query()).toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
shareBar->setCreateEnabled(false);
|
||||||
|
PendingCommand *pend = tabSupervisor->getClient()->prepareSessionCommand(cmd);
|
||||||
|
connect(pend, &PendingCommand::finished, this, &TabDeckStorageVisual::shareFinished);
|
||||||
|
tabSupervisor->getClient()->sendCommand(pend);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::shareFinished(const Response &response, const CommandContainer & /*commandContainer*/)
|
||||||
|
{
|
||||||
|
shareBar->setCreateEnabled(true);
|
||||||
|
if (response.response_code() != Response::RespOk) {
|
||||||
|
showShareNotice(tr("Failed to create the share link (server response code %1).")
|
||||||
|
.arg(QString::number(static_cast<int>(response.response_code()))),
|
||||||
|
true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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);
|
||||||
|
|
||||||
|
showShareNotice(
|
||||||
|
tr("Share link copied to the clipboard.\nExpires on %1.").arg(DeckShareUtils::formatShareExpiry(expiry)));
|
||||||
|
exitShareMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::handleConnectionChanged(ClientStatus status)
|
||||||
|
{
|
||||||
|
shareDeckAvailable = (status == StatusLoggedIn);
|
||||||
|
visualDeckStorageWidget->setShareAvailable(shareDeckAvailable);
|
||||||
|
if (!shareDeckAvailable && shareBar->isVisible()) {
|
||||||
|
exitShareMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabDeckStorageVisual::showShareNotice(const QString &message, bool warning)
|
||||||
|
{
|
||||||
|
QMessageBox box(warning ? QMessageBox::Warning : QMessageBox::Information, tr("Deck share"), message,
|
||||||
|
QMessageBox::Ok, this);
|
||||||
|
box.exec();
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,12 @@
|
||||||
#ifndef TAB_DECK_STORAGE_VISUAL_H
|
#ifndef TAB_DECK_STORAGE_VISUAL_H
|
||||||
#define TAB_DECK_STORAGE_VISUAL_H
|
#define TAB_DECK_STORAGE_VISUAL_H
|
||||||
|
|
||||||
|
#include "../../deck_share/share_bar_widget.h"
|
||||||
#include "../tab.h"
|
#include "../tab.h"
|
||||||
|
|
||||||
|
#include <QStringList>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
|
||||||
struct LoadedDeck;
|
struct LoadedDeck;
|
||||||
class AbstractClient;
|
class AbstractClient;
|
||||||
class CommandContainer;
|
class CommandContainer;
|
||||||
|
|
@ -27,22 +31,49 @@ class TabDeckStorageVisual final : public Tab
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
|
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
|
||||||
void retranslateUi() override
|
void retranslateUi() override;
|
||||||
{
|
|
||||||
}
|
|
||||||
[[nodiscard]] QString getTabText() const override
|
[[nodiscard]] QString getTabText() const override
|
||||||
{
|
{
|
||||||
return tr("Visual Deck Storage");
|
return tr("Visual Deck Storage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enters share-selection mode, optionally preselecting the given deck files.
|
||||||
|
*/
|
||||||
|
void enterShareMode(const QStringList &preselectFiles = {});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Leaves share-selection mode and clears the selection.
|
||||||
|
*/
|
||||||
|
void exitShareMode();
|
||||||
|
|
||||||
|
[[nodiscard]] bool isShareModeActive() const
|
||||||
|
{
|
||||||
|
return shareBar->isVisible();
|
||||||
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void actOpenLocalDeck(const QString &filePath);
|
void actOpenLocalDeck(const QString &filePath);
|
||||||
|
void actShareDeck(const QString &filePath);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void openDeckEditor(const LoadedDeck &deck);
|
void openDeckEditor(const LoadedDeck &deck);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void actShareSelected();
|
||||||
|
void shareFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
|
void onShareSelectionChanged();
|
||||||
|
void handleConnectionChanged(ClientStatus status);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void showShareNotice(const QString &message, bool warning = false);
|
||||||
|
void updateShareHint();
|
||||||
|
|
||||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||||
|
|
||||||
|
ShareBarWidget *shareBar;
|
||||||
|
bool shareDeckAvailable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
#include "deck_preview_color_identity_filter_widget.h"
|
#include "deck_preview_color_identity_filter_widget.h"
|
||||||
|
|
||||||
#include "../../cards/additional_info/mana_symbol_widget.h"
|
#include "../../cards/additional_info/mana_symbol_widget.h"
|
||||||
#include "../visual_deck_storage_widget.h"
|
|
||||||
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
|
||||||
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent)
|
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent)
|
||||||
: QWidget(parent), layout(new QHBoxLayout(this))
|
: QWidget(parent), layout(new QHBoxLayout(this))
|
||||||
{
|
{
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,12 @@
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class VisualDeckStorageWidget;
|
|
||||||
|
|
||||||
class DeckPreviewColorIdentityFilterWidget : public QWidget
|
class DeckPreviewColorIdentityFilterWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit DeckPreviewColorIdentityFilterWidget(VisualDeckStorageWidget *parent);
|
explicit DeckPreviewColorIdentityFilterWidget(QWidget *parent = nullptr);
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h"
|
#include "../../../../interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h"
|
||||||
#include "../../../deck_loader/deck_loader.h"
|
#include "../../../deck_loader/deck_loader.h"
|
||||||
#include "../../cards/additional_info/color_identity_widget.h"
|
#include "../../cards/additional_info/color_identity_widget.h"
|
||||||
|
#include "../../cards/additional_info/deck_color_identity.h"
|
||||||
#include "../../cards/deck_preview_card_picture_widget.h"
|
#include "../../cards/deck_preview_card_picture_widget.h"
|
||||||
#include "../visual_deck_storage_quick_settings_widget.h"
|
#include "../visual_deck_storage_quick_settings_widget.h"
|
||||||
#include "../visual_deck_storage_tag_filter_widget.h"
|
#include "../visual_deck_storage_tag_filter_widget.h"
|
||||||
|
|
@ -11,6 +12,7 @@
|
||||||
#include "deck_preview_deck_tags_display_widget.h"
|
#include "deck_preview_deck_tags_display_widget.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QFrame>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
|
@ -27,7 +29,7 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||||
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
VisualDeckStorageWidget *_visualDeckStorageWidget,
|
||||||
VisualDeckStorageModel *_model,
|
VisualDeckStorageModel *_model,
|
||||||
const QString &_filePath)
|
const QString &_filePath)
|
||||||
: QWidget(_parent), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model), filePath(_filePath)
|
: QWidget(_parent), filePath(_filePath), visualDeckStorageWidget(_visualDeckStorageWidget), model(_model)
|
||||||
{
|
{
|
||||||
layout = new QVBoxLayout(this);
|
layout = new QVBoxLayout(this);
|
||||||
setLayout(layout);
|
setLayout(layout);
|
||||||
|
|
@ -36,6 +38,8 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||||
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
|
new DeckPreviewCardPictureWidget(this, false, visualDeckStorageWidget->deckPreviewSelectionAnimationEnabled);
|
||||||
pictureWidget->setFontSize(24);
|
pictureWidget->setFontSize(24);
|
||||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent);
|
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageClicked, this, &DeckPreviewWidget::imageClickedEvent);
|
||||||
|
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageSingleClicked, this,
|
||||||
|
&DeckPreviewWidget::imageSingleClicked);
|
||||||
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
connect(pictureWidget, &DeckPreviewCardPictureWidget::imageDoubleClicked, this,
|
||||||
&DeckPreviewWidget::imageDoubleClickedEvent);
|
&DeckPreviewWidget::imageDoubleClickedEvent);
|
||||||
bannerCardDisplayWidget = pictureWidget;
|
bannerCardDisplayWidget = pictureWidget;
|
||||||
|
|
@ -99,6 +103,15 @@ DeckPreviewWidget::DeckPreviewWidget(QWidget *_parent,
|
||||||
// to keep the resize handler from searching the widget tree on every layout pass.
|
// to keep the resize handler from searching the widget tree on every layout pass.
|
||||||
fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel,
|
fixedWidthChildren = {bannerCardDisplayWidget, colorIdentityWidget, deckTagsDisplayWidget, bannerCardLabel,
|
||||||
bannerCardComboBox};
|
bannerCardComboBox};
|
||||||
|
|
||||||
|
// Child of the banner widget so the frame tracks the banner's selection animation
|
||||||
|
// (which animates the banner's position) instead of staying at a stale static offset.
|
||||||
|
selectionFrame = new QFrame(bannerCardDisplayWidget);
|
||||||
|
selectionFrame->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
selectionFrame->setStyleSheet(QStringLiteral(
|
||||||
|
"QFrame { border: 2px solid palette(highlight); border-radius: 4px; background: transparent; }"));
|
||||||
|
selectionFrame->setVisible(false);
|
||||||
|
bannerCardDisplayWidget->installEventFilter(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckPreviewWidget::retranslateUi()
|
void DeckPreviewWidget::retranslateUi()
|
||||||
|
|
@ -122,6 +135,63 @@ void DeckPreviewWidget::resizeEvent(QResizeEvent *event)
|
||||||
for (QWidget *widget : fixedWidthChildren) {
|
for (QWidget *widget : fixedWidthChildren) {
|
||||||
widget->setMaximumWidth(width);
|
widget->setMaximumWidth(width);
|
||||||
}
|
}
|
||||||
|
updateSelectionFrameGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckPreviewWidget::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if (watched == bannerCardDisplayWidget && (event->type() == QEvent::Resize || event->type() == QEvent::Move)) {
|
||||||
|
updateSelectionFrameGeometry();
|
||||||
|
}
|
||||||
|
return QWidget::eventFilter(watched, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckPreviewWidget::setShareSelectable(bool selectable)
|
||||||
|
{
|
||||||
|
shareSelectable = selectable;
|
||||||
|
if (!selectable) {
|
||||||
|
setShareSelected(false);
|
||||||
|
}
|
||||||
|
updateSelectionStyle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckPreviewWidget::setShareSelected(bool selected)
|
||||||
|
{
|
||||||
|
if (shareSelected == selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shareSelected = selected;
|
||||||
|
updateSelectionStyle();
|
||||||
|
emit shareSelectionToggled(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckPreviewWidget::isShareSelected() const
|
||||||
|
{
|
||||||
|
return shareSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckPreviewWidget::isShareSelectable() const
|
||||||
|
{
|
||||||
|
return shareSelectable;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckPreviewWidget::updateSelectionFrameGeometry()
|
||||||
|
{
|
||||||
|
if (selectionFrame == nullptr || bannerCardDisplayWidget == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Frame is a child of the banner, so it is positioned in banner coordinates and
|
||||||
|
// tracks the banner's selection animation automatically. A small inset keeps the
|
||||||
|
// highlight visible around the card art without occluding it.
|
||||||
|
selectionFrame->setGeometry(bannerCardDisplayWidget->rect().adjusted(1, 1, -1, -1));
|
||||||
|
selectionFrame->raise();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckPreviewWidget::updateSelectionStyle()
|
||||||
|
{
|
||||||
|
if (selectionFrame != nullptr) {
|
||||||
|
selectionFrame->setVisible(shareSelectable && isShareSelected());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckPreviewWidget::enterEvent(QEnterEvent *event)
|
void DeckPreviewWidget::enterEvent(QEnterEvent *event)
|
||||||
|
|
@ -226,10 +296,6 @@ void DeckPreviewWidget::updateTagsVisibility(bool visible)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes the banner card text.
|
|
||||||
* This also calls `refreshBannerCardToolTip`, since those two often need to be updated together.
|
|
||||||
*/
|
|
||||||
void DeckPreviewWidget::refreshBannerCardText()
|
void DeckPreviewWidget::refreshBannerCardText()
|
||||||
{
|
{
|
||||||
bannerCardDisplayWidget->setOverlayText(getDisplayName());
|
bannerCardDisplayWidget->setOverlayText(getDisplayName());
|
||||||
|
|
@ -337,10 +403,20 @@ void DeckPreviewWidget::imageClickedEvent(QMouseEvent *event, DeckPreviewCardPic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckPreviewWidget::imageSingleClicked()
|
||||||
|
{
|
||||||
|
if (isShareSelectable()) {
|
||||||
|
setShareSelected(!isShareSelected());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
void DeckPreviewWidget::imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance)
|
||||||
{
|
{
|
||||||
Q_UNUSED(event);
|
Q_UNUSED(event);
|
||||||
Q_UNUSED(instance);
|
Q_UNUSED(instance);
|
||||||
|
if (isShareSelectable()) {
|
||||||
|
return; // in share mode a double click would just toggle a single selection
|
||||||
|
}
|
||||||
emit deckLoadRequested(filePath);
|
emit deckLoadRequested(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,6 +441,9 @@ QMenu *DeckPreviewWidget::createRightClickMenu()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(menu->addAction(tr("Share deck...")), &QAction::triggered, this,
|
||||||
|
[this] { emit shareDeckRequested(filePath); });
|
||||||
|
|
||||||
connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget,
|
connect(menu->addAction(tr("Edit Tags")), &QAction::triggered, deckTagsDisplayWidget,
|
||||||
&DeckPreviewDeckTagsDisplayWidget::openTagEditDlg);
|
&DeckPreviewDeckTagsDisplayWidget::openTagEditDlg);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class QEnterEvent;
|
class QEnterEvent;
|
||||||
|
class QFrame;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QMenu;
|
class QMenu;
|
||||||
class QMouseEvent;
|
class QMouseEvent;
|
||||||
|
|
@ -41,9 +42,19 @@ public:
|
||||||
*/
|
*/
|
||||||
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
DeckPreviewCardPictureWidget *bannerCardDisplayWidget;
|
||||||
|
|
||||||
|
/** @brief The path of the deck file backing this preview. */
|
||||||
|
QString filePath;
|
||||||
|
|
||||||
|
void setShareSelectable(bool selectable);
|
||||||
|
void setShareSelected(bool selected);
|
||||||
|
[[nodiscard]] bool isShareSelected() const;
|
||||||
|
[[nodiscard]] bool isShareSelectable() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void deckLoadRequested(const QString &filePath);
|
void deckLoadRequested(const QString &filePath);
|
||||||
void openDeckEditor(const LoadedDeck &deck);
|
void openDeckEditor(const LoadedDeck &deck);
|
||||||
|
void shareDeckRequested(const QString &filePath);
|
||||||
|
void shareSelectionToggled(bool selected);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,6 +77,7 @@ public slots:
|
||||||
protected:
|
protected:
|
||||||
void enterEvent(QEnterEvent *event) override;
|
void enterEvent(QEnterEvent *event) override;
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] int row() const;
|
[[nodiscard]] int row() const;
|
||||||
|
|
@ -76,6 +88,7 @@ private:
|
||||||
QMenu *createRightClickMenu();
|
QMenu *createRightClickMenu();
|
||||||
void addSetBannerCardMenu(QMenu *menu);
|
void addSetBannerCardMenu(QMenu *menu);
|
||||||
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
void imageClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||||
|
void imageSingleClicked();
|
||||||
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
void imageDoubleClickedEvent(QMouseEvent *event, DeckPreviewCardPictureWidget *instance);
|
||||||
|
|
||||||
void actRenameDeck();
|
void actRenameDeck();
|
||||||
|
|
@ -84,7 +97,6 @@ private:
|
||||||
|
|
||||||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||||
VisualDeckStorageModel *model;
|
VisualDeckStorageModel *model;
|
||||||
QString filePath;
|
|
||||||
QVBoxLayout *layout;
|
QVBoxLayout *layout;
|
||||||
ColorIdentityWidget *colorIdentityWidget;
|
ColorIdentityWidget *colorIdentityWidget;
|
||||||
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
DeckPreviewDeckTagsDisplayWidget *deckTagsDisplayWidget;
|
||||||
|
|
@ -92,6 +104,12 @@ private:
|
||||||
QComboBox *bannerCardComboBox;
|
QComboBox *bannerCardComboBox;
|
||||||
QList<QWidget *> fixedWidthChildren; ///< Children clamped to the picture width on resize.
|
QList<QWidget *> fixedWidthChildren; ///< Children clamped to the picture width on resize.
|
||||||
int lastKnownBannerWidth = -1; ///< The picture width last applied to the children.
|
int lastKnownBannerWidth = -1; ///< The picture width last applied to the children.
|
||||||
|
QFrame *selectionFrame = nullptr;
|
||||||
|
bool shareSelectable = false;
|
||||||
|
bool shareSelected = false;
|
||||||
|
|
||||||
|
void updateSelectionStyle();
|
||||||
|
void updateSelectionFrameGeometry();
|
||||||
};
|
};
|
||||||
|
|
||||||
class NoScrollFilter : public QObject
|
class NoScrollFilter : public QObject
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,11 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget
|
||||||
&VisualDeckStorageWidget::deckLoadRequested);
|
&VisualDeckStorageWidget::deckLoadRequested);
|
||||||
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
connect(deckPreviewWidget, &DeckPreviewWidget::openDeckEditor, visualDeckStorageWidget,
|
||||||
&VisualDeckStorageWidget::openDeckEditor);
|
&VisualDeckStorageWidget::openDeckEditor);
|
||||||
|
connect(deckPreviewWidget, &DeckPreviewWidget::shareDeckRequested, visualDeckStorageWidget,
|
||||||
|
&VisualDeckStorageWidget::shareDeckRequested);
|
||||||
|
connect(deckPreviewWidget, &DeckPreviewWidget::shareSelectionToggled, visualDeckStorageWidget,
|
||||||
|
&VisualDeckStorageWidget::shareSelectionChanged);
|
||||||
|
deckPreviewWidget->setShareSelectable(visualDeckStorageWidget->isShareSelectable());
|
||||||
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
connect(visualDeckStorageWidget->settings(), &VisualDeckStorageQuickSettingsWidget::cardSizeChanged,
|
||||||
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
deckPreviewWidget->bannerCardDisplayWidget, &CardInfoPictureWidget::setScaleFactor);
|
||||||
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
deckPreviewWidget->bannerCardDisplayWidget->setScaleFactor(visualDeckStorageWidget->settings()->getCardSize());
|
||||||
|
|
@ -216,6 +221,18 @@ DeckPreviewWidget *VisualDeckStorageFolderDisplayWidget::createDeckPreviewWidget
|
||||||
return deckPreviewWidget;
|
return deckPreviewWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageFolderDisplayWidget::setShareSelectable(bool selectable)
|
||||||
|
{
|
||||||
|
const auto previews = flowWidget->findChildren<DeckPreviewWidget *>();
|
||||||
|
for (DeckPreviewWidget *preview : previews) {
|
||||||
|
preview->setShareSelectable(selectable);
|
||||||
|
}
|
||||||
|
const auto subFolders = findChildren<VisualDeckStorageFolderDisplayWidget *>();
|
||||||
|
for (VisualDeckStorageFolderDisplayWidget *subFolder : subFolders) {
|
||||||
|
subFolder->setShareSelectable(selectable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Creates, removes and keeps in sync the subfolder widgets of this folder.
|
* @brief Creates, removes and keeps in sync the subfolder widgets of this folder.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ public slots:
|
||||||
*/
|
*/
|
||||||
void scheduleReconcile();
|
void scheduleReconcile();
|
||||||
void updateShowFolders(bool enabled);
|
void updateShowFolders(bool enabled);
|
||||||
|
void setShareSelectable(bool selectable);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,8 @@ VisualDeckStorageSearchWidget::VisualDeckStorageSearchWidget(QWidget *parent) :
|
||||||
|
|
||||||
connect(searchDebounceTimer, &QTimer::timeout, this, [this] { emit searchTextChanged(searchBar->text()); });
|
connect(searchDebounceTimer, &QTimer::timeout, this, [this] { emit searchTextChanged(searchBar->text()); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageSearchWidget::setPlaceholderText(const QString &text)
|
||||||
|
{
|
||||||
|
searchBar->setPlaceholderText(text);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ class VisualDeckStorageSearchWidget : public QWidget
|
||||||
public:
|
public:
|
||||||
explicit VisualDeckStorageSearchWidget(QWidget *parent);
|
explicit VisualDeckStorageSearchWidget(QWidget *parent);
|
||||||
|
|
||||||
|
void setPlaceholderText(const QString &text);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
/**
|
/**
|
||||||
* Emitted once the debounce timer fires after the user stopped typing.
|
* Emitted once the debounce timer fires after the user stopped typing.
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,10 @@
|
||||||
|
|
||||||
#include "../general/layout_containers/flow_widget.h"
|
#include "../general/layout_containers/flow_widget.h"
|
||||||
#include "deck_preview/deck_preview_tag_display_widget.h"
|
#include "deck_preview/deck_preview_tag_display_widget.h"
|
||||||
#include "visual_deck_storage_model.h"
|
|
||||||
#include "visual_deck_storage_sort_filter_proxy_model.h"
|
|
||||||
#include "visual_deck_storage_widget.h"
|
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
|
||||||
VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent)
|
VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(QWidget *parent) : QWidget(parent)
|
||||||
: QWidget(_parent), parent(_parent)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
||||||
|
|
@ -25,97 +21,62 @@ VisualDeckStorageTagFilterWidget::VisualDeckStorageTagFilterWidget(VisualDeckSto
|
||||||
layout->addWidget(flowWidget);
|
layout->addWidget(flowWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageTagFilterWidget::setAllTagsProvider(const std::function<QSet<QString>()> &provider)
|
||||||
|
{
|
||||||
|
allTagsProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event)
|
void VisualDeckStorageTagFilterWidget::showEvent(QShowEvent *event)
|
||||||
{
|
{
|
||||||
QWidget::showEvent(event);
|
QWidget::showEvent(event);
|
||||||
refreshTags();
|
refreshTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The tags of all decks currently accepted by the proxy model.
|
|
||||||
*/
|
|
||||||
QSet<QString> VisualDeckStorageTagFilterWidget::gatherAllTags() const
|
|
||||||
{
|
|
||||||
QSet<QString> allTags;
|
|
||||||
auto *proxy = parent->proxyModel();
|
|
||||||
|
|
||||||
for (int proxyRow = 0; proxyRow < proxy->rowCount(); ++proxyRow) {
|
|
||||||
const QModelIndex index = proxy->index(proxyRow, 0);
|
|
||||||
if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList();
|
|
||||||
for (const QString &tag : deckTags) {
|
|
||||||
allTags.insert(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allTags;
|
|
||||||
}
|
|
||||||
|
|
||||||
void VisualDeckStorageTagFilterWidget::refreshTags()
|
void VisualDeckStorageTagFilterWidget::refreshTags()
|
||||||
{
|
{
|
||||||
QSet<QString> allTags = gatherAllTags();
|
const QSet<QString> allTags = allTagsProvider ? allTagsProvider() : QSet<QString>();
|
||||||
removeTagsNotInList(allTags);
|
|
||||||
addTagsIfNotPresent(allTags);
|
|
||||||
sortTags();
|
|
||||||
}
|
|
||||||
|
|
||||||
void VisualDeckStorageTagFilterWidget::removeTagsNotInList(const QSet<QString> &tags)
|
// Existing chips survive if their tag is still part of the deck set, or if the chip
|
||||||
{
|
// is currently selected/excluded. Everything else is dropped. Dropped chips must NOT
|
||||||
|
// be re-added to the layout afterwards: they are scheduled for a deferred delete, and
|
||||||
|
// the flow layout would keep a dangling reference to them once the deletion runs on
|
||||||
|
// the next event-loop cycle.
|
||||||
|
QList<DeckPreviewTagDisplayWidget *> chips;
|
||||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
||||||
const QString &tagName = tagWidget->getTagName();
|
if (tagWidget->getState() != TagState::NotSelected || allTags.contains(tagWidget->getTagName())) {
|
||||||
|
chips.append(tagWidget);
|
||||||
// Keep the tag widget if it is either selected or excluded
|
} else {
|
||||||
if (!tags.contains(tagName) && tagWidget->getState() == TagState::NotSelected) {
|
|
||||||
flowWidget->removeWidget(tagWidget);
|
flowWidget->removeWidget(tagWidget);
|
||||||
tagWidget->deleteLater();
|
tagWidget->deleteLater();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void VisualDeckStorageTagFilterWidget::addTagsIfNotPresent(const QSet<QString> &tags)
|
// Add chips for tags that are not shown yet.
|
||||||
{
|
for (const QString &tag : allTags) {
|
||||||
for (const QString &tag : tags) {
|
bool tagExists = false;
|
||||||
addTagIfNotPresent(tag);
|
for (DeckPreviewTagDisplayWidget *tagWidget : chips) {
|
||||||
}
|
if (tagWidget->getTagName() == tag) {
|
||||||
}
|
tagExists = true;
|
||||||
|
break;
|
||||||
void VisualDeckStorageTagFilterWidget::addTagIfNotPresent(const QString &tag)
|
}
|
||||||
{
|
}
|
||||||
// Check if the tag already exists in the flow widget
|
if (!tagExists) {
|
||||||
bool tagExists = false;
|
auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag);
|
||||||
for (DeckPreviewTagDisplayWidget *tagWidget : findChildren<DeckPreviewTagDisplayWidget *>()) {
|
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, this,
|
||||||
if (tagWidget->getTagName() == tag) {
|
&VisualDeckStorageTagFilterWidget::filterChanged);
|
||||||
tagExists = true;
|
flowWidget->addWidget(newTagWidget);
|
||||||
break;
|
chips.append(newTagWidget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the tag doesn't exist, add a new DeckPreviewTagDisplayWidget
|
// Clear and re-add the chips in sorted order.
|
||||||
if (!tagExists) {
|
std::sort(chips.begin(), chips.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) {
|
||||||
auto *newTagWidget = new DeckPreviewTagDisplayWidget(this, tag);
|
|
||||||
connect(newTagWidget, &DeckPreviewTagDisplayWidget::tagClicked, parent,
|
|
||||||
&VisualDeckStorageWidget::updateTagFilter);
|
|
||||||
flowWidget->addWidget(newTagWidget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VisualDeckStorageTagFilterWidget::sortTags()
|
|
||||||
{
|
|
||||||
// Get all tag widgets
|
|
||||||
QList<DeckPreviewTagDisplayWidget *> tagWidgets = findChildren<DeckPreviewTagDisplayWidget *>();
|
|
||||||
|
|
||||||
// Sort widgets by tag name
|
|
||||||
std::sort(tagWidgets.begin(), tagWidgets.end(), [](DeckPreviewTagDisplayWidget *a, DeckPreviewTagDisplayWidget *b) {
|
|
||||||
return a->getTagName().toLower() < b->getTagName().toLower();
|
return a->getTagName().toLower() < b->getTagName().toLower();
|
||||||
});
|
});
|
||||||
|
for (DeckPreviewTagDisplayWidget *tagWidget : chips) {
|
||||||
// Clear and re-add widgets in sorted order
|
|
||||||
for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) {
|
|
||||||
flowWidget->removeWidget(tagWidget);
|
flowWidget->removeWidget(tagWidget);
|
||||||
}
|
}
|
||||||
for (DeckPreviewTagDisplayWidget *tagWidget : tagWidgets) {
|
for (DeckPreviewTagDisplayWidget *tagWidget : chips) {
|
||||||
flowWidget->addWidget(tagWidget);
|
flowWidget->addWidget(tagWidget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,27 @@
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
class FlowWidget;
|
class FlowWidget;
|
||||||
class VisualDeckStorageWidget;
|
class VisualDeckStorageWidget;
|
||||||
|
|
||||||
class VisualDeckStorageTagFilterWidget : public QWidget
|
class VisualDeckStorageTagFilterWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
VisualDeckStorageWidget *parent;
|
|
||||||
FlowWidget *flowWidget;
|
FlowWidget *flowWidget;
|
||||||
|
std::function<QSet<QString>()> allTagsProvider;
|
||||||
[[nodiscard]] QSet<QString> gatherAllTags() const;
|
|
||||||
void removeTagsNotInList(const QSet<QString> &tags);
|
|
||||||
void addTagsIfNotPresent(const QSet<QString> &tags);
|
|
||||||
void addTagIfNotPresent(const QString &tag);
|
|
||||||
void sortTags();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit VisualDeckStorageTagFilterWidget(VisualDeckStorageWidget *_parent);
|
explicit VisualDeckStorageTagFilterWidget(QWidget *parent = nullptr);
|
||||||
[[nodiscard]] QStringList getAllKnownTags() const;
|
[[nodiscard]] QStringList getAllKnownTags() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets a provider for the full set of tags to draw chips from.
|
||||||
|
*/
|
||||||
|
void setAllTagsProvider(const std::function<QSet<QString>()> &provider);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The tags currently in "selected" state.
|
* @brief The tags currently in "selected" state.
|
||||||
*/
|
*/
|
||||||
|
|
@ -39,9 +40,15 @@ public:
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] QStringList excludedTags() const;
|
[[nodiscard]] QStringList excludedTags() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* Emitted whenever a chip's selection/exclusion state changes.
|
||||||
|
*/
|
||||||
|
void filterChanged();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
/**
|
||||||
* @brief Rebuilds the tag chips from the tags of the currently visible decks.
|
* @brief Rebuilds the tag chips from the currently available tags.
|
||||||
*/
|
*/
|
||||||
void refreshTags();
|
void refreshTags();
|
||||||
void showEvent(QShowEvent *event) override;
|
void showEvent(QShowEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,13 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
||||||
refreshButton->setFixedSize(32, 32);
|
refreshButton->setFixedSize(32, 32);
|
||||||
connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible);
|
connect(refreshButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::refreshIfPossible);
|
||||||
|
|
||||||
|
shareButton = new QToolButton(this);
|
||||||
|
shareButton->setIcon(QPixmap("theme:icons/share"));
|
||||||
|
shareButton->setFixedSize(32, 32);
|
||||||
|
shareButton->setToolTip(tr("Select decks to share"));
|
||||||
|
shareButton->setVisible(false);
|
||||||
|
connect(shareButton, &QPushButton::clicked, this, &VisualDeckStorageWidget::shareRequested);
|
||||||
|
|
||||||
quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this);
|
quickSettingsWidget = new VisualDeckStorageQuickSettingsWidget(this);
|
||||||
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showFoldersChanged, this,
|
connect(quickSettingsWidget, &VisualDeckStorageQuickSettingsWidget::showFoldersChanged, this,
|
||||||
&VisualDeckStorageWidget::updateShowFolders);
|
&VisualDeckStorageWidget::updateShowFolders);
|
||||||
|
|
@ -57,10 +64,14 @@ VisualDeckStorageWidget::VisualDeckStorageWidget(QWidget *parent) : QWidget(pare
|
||||||
searchAndSortLayout->addWidget(sortWidget);
|
searchAndSortLayout->addWidget(sortWidget);
|
||||||
searchAndSortLayout->addWidget(searchWidget);
|
searchAndSortLayout->addWidget(searchWidget);
|
||||||
searchAndSortLayout->addWidget(refreshButton);
|
searchAndSortLayout->addWidget(refreshButton);
|
||||||
|
searchAndSortLayout->addWidget(shareButton);
|
||||||
searchAndSortLayout->addWidget(quickSettingsWidget);
|
searchAndSortLayout->addWidget(quickSettingsWidget);
|
||||||
|
|
||||||
// tag filter box
|
// tag filter box
|
||||||
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
tagFilterWidget = new VisualDeckStorageTagFilterWidget(this);
|
||||||
|
tagFilterWidget->setAllTagsProvider([this] { return gatherVisibleTags(); });
|
||||||
|
connect(tagFilterWidget, &VisualDeckStorageTagFilterWidget::filterChanged, this,
|
||||||
|
&VisualDeckStorageWidget::updateTagFilter);
|
||||||
updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter());
|
updateTagsVisibility(SettingsCache::instance().visualDeckStorage().getVisualDeckStorageShowTagFilter());
|
||||||
|
|
||||||
deckPreviewSelectionAnimationEnabled =
|
deckPreviewSelectionAnimationEnabled =
|
||||||
|
|
@ -159,6 +170,63 @@ void VisualDeckStorageWidget::retranslateUi()
|
||||||
sortWidget->retranslateUi();
|
sortWidget->retranslateUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageWidget::setShareSelectable(bool selectable)
|
||||||
|
{
|
||||||
|
if (shareSelectable == selectable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shareSelectable = selectable;
|
||||||
|
if (folderWidget != nullptr) {
|
||||||
|
folderWidget->setShareSelectable(selectable);
|
||||||
|
}
|
||||||
|
emit shareSelectionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VisualDeckStorageWidget::isShareSelectable() const
|
||||||
|
{
|
||||||
|
return shareSelectable;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList VisualDeckStorageWidget::selectedFilePaths() const
|
||||||
|
{
|
||||||
|
QStringList selectedPaths;
|
||||||
|
if (folderWidget != nullptr) {
|
||||||
|
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||||
|
for (DeckPreviewWidget *preview : previews) {
|
||||||
|
if (preview->isShareSelected()) {
|
||||||
|
selectedPaths.append(preview->filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageWidget::clearShareSelection()
|
||||||
|
{
|
||||||
|
if (folderWidget != nullptr) {
|
||||||
|
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||||
|
for (DeckPreviewWidget *preview : previews) {
|
||||||
|
preview->setShareSelected(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageWidget::setShareAvailable(bool available)
|
||||||
|
{
|
||||||
|
shareButton->setVisible(available);
|
||||||
|
shareButton->setEnabled(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VisualDeckStorageWidget::setShareSelectedFiles(const QStringList &paths)
|
||||||
|
{
|
||||||
|
if (folderWidget != nullptr) {
|
||||||
|
const auto previews = folderWidget->findChildren<DeckPreviewWidget *>();
|
||||||
|
for (DeckPreviewWidget *preview : previews) {
|
||||||
|
preview->setShareSelected(paths.contains(preview->filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a const pointer to the quick settings so that the values can be accessed.
|
* Gets a const pointer to the quick settings so that the values can be accessed.
|
||||||
*/
|
*/
|
||||||
|
|
@ -216,6 +284,25 @@ void VisualDeckStorageWidget::updateTagFilter()
|
||||||
tagFilterWidget->refreshTags();
|
tagFilterWidget->refreshTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The tags of all decks currently accepted by the proxy model.
|
||||||
|
*/
|
||||||
|
QSet<QString> VisualDeckStorageWidget::gatherVisibleTags() const
|
||||||
|
{
|
||||||
|
QSet<QString> allTags;
|
||||||
|
for (int proxyRow = 0; proxyRow < storageProxyModel->rowCount(); ++proxyRow) {
|
||||||
|
const QModelIndex index = storageProxyModel->index(proxyRow, 0);
|
||||||
|
if (!index.data(VisualDeckStorageRoles::FilterMatchRole).toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QStringList deckTags = index.data(VisualDeckStorageRoles::TagsRole).toStringList();
|
||||||
|
for (const QString &tag : deckTags) {
|
||||||
|
allTags.insert(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allTags;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushes the color identity filter widget's state into the proxy model.
|
* Pushes the color identity filter widget's state into the proxy model.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ public:
|
||||||
explicit VisualDeckStorageWidget(QWidget *parent);
|
explicit VisualDeckStorageWidget(QWidget *parent);
|
||||||
void refreshIfPossible();
|
void refreshIfPossible();
|
||||||
void retranslateUi();
|
void retranslateUi();
|
||||||
|
void setShareSelectable(bool selectable);
|
||||||
|
[[nodiscard]] bool isShareSelectable() const;
|
||||||
|
[[nodiscard]] QStringList selectedFilePaths() const;
|
||||||
|
void setShareSelectedFiles(const QStringList &paths);
|
||||||
|
void clearShareSelection();
|
||||||
|
void setShareAvailable(bool available);
|
||||||
|
|
||||||
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
VisualDeckStorageTagFilterWidget *tagFilterWidget;
|
||||||
bool deckPreviewSelectionAnimationEnabled;
|
bool deckPreviewSelectionAnimationEnabled;
|
||||||
|
|
@ -63,6 +69,9 @@ public slots:
|
||||||
signals:
|
signals:
|
||||||
void deckLoadRequested(const QString &filePath);
|
void deckLoadRequested(const QString &filePath);
|
||||||
void openDeckEditor(const LoadedDeck &deck);
|
void openDeckEditor(const LoadedDeck &deck);
|
||||||
|
void shareDeckRequested(const QString &filePath);
|
||||||
|
void shareSelectionChanged();
|
||||||
|
void shareRequested();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
@ -70,6 +79,7 @@ protected:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void reapplySortAndFilters();
|
void reapplySortAndFilters();
|
||||||
|
[[nodiscard]] QSet<QString> gatherVisibleTags() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QVBoxLayout *layout;
|
QVBoxLayout *layout;
|
||||||
|
|
@ -80,12 +90,14 @@ private:
|
||||||
VisualDeckStorageSearchWidget *searchWidget;
|
VisualDeckStorageSearchWidget *searchWidget;
|
||||||
DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget;
|
DeckPreviewColorIdentityFilterWidget *deckPreviewColorIdentityFilterWidget;
|
||||||
QToolButton *refreshButton;
|
QToolButton *refreshButton;
|
||||||
|
QToolButton *shareButton;
|
||||||
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
VisualDeckStorageQuickSettingsWidget *quickSettingsWidget;
|
||||||
QScrollArea *scrollArea;
|
QScrollArea *scrollArea;
|
||||||
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
|
VisualDeckStorageFolderDisplayWidget *folderWidget = nullptr;
|
||||||
VisualDeckStorageModel *storageModel = nullptr;
|
VisualDeckStorageModel *storageModel = nullptr;
|
||||||
VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr;
|
VisualDeckStorageSortFilterProxyModel *storageProxyModel = nullptr;
|
||||||
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
|
QTimer *refreshTimer = nullptr; ///< Coalesces the re-apply/refresh burst following a batch of deck loads.
|
||||||
|
bool shareSelectable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // VISUAL_DECK_STORAGE_WIDGET_H
|
#endif // VISUAL_DECK_STORAGE_WIDGET_H
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||||
|
|
||||||
connectionController = new ConnectionController(this, this);
|
connectionController = new ConnectionController(this, this);
|
||||||
urlParser = new IntentUrlParser(this, this);
|
urlParser = new IntentUrlParser(this, this);
|
||||||
|
connect(urlParser, &IntentUrlParser::urlChainFinished, this, &MainWindow::onUrlChainFinished);
|
||||||
|
|
||||||
createActions();
|
createActions();
|
||||||
createMenus();
|
createMenus();
|
||||||
|
|
@ -722,6 +723,7 @@ void MainWindow::applyStartupDestination()
|
||||||
|
|
||||||
connect(credentials, &Intent::finished, connector, &Intent::execute);
|
connect(credentials, &Intent::finished, connector, &Intent::execute);
|
||||||
connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed);
|
connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed);
|
||||||
|
connect(credentials, &Intent::cancelled, this, [this]() { startupDestinationFailed(tr("Sign-in cancelled")); });
|
||||||
connect(connector, &Intent::finished, this,
|
connect(connector, &Intent::finished, this,
|
||||||
[this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); });
|
[this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); });
|
||||||
connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed);
|
connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed);
|
||||||
|
|
@ -862,18 +864,7 @@ void MainWindow::changeEvent(QEvent *event)
|
||||||
} else if (event->type() == QEvent::ActivationChange) {
|
} else if (event->type() == QEvent::ActivationChange) {
|
||||||
if (isActiveWindow() && !bHasActivated) {
|
if (isActiveWindow() && !bHasActivated) {
|
||||||
bHasActivated = true;
|
bHasActivated = true;
|
||||||
if (!connectTo.isEmpty()) {
|
attemptStartupAutoConnect();
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -899,6 +890,40 @@ void MainWindow::handleCockatriceLink(const QString &url)
|
||||||
urlParser->handle(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()
|
void MainWindow::cardDatabaseLoadingFailed()
|
||||||
{
|
{
|
||||||
if (askedForDbUpdater) {
|
if (askedForDbUpdater) {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ public slots:
|
||||||
void actCheckClientUpdates();
|
void actCheckClientUpdates();
|
||||||
void actConnect();
|
void actConnect();
|
||||||
void actExit();
|
void actExit();
|
||||||
|
void handleCockatriceLink(const QString &url);
|
||||||
private slots:
|
private slots:
|
||||||
void updateTabMenu(const QList<QMenu *> &newMenuList);
|
void updateTabMenu(const QList<QMenu *> &newMenuList);
|
||||||
void statusChanged(ClientStatus _status);
|
void statusChanged(ClientStatus _status);
|
||||||
|
|
@ -92,7 +93,7 @@ private slots:
|
||||||
void actOpenSettingsFolder();
|
void actOpenSettingsFolder();
|
||||||
void actShow();
|
void actShow();
|
||||||
void showWindowIfHidden();
|
void showWindowIfHidden();
|
||||||
void handleCockatriceLink(const QString &url);
|
void onUrlChainFinished(bool connected);
|
||||||
|
|
||||||
void cardUpdateError(QProcess::ProcessError err);
|
void cardUpdateError(QProcess::ProcessError err);
|
||||||
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||||
|
|
@ -120,6 +121,8 @@ private slots:
|
||||||
void startupDestinationFailed(const QString &reason);
|
void startupDestinationFailed(const QString &reason);
|
||||||
[[nodiscard]] bool startupDestinationConnectsToServer() const;
|
[[nodiscard]] bool startupDestinationConnectsToServer() const;
|
||||||
|
|
||||||
|
void attemptStartupAutoConnect();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString appName;
|
static const QString appName;
|
||||||
static const QStringList fileNameFilters;
|
static const QStringList fileNameFilters;
|
||||||
|
|
@ -158,6 +161,8 @@ private:
|
||||||
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
||||||
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
|
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
|
||||||
bool bHasActivated, askedForDbUpdater;
|
bool bHasActivated, askedForDbUpdater;
|
||||||
|
bool skipStartupAutoConnect = false;
|
||||||
|
bool startupAutoConnectAttempted = false;
|
||||||
QProcess *cardUpdateProcess;
|
QProcess *cardUpdateProcess;
|
||||||
DlgViewLog *logviewDialog;
|
DlgViewLog *logviewDialog;
|
||||||
GameReplay *replay;
|
GameReplay *replay;
|
||||||
|
|
@ -170,6 +175,16 @@ public:
|
||||||
{
|
{
|
||||||
connectTo = QUrl(QString("cockatrice://%1").arg(url));
|
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;
|
~MainWindow() override;
|
||||||
|
|
||||||
RemoteClient *getRemoteClient() const
|
RemoteClient *getRemoteClient() const
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
#include "client/url_scheme_event_filter.h"
|
#include "client/url_scheme_event_filter.h"
|
||||||
#include "database/interface/settings_card_preference_provider.h"
|
#include "database/interface/settings_card_preference_provider.h"
|
||||||
#include "interface/intents/intent_open_local_deck.h"
|
#include "interface/intents/intent_open_local_deck.h"
|
||||||
#include "interface/intents/url_parser.h"
|
|
||||||
#include "interface/logger.h"
|
#include "interface/logger.h"
|
||||||
#include "interface/pixel_map_generator.h"
|
#include "interface/pixel_map_generator.h"
|
||||||
#include "interface/theme_manager.h"
|
#include "interface/theme_manager.h"
|
||||||
|
|
@ -45,6 +44,7 @@
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QSystemTrayIcon>
|
#include <QSystemTrayIcon>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
|
#include <algorithm>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
#include <libcockatrice/rng/rng_sfmt.h>
|
#include <libcockatrice/rng/rng_sfmt.h>
|
||||||
#include <libcockatrice/settings/appearance_settings.h>
|
#include <libcockatrice/settings/appearance_settings.h>
|
||||||
|
|
@ -273,10 +273,12 @@ int main(int argc, char *argv[])
|
||||||
SingleInstanceManager instance;
|
SingleInstanceManager instance;
|
||||||
|
|
||||||
if (hasActivationFiles) {
|
if (hasActivationFiles) {
|
||||||
|
qInfo() << "Activation launch, files:" << startupFiles;
|
||||||
// Activation launch: hand off to the primary instance if one is
|
// Activation launch: hand off to the primary instance if one is
|
||||||
// running, otherwise become the primary ourselves. Do this before
|
// running, otherwise become the primary ourselves. Do this before
|
||||||
// constructing the main window so a hand-off exits cheaply.
|
// constructing the main window so a hand-off exits cheaply.
|
||||||
if (!instance.tryRun(startupFiles)) {
|
if (!instance.tryRun(startupFiles)) {
|
||||||
|
qInfo() << "Handed off to a running instance, exiting";
|
||||||
// Sent successfully → exit
|
// Sent successfully → exit
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -325,10 +327,22 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
MainWindow ui;
|
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) {
|
auto handleActivation = [&ui](const QString &file) {
|
||||||
if (file.startsWith("cockatrice://")) {
|
if (file.startsWith("cockatrice://")) {
|
||||||
auto urlParser = new IntentUrlParser(&ui, &ui);
|
qInfo() << "Handling URL activation:" << file;
|
||||||
urlParser->handle(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()) {
|
} else if (QFileInfo(file).exists()) {
|
||||||
auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
|
auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
|
||||||
QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) {
|
QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
#include <QDir>
|
#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)
|
SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +80,14 @@ bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend)
|
||||||
socket.flush();
|
socket.flush();
|
||||||
socket.waitForBytesWritten(1000);
|
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()
|
void SingleInstanceManager::handleNewConnection()
|
||||||
|
|
@ -111,6 +126,14 @@ void SingleInstanceManager::handleNewConnection()
|
||||||
QStringList files;
|
QStringList files;
|
||||||
payloadStream >> 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);
|
emit filesReceived(files);
|
||||||
|
|
||||||
// Reset buffer (single message use-case)
|
// Reset buffer (single message use-case)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,15 @@ set(PROTO_FILES
|
||||||
command_deck_del.proto
|
command_deck_del.proto
|
||||||
command_deck_del_dir.proto
|
command_deck_del_dir.proto
|
||||||
command_deck_download.proto
|
command_deck_download.proto
|
||||||
|
command_deck_download_public.proto
|
||||||
command_deck_list.proto
|
command_deck_list.proto
|
||||||
|
command_deck_list_other_user.proto
|
||||||
command_deck_new_dir.proto
|
command_deck_new_dir.proto
|
||||||
command_deck_select.proto
|
command_deck_select.proto
|
||||||
|
command_deck_set_visibility.proto
|
||||||
|
command_deck_share_create.proto
|
||||||
|
command_deck_share_download.proto
|
||||||
|
command_deck_share_list.proto
|
||||||
command_deck_upload.proto
|
command_deck_upload.proto
|
||||||
command_del_counter.proto
|
command_del_counter.proto
|
||||||
command_delete_arrow.proto
|
command_delete_arrow.proto
|
||||||
|
|
@ -135,6 +141,9 @@ set(PROTO_FILES
|
||||||
response_card_art_rule_entry.proto
|
response_card_art_rule_entry.proto
|
||||||
response_deck_download.proto
|
response_deck_download.proto
|
||||||
response_deck_list.proto
|
response_deck_list.proto
|
||||||
|
response_deck_share_create.proto
|
||||||
|
response_deck_share_download.proto
|
||||||
|
response_deck_share_list.proto
|
||||||
response_deck_upload.proto
|
response_deck_upload.proto
|
||||||
response_dump_zone.proto
|
response_dump_zone.proto
|
||||||
response_forgotpasswordrequest.proto
|
response_forgotpasswordrequest.proto
|
||||||
|
|
@ -172,6 +181,7 @@ set(PROTO_FILES
|
||||||
serverinfo_cardcounter.proto
|
serverinfo_cardcounter.proto
|
||||||
serverinfo_chat_message.proto
|
serverinfo_chat_message.proto
|
||||||
serverinfo_counter.proto
|
serverinfo_counter.proto
|
||||||
|
serverinfo_deck_share_item.proto
|
||||||
serverinfo_deckstorage.proto
|
serverinfo_deckstorage.proto
|
||||||
serverinfo_game.proto
|
serverinfo_game.proto
|
||||||
serverinfo_gametype.proto
|
serverinfo_gametype.proto
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message Command_DeckDownloadPublic {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckDownloadPublic ext = 1031;
|
||||||
|
}
|
||||||
|
optional uint32 deck_id = 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message Command_DeckListOtherUser {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckListOtherUser ext = 1029;
|
||||||
|
}
|
||||||
|
optional string user_name = 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message Command_DeckSetVisibility {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckSetVisibility ext = 1030;
|
||||||
|
}
|
||||||
|
// Set the public visibility of a single deck (mutually exclusive with folder_path).
|
||||||
|
optional uint32 deck_id = 1;
|
||||||
|
// Set the public visibility of a folder (all decks under it inherit).
|
||||||
|
optional string folder_path = 2;
|
||||||
|
optional bool is_public = 3;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message DeckShareItem {
|
||||||
|
// Reference an existing deck in the sharer's personal deck storage.
|
||||||
|
// Mutually exclusive with deck_list.
|
||||||
|
optional uint32 deck_id = 1;
|
||||||
|
// Inline deck content in the native format.
|
||||||
|
// Mutually exclusive with deck_id.
|
||||||
|
optional string deck_list = 2;
|
||||||
|
// Color identity of the deck (e.g. "WUBRG"), computed by the sharing client.
|
||||||
|
optional string color_identity = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Command_DeckShareCreate {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckShareCreate ext = 1026;
|
||||||
|
}
|
||||||
|
optional string name = 1;
|
||||||
|
repeated DeckShareItem items = 2;
|
||||||
|
// Path of a folder in the sharer's personal deck storage. When set, all
|
||||||
|
// decks in that folder are shared (resolved by the server).
|
||||||
|
optional string folder_path = 3;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message Command_DeckShareDownload {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckShareDownload ext = 1028;
|
||||||
|
}
|
||||||
|
optional string token = 1;
|
||||||
|
optional uint32 item_id = 2;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "session_commands.proto";
|
||||||
|
|
||||||
|
message Command_DeckShareList {
|
||||||
|
extend SessionCommand {
|
||||||
|
optional Command_DeckShareList ext = 1027;
|
||||||
|
}
|
||||||
|
optional string token = 1;
|
||||||
|
}
|
||||||
|
|
@ -8,4 +8,12 @@ message Command_DeckUpload {
|
||||||
optional string path = 1; // to upload a new deck
|
optional string path = 1; // to upload a new deck
|
||||||
optional uint32 deck_id = 2; // to replace an existing deck
|
optional uint32 deck_id = 2; // to replace an existing deck
|
||||||
optional string deck_list = 3;
|
optional string deck_list = 3;
|
||||||
|
optional bool is_public = 4; // mark the deck public on upload (publish)
|
||||||
|
// Preview metadata computed by the uploading client (see ServerInfo_DeckStorage_File).
|
||||||
|
optional string banner_card_name = 5;
|
||||||
|
optional string banner_card_provider = 6;
|
||||||
|
optional string color_identity = 7;
|
||||||
|
// Comma-separated list of tag names associated with the deck, used to render
|
||||||
|
// and filter another user's public decks on the client.
|
||||||
|
optional string tags = 8;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,9 @@ message Response {
|
||||||
REPLAY_LIST = 1100; // Response listing replays
|
REPLAY_LIST = 1100; // Response listing replays
|
||||||
REPLAY_DOWNLOAD = 1101; // Response for replay download
|
REPLAY_DOWNLOAD = 1101; // Response for replay download
|
||||||
REPLAY_GET_CODE = 1102; // Response containing replay code
|
REPLAY_GET_CODE = 1102; // Response containing replay code
|
||||||
|
DECK_SHARE_CREATE = 1103; // Response to deck share creation
|
||||||
|
DECK_SHARE_LIST = 1104; // Response listing shared decks
|
||||||
|
DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download
|
||||||
CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules
|
CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "response.proto";
|
||||||
|
|
||||||
|
message Response_DeckShareCreate {
|
||||||
|
extend Response {
|
||||||
|
optional Response_DeckShareCreate ext = 1103;
|
||||||
|
}
|
||||||
|
optional string token = 1;
|
||||||
|
optional uint64 expires_at = 2;
|
||||||
|
optional uint32 item_count = 3;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "response.proto";
|
||||||
|
|
||||||
|
message Response_DeckShareDownload {
|
||||||
|
extend Response {
|
||||||
|
optional Response_DeckShareDownload ext = 1105;
|
||||||
|
}
|
||||||
|
optional string deck = 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
import "response.proto";
|
||||||
|
import "serverinfo_deck_share_item.proto";
|
||||||
|
|
||||||
|
message Response_DeckShareList {
|
||||||
|
extend Response {
|
||||||
|
optional Response_DeckShareList ext = 1104;
|
||||||
|
}
|
||||||
|
optional string name = 1;
|
||||||
|
optional uint64 expires_at = 2;
|
||||||
|
repeated ServerInfo_DeckShareItem items = 3;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
message ServerInfo_DeckShareItem {
|
||||||
|
optional uint32 id = 1;
|
||||||
|
optional string name = 2;
|
||||||
|
repeated string tags = 3;
|
||||||
|
optional string banner_card = 4;
|
||||||
|
optional string game_format = 5;
|
||||||
|
optional string color_identity = 6;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
syntax = "proto2";
|
syntax = "proto2";
|
||||||
message ServerInfo_DeckStorage_File {
|
message ServerInfo_DeckStorage_File {
|
||||||
optional uint32 creation_time = 1;
|
optional uint32 creation_time = 1;
|
||||||
|
optional bool is_public = 2;
|
||||||
|
// Preview metadata computed by the uploading client, so other clients can
|
||||||
|
// render this deck (e.g. in a visual storage grid) without downloading the
|
||||||
|
// full deck list. Empty for decks uploaded before the metadata columns.
|
||||||
|
optional string banner_card_name = 3;
|
||||||
|
optional string banner_card_provider = 4;
|
||||||
|
optional string color_identity = 5;
|
||||||
|
// Comma-separated list of tag names, matching the corresponding
|
||||||
|
// ServerInfo_DeckStorage_File upload metadata. Empty for decks uploaded
|
||||||
|
// before the tags column existed.
|
||||||
|
optional string tags = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ServerInfo_DeckStorage_Folder {
|
message ServerInfo_DeckStorage_Folder {
|
||||||
repeated ServerInfo_DeckStorage_TreeItem items = 1;
|
repeated ServerInfo_DeckStorage_TreeItem items = 1;
|
||||||
|
optional bool is_public = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ServerInfo_DeckStorage_TreeItem {
|
message ServerInfo_DeckStorage_TreeItem {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ message SessionCommand {
|
||||||
FORGOT_PASSWORD_CHALLENGE = 1023;
|
FORGOT_PASSWORD_CHALLENGE = 1023;
|
||||||
REQUEST_PASSWORD_SALT = 1024;
|
REQUEST_PASSWORD_SALT = 1024;
|
||||||
SET_CARD_ART_PARAMS = 1025;
|
SET_CARD_ART_PARAMS = 1025;
|
||||||
|
DECK_SHARE_CREATE = 1026;
|
||||||
|
DECK_SHARE_LIST = 1027;
|
||||||
|
DECK_SHARE_DOWNLOAD = 1028;
|
||||||
|
DECK_LIST_OTHER_USER = 1029;
|
||||||
|
DECK_SET_VISIBILITY = 1030;
|
||||||
|
DECK_DOWNLOAD_PUBLIC = 1031;
|
||||||
REPLAY_LIST = 1100;
|
REPLAY_LIST = 1100;
|
||||||
REPLAY_DOWNLOAD = 1101;
|
REPLAY_DOWNLOAD = 1101;
|
||||||
REPLAY_MODIFY_MATCH = 1102;
|
REPLAY_MODIFY_MATCH = 1102;
|
||||||
|
|
|
||||||
71
servatrice/migrations/servatrice_0036_to_0037.sql
Normal file
71
servatrice/migrations/servatrice_0036_to_0037.sql
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- Servatrice db migration from version 36 to version 37
|
||||||
|
|
||||||
|
-- Deck sharing (temporary share links + permanent public decks).
|
||||||
|
--
|
||||||
|
-- This feature was developed behind several intermediate migrations that have
|
||||||
|
-- never shipped, so they are folded into this single 36 -> 37 migration:
|
||||||
|
-- temporary share links, permanent public-deck visibility, preview metadata,
|
||||||
|
-- and per-deck tags.
|
||||||
|
|
||||||
|
-- 1. Temporary deck shares: a named bundle of decks that can be fetched by
|
||||||
|
-- anyone who knows the (unguessable) token, until the share expires.
|
||||||
|
CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` (
|
||||||
|
`id` int(7) unsigned zerofill NOT NULL auto_increment,
|
||||||
|
`token` varchar(64) NOT NULL,
|
||||||
|
`name` varchar(64) NOT NULL,
|
||||||
|
`created_by` int(7) unsigned NULL,
|
||||||
|
`created_at` datetime NOT NULL,
|
||||||
|
`expires_at` datetime NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `token` (`token`),
|
||||||
|
KEY `expires_at` (`expires_at`),
|
||||||
|
FOREIGN KEY(`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Individual decks inside a share bundle. Content is materialized at share
|
||||||
|
-- time so expiring/deleting a share can cascade cleanly. The metadata columns
|
||||||
|
-- are nullable because Qt's MySQL driver binds empty QStrings as NULL when
|
||||||
|
-- using prepared statements.
|
||||||
|
CREATE TABLE IF NOT EXISTS `cockatrice_deck_share_item` (
|
||||||
|
`id` int(7) unsigned zerofill NOT NULL auto_increment,
|
||||||
|
`share_id` int(7) unsigned zerofill NOT NULL,
|
||||||
|
`name` varchar(50) NOT NULL,
|
||||||
|
`tags` text NULL,
|
||||||
|
`banner_card` varchar(255) NULL,
|
||||||
|
`game_format` varchar(50) NULL,
|
||||||
|
`color_identity` varchar(5) NULL,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`position` int(7) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `share_id` (`share_id`),
|
||||||
|
FOREIGN KEY(`share_id`) REFERENCES `cockatrice_deck_share`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 2. Permanent deck sharing: add public visibility flags to the deck storage
|
||||||
|
-- tables. A deck is visible to other users if it is marked public, or if any
|
||||||
|
-- ancestor folder is marked public (inherited). Existing decks default to
|
||||||
|
-- private, so the upgrade does not expose any data.
|
||||||
|
ALTER TABLE `cockatrice_decklist_files`
|
||||||
|
ADD COLUMN `is_public` tinyint(1) NOT NULL DEFAULT 0 AFTER `content`;
|
||||||
|
|
||||||
|
ALTER TABLE `cockatrice_decklist_folders`
|
||||||
|
ADD COLUMN `is_public` tinyint(1) NOT NULL DEFAULT 0 AFTER `name`;
|
||||||
|
|
||||||
|
-- 3. Per-deck preview metadata so clients can render another user's public
|
||||||
|
-- decks (e.g. in a visual deck storage grid) without downloading each deck
|
||||||
|
-- list. The metadata is computed by the uploading client; decks uploaded
|
||||||
|
-- before this migration have empty values until they are re-uploaded.
|
||||||
|
ALTER TABLE `cockatrice_decklist_files`
|
||||||
|
ADD COLUMN `banner_card_name` varchar(255) NULL AFTER `content`,
|
||||||
|
ADD COLUMN `banner_card_provider` varchar(32) NULL AFTER `banner_card_name`,
|
||||||
|
ADD COLUMN `color_identity` varchar(5) NULL AFTER `banner_card_provider`;
|
||||||
|
|
||||||
|
-- 4. Per-deck tags for public decks. The uploading client sends a
|
||||||
|
-- comma-separated tag string (matching the deck's own tags), so another user's
|
||||||
|
-- public decks can render and filter by tag without downloading each deck list.
|
||||||
|
-- Decks uploaded before this migration have NULL tags until they are
|
||||||
|
-- re-uploaded.
|
||||||
|
ALTER TABLE `cockatrice_decklist_files`
|
||||||
|
ADD COLUMN `tags` text NULL AFTER `color_identity`;
|
||||||
|
|
||||||
|
UPDATE cockatrice_schema_version SET version=37 WHERE version=36;
|
||||||
|
|
@ -439,3 +439,19 @@ ssl_cert=ssl_cert.pem
|
||||||
|
|
||||||
; Filename of the private key for the server-to-server certificate
|
; Filename of the private key for the server-to-server certificate
|
||||||
ssl_key=ssl_key.pem
|
ssl_key=ssl_key.pem
|
||||||
|
|
||||||
|
|
||||||
|
[deck_share]
|
||||||
|
|
||||||
|
; How many days a created deck share link remains valid before it expires.
|
||||||
|
; Default: 7
|
||||||
|
expiry_days=7
|
||||||
|
|
||||||
|
; How often (in minutes) the server checks for and removes expired deck shares.
|
||||||
|
; A value of 0 disables the automatic cleanup.
|
||||||
|
; Default: 60
|
||||||
|
cleanup_interval=60
|
||||||
|
|
||||||
|
; Maximum number of decks a single share link can contain.
|
||||||
|
; Default: 50
|
||||||
|
max_decks_per_share=50
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` (
|
||||||
PRIMARY KEY (`version`)
|
PRIMARY KEY (`version`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
INSERT INTO cockatrice_schema_version VALUES(36);
|
INSERT INTO cockatrice_schema_version VALUES(37);
|
||||||
|
|
||||||
-- users and user data tables
|
-- users and user data tables
|
||||||
CREATE TABLE IF NOT EXISTS `cockatrice_users` (
|
CREATE TABLE IF NOT EXISTS `cockatrice_users` (
|
||||||
|
|
@ -63,16 +63,56 @@ CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` (
|
||||||
`name` varchar(50) NOT NULL,
|
`name` varchar(50) NOT NULL,
|
||||||
`upload_time` datetime NOT NULL,
|
`upload_time` datetime NOT NULL,
|
||||||
`content` text NOT NULL,
|
`content` text NOT NULL,
|
||||||
|
`is_public` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`banner_card_name` varchar(255) NULL,
|
||||||
|
`banner_card_provider` varchar(32) NULL,
|
||||||
|
`color_identity` varchar(5) NULL,
|
||||||
|
`tags` text NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `FolderPlusUser` (`id_folder`,`id_user`),
|
KEY `FolderPlusUser` (`id_folder`,`id_user`),
|
||||||
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Temporary deck shares: a named bundle of decks that can be fetched by
|
||||||
|
-- anyone who knows the (unguessable) token, until the share expires.
|
||||||
|
CREATE TABLE IF NOT EXISTS `cockatrice_deck_share` (
|
||||||
|
`id` int(7) unsigned zerofill NOT NULL auto_increment,
|
||||||
|
`token` varchar(64) NOT NULL,
|
||||||
|
`name` varchar(64) NOT NULL,
|
||||||
|
`created_by` int(7) unsigned NULL,
|
||||||
|
`created_at` datetime NOT NULL,
|
||||||
|
`expires_at` datetime NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `token` (`token`),
|
||||||
|
KEY `expires_at` (`expires_at`),
|
||||||
|
FOREIGN KEY(`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Individual decks inside a share bundle. Content is materialized at share
|
||||||
|
-- time so expiring/deleting a share can cascade cleanly. The metadata columns
|
||||||
|
-- are nullable because Qt's MySQL driver binds empty QStrings as NULL when
|
||||||
|
-- using prepared statements.
|
||||||
|
CREATE TABLE IF NOT EXISTS `cockatrice_deck_share_item` (
|
||||||
|
`id` int(7) unsigned zerofill NOT NULL auto_increment,
|
||||||
|
`share_id` int(7) unsigned zerofill NOT NULL,
|
||||||
|
`name` varchar(50) NOT NULL,
|
||||||
|
`tags` text NULL,
|
||||||
|
`banner_card` varchar(255) NULL,
|
||||||
|
`game_format` varchar(50) NULL,
|
||||||
|
`color_identity` varchar(5) NULL,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`position` int(7) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `share_id` (`share_id`),
|
||||||
|
FOREIGN KEY(`share_id`) REFERENCES `cockatrice_deck_share`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` (
|
CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` (
|
||||||
`id` int(7) unsigned zerofill NOT NULL auto_increment,
|
`id` int(7) unsigned zerofill NOT NULL auto_increment,
|
||||||
`id_parent` int(7) unsigned zerofill NOT NULL,
|
`id_parent` int(7) unsigned zerofill NOT NULL,
|
||||||
`id_user` int(7) unsigned NULL,
|
`id_user` int(7) unsigned NULL,
|
||||||
`name` varchar(30) NOT NULL,
|
`name` varchar(30) NOT NULL,
|
||||||
|
`is_public` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `ParentPlusUser` (`id_parent`,`id_user`),
|
KEY `ParentPlusUser` (`id_parent`,`id_user`),
|
||||||
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,14 @@ bool Servatrice::initServer()
|
||||||
statusUpdateClock->start(getServerStatusUpdateTime());
|
statusUpdateClock->start(getServerStatusUpdateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deckShareCleanupClock = new QTimer(this);
|
||||||
|
connect(deckShareCleanupClock, SIGNAL(timeout()), this, SLOT(cleanupExpiredDeckShares()));
|
||||||
|
const int deckShareCleanupInterval = getDeckShareCleanupInterval();
|
||||||
|
if (deckShareCleanupInterval > 0) {
|
||||||
|
qDebug() << "Starting deck share cleanup clock, interval" << deckShareCleanupInterval << "ms";
|
||||||
|
deckShareCleanupClock->start(deckShareCleanupInterval);
|
||||||
|
}
|
||||||
|
|
||||||
// SOCKET SERVER
|
// SOCKET SERVER
|
||||||
if (getNumberOfTCPPools() > 0) {
|
if (getNumberOfTCPPools() > 0) {
|
||||||
gameServer =
|
gameServer =
|
||||||
|
|
@ -600,6 +608,11 @@ void Servatrice::setRequiredFeatures(const QString &featureList)
|
||||||
qDebug() << "Set required client features to:" << serverRequiredFeatureList;
|
qDebug() << "Set required client features to:" << serverRequiredFeatureList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Servatrice::cleanupExpiredDeckShares()
|
||||||
|
{
|
||||||
|
servatriceDatabaseInterface->cleanupExpiredDeckShares();
|
||||||
|
}
|
||||||
|
|
||||||
void Servatrice::statusUpdate()
|
void Servatrice::statusUpdate()
|
||||||
{
|
{
|
||||||
if (!servatriceDatabaseInterface->checkSql()) {
|
if (!servatriceDatabaseInterface->checkSql()) {
|
||||||
|
|
@ -1012,6 +1025,22 @@ int Servatrice::getServerStatusUpdateTime() const
|
||||||
return settingsCache->value("server/statusupdate", 15000).toInt();
|
return settingsCache->value("server/statusupdate", 15000).toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Servatrice::getDeckShareExpiryDays() const
|
||||||
|
{
|
||||||
|
return settingsCache->value("deck_share/expiry_days", 7).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Servatrice::getDeckShareCleanupInterval() const
|
||||||
|
{
|
||||||
|
// default: every 60 minutes
|
||||||
|
return settingsCache->value("deck_share/cleanup_interval", 60).toInt() * 60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Servatrice::getDeckShareMaxDecksPerShare() const
|
||||||
|
{
|
||||||
|
return settingsCache->value("deck_share/max_decks_per_share", 50).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
int Servatrice::getNumberOfTCPPools() const
|
int Servatrice::getNumberOfTCPPools() const
|
||||||
{
|
{
|
||||||
return settingsCache->value("server/number_pools", 1).toInt();
|
return settingsCache->value("server/number_pools", 1).toInt();
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ public:
|
||||||
private slots:
|
private slots:
|
||||||
void statusUpdate();
|
void statusUpdate();
|
||||||
void shutdownTimeout();
|
void shutdownTimeout();
|
||||||
|
void cleanupExpiredDeckShares();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void doSendIslMessage(const IslMessage &msg, int _serverId) override;
|
void doSendIslMessage(const IslMessage &msg, int _serverId) override;
|
||||||
|
|
@ -156,6 +157,7 @@ private:
|
||||||
AuthenticationMethod authenticationMethod;
|
AuthenticationMethod authenticationMethod;
|
||||||
DatabaseType databaseType;
|
DatabaseType databaseType;
|
||||||
QTimer *pingClock, *statusUpdateClock;
|
QTimer *pingClock, *statusUpdateClock;
|
||||||
|
QTimer *deckShareCleanupClock;
|
||||||
Servatrice_GameServer *gameServer;
|
Servatrice_GameServer *gameServer;
|
||||||
Servatrice_WebsocketGameServer *websocketGameServer;
|
Servatrice_WebsocketGameServer *websocketGameServer;
|
||||||
Servatrice_IslServer *islServer;
|
Servatrice_IslServer *islServer;
|
||||||
|
|
@ -267,6 +269,9 @@ public:
|
||||||
int getMaxGameInactivityTime() const override;
|
int getMaxGameInactivityTime() const override;
|
||||||
int getMaxPlayerInactivityTime() const override;
|
int getMaxPlayerInactivityTime() const override;
|
||||||
int getClientKeepAlive() const override;
|
int getClientKeepAlive() const override;
|
||||||
|
int getDeckShareExpiryDays() const;
|
||||||
|
int getDeckShareCleanupInterval() const;
|
||||||
|
int getDeckShareMaxDecksPerShare() const;
|
||||||
int getMaxUsersPerAddress() const;
|
int getMaxUsersPerAddress() const;
|
||||||
int getMessageCountingInterval() const override;
|
int getMessageCountingInterval() const override;
|
||||||
int getMaxMessageCountPerInterval() const override;
|
int getMaxMessageCountPerInterval() const override;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include <QChar>
|
#include <QChar>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
|
@ -1026,6 +1027,127 @@ DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int user
|
||||||
return deck;
|
return deck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Servatrice_DatabaseInterface::createDeckShare(const QString &token,
|
||||||
|
const QString &name,
|
||||||
|
int userId,
|
||||||
|
const QList<DeckShareItemRecord> &items,
|
||||||
|
int expiryDays)
|
||||||
|
{
|
||||||
|
checkSql();
|
||||||
|
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDatabase.transaction();
|
||||||
|
|
||||||
|
QSqlQuery *query = prepareQuery("insert into {prefix}_deck_share (token, name, created_by, created_at, expires_at) "
|
||||||
|
"values (:token, :name, :created_by, NOW(), DATE_ADD(NOW(), INTERVAL :days DAY))");
|
||||||
|
query->bindValue(":token", token);
|
||||||
|
query->bindValue(":name", name);
|
||||||
|
query->bindValue(":created_by", userId < 1 ? QVariant() : userId);
|
||||||
|
query->bindValue(":days", expiryDays);
|
||||||
|
if (!execSqlQuery(query)) {
|
||||||
|
sqlDatabase.rollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int shareId = query->lastInsertId().toInt();
|
||||||
|
for (int i = 0; i < items.size(); ++i) {
|
||||||
|
const DeckShareItemRecord &item = items.at(i);
|
||||||
|
QSqlQuery *itemQuery = prepareQuery("insert into {prefix}_deck_share_item (share_id, name, tags, banner_card, "
|
||||||
|
"game_format, color_identity, content, position) values (:share_id, :name, "
|
||||||
|
":tags, :banner_card, :game_format, :color_identity, :content, :position)");
|
||||||
|
itemQuery->bindValue(":share_id", shareId);
|
||||||
|
itemQuery->bindValue(":name", item.name);
|
||||||
|
QJsonArray tagArray;
|
||||||
|
for (const QString &tag : item.tags) {
|
||||||
|
tagArray.append(tag);
|
||||||
|
}
|
||||||
|
itemQuery->bindValue(":tags", QString::fromUtf8(QJsonDocument(tagArray).toJson(QJsonDocument::Compact)));
|
||||||
|
itemQuery->bindValue(":banner_card", item.bannerCard);
|
||||||
|
itemQuery->bindValue(":game_format", item.gameFormat);
|
||||||
|
itemQuery->bindValue(":color_identity", item.colorIdentity);
|
||||||
|
itemQuery->bindValue(":content", item.content);
|
||||||
|
itemQuery->bindValue(":position", i);
|
||||||
|
if (!execSqlQuery(itemQuery)) {
|
||||||
|
sqlDatabase.rollback();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDatabase.commit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Servatrice_DatabaseInterface::getDeckShareList(const QString &token,
|
||||||
|
QString &name,
|
||||||
|
qint64 &expiresAt,
|
||||||
|
QList<DeckShareItemRecord> &items)
|
||||||
|
{
|
||||||
|
checkSql();
|
||||||
|
|
||||||
|
QSqlQuery *query =
|
||||||
|
prepareQuery("select id, name, UNIX_TIMESTAMP(expires_at) from {prefix}_deck_share where token = "
|
||||||
|
":token and expires_at > now()");
|
||||||
|
query->bindValue(":token", token);
|
||||||
|
execSqlQuery(query);
|
||||||
|
if (!query->next()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int shareId = query->value(0).toInt();
|
||||||
|
name = query->value(1).toString();
|
||||||
|
expiresAt = query->value(2).toLongLong();
|
||||||
|
items.clear();
|
||||||
|
|
||||||
|
QSqlQuery *itemQuery =
|
||||||
|
prepareQuery("select id, name, tags, banner_card, game_format, color_identity from {prefix}_deck_share_item "
|
||||||
|
"where share_id = :share_id order by position");
|
||||||
|
itemQuery->bindValue(":share_id", shareId);
|
||||||
|
execSqlQuery(itemQuery);
|
||||||
|
while (itemQuery->next()) {
|
||||||
|
DeckShareItemRecord item;
|
||||||
|
item.id = itemQuery->value(0).toInt();
|
||||||
|
item.name = itemQuery->value(1).toString();
|
||||||
|
const QJsonArray tagArray = QJsonDocument::fromJson(itemQuery->value(2).toString().toUtf8()).array();
|
||||||
|
for (const QJsonValue &tag : tagArray) {
|
||||||
|
item.tags.append(tag.toString());
|
||||||
|
}
|
||||||
|
item.bannerCard = itemQuery->value(3).toString();
|
||||||
|
item.gameFormat = itemQuery->value(4).toString();
|
||||||
|
item.colorIdentity = itemQuery->value(5).toString();
|
||||||
|
items.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Servatrice_DatabaseInterface::getDeckShareItem(const QString &token, int itemId, QString &content)
|
||||||
|
{
|
||||||
|
checkSql();
|
||||||
|
|
||||||
|
QSqlQuery *query = prepareQuery("select i.content from {prefix}_deck_share_item i join {prefix}_deck_share s on "
|
||||||
|
"s.id = i.share_id where s.token = :token and s.expires_at > now() and i.id = :id");
|
||||||
|
query->bindValue(":token", token);
|
||||||
|
query->bindValue(":id", itemId);
|
||||||
|
execSqlQuery(query);
|
||||||
|
if (!query->next()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = query->value(0).toString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Servatrice_DatabaseInterface::cleanupExpiredDeckShares()
|
||||||
|
{
|
||||||
|
checkSql();
|
||||||
|
|
||||||
|
QSqlQuery *query = prepareQuery("delete from {prefix}_deck_share where expires_at < now()");
|
||||||
|
execSqlQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
void Servatrice_DatabaseInterface::logMessage(const int senderId,
|
void Servatrice_DatabaseInterface::logMessage(const int senderId,
|
||||||
const QString &senderName,
|
const QString &senderName,
|
||||||
const QString &senderIp,
|
const QString &senderIp,
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,22 @@
|
||||||
#include <server.h>
|
#include <server.h>
|
||||||
#include <server_database_interface.h>
|
#include <server_database_interface.h>
|
||||||
|
|
||||||
#define DATABASE_SCHEMA_VERSION 36
|
#define DATABASE_SCHEMA_VERSION 37
|
||||||
|
|
||||||
class Servatrice;
|
class Servatrice;
|
||||||
|
|
||||||
|
/** @brief Metadata of a single deck inside a temporary deck share bundle. */
|
||||||
|
struct DeckShareItemRecord
|
||||||
|
{
|
||||||
|
int id = -1; ///< Database id, used for downloads.
|
||||||
|
QString name; ///< Deck name.
|
||||||
|
QStringList tags; ///< Deck tags.
|
||||||
|
QString bannerCard; ///< Banner card name (deck image).
|
||||||
|
QString gameFormat; ///< Game format the deck was built for.
|
||||||
|
QString colorIdentity; ///< Color identity, e.g. "WUBRG".
|
||||||
|
QString content; ///< Deck content (native format); empty in list queries.
|
||||||
|
};
|
||||||
|
|
||||||
class Servatrice_DatabaseInterface : public Server_DatabaseInterface
|
class Servatrice_DatabaseInterface : public Server_DatabaseInterface
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
@ -79,6 +91,25 @@ public:
|
||||||
const QList<GameReplay *> &replayList) override;
|
const QList<GameReplay *> &replayList) override;
|
||||||
DeckList *getDeckFromDatabase(int deckId, int userId) override;
|
DeckList *getDeckFromDatabase(int deckId, int userId) override;
|
||||||
|
|
||||||
|
/** @brief Creates a new temporary deck share bundle. Returns false on failure. */
|
||||||
|
bool createDeckShare(const QString &token,
|
||||||
|
const QString &name,
|
||||||
|
int userId,
|
||||||
|
const QList<DeckShareItemRecord> &items,
|
||||||
|
int expiryDays);
|
||||||
|
/**
|
||||||
|
* @brief Looks up a valid (non-expired) share bundle by token.
|
||||||
|
* @return false if the token is unknown or expired.
|
||||||
|
*/
|
||||||
|
bool getDeckShareList(const QString &token, QString &name, qint64 &expiresAt, QList<DeckShareItemRecord> &items);
|
||||||
|
/**
|
||||||
|
* @brief Fetches the content of one item of a valid share bundle.
|
||||||
|
* @return false if the token is unknown/expired or the item does not belong to the bundle.
|
||||||
|
*/
|
||||||
|
bool getDeckShareItem(const QString &token, int itemId, QString &content);
|
||||||
|
/** @brief Deletes all expired share bundles (cascades to their items). */
|
||||||
|
void cleanupExpiredDeckShares();
|
||||||
|
|
||||||
int getNextGameId() override;
|
int getNextGameId() override;
|
||||||
int getNextReplayId() override;
|
int getNextReplayId() override;
|
||||||
int getActiveUserCount(QString connectionType = QString()) override;
|
int getActiveUserCount(QString connectionType = QString()) override;
|
||||||
|
|
|
||||||
|
|
@ -34,18 +34,26 @@
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
|
#include <QRandomGenerator>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <algorithm>
|
||||||
#include <game/server_player.h>
|
#include <game/server_player.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <libcockatrice/deck_list/deck_list.h>
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_del.pb.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_del_dir.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_list_other_user.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.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_share_download.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_replay_delete_match.pb.h>
|
#include <libcockatrice/protocol/pb/command_replay_delete_match.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_replay_download.pb.h>
|
#include <libcockatrice/protocol/pb/command_replay_download.pb.h>
|
||||||
|
|
@ -77,6 +85,9 @@
|
||||||
#include <libcockatrice/protocol/pb/response_card_art_rule_entry.pb.h>
|
#include <libcockatrice/protocol/pb/response_card_art_rule_entry.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_share_create.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/response_deck_upload.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.pb.h>
|
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
|
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
|
||||||
|
|
@ -201,6 +212,12 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
|
||||||
return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc);
|
return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc);
|
||||||
case SessionCommand::DECK_LIST:
|
case SessionCommand::DECK_LIST:
|
||||||
return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc);
|
return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc);
|
||||||
|
case SessionCommand::DECK_LIST_OTHER_USER:
|
||||||
|
return cmdDeckListOtherUser(cmd.GetExtension(Command_DeckListOtherUser::ext), rc);
|
||||||
|
case SessionCommand::DECK_SET_VISIBILITY:
|
||||||
|
return cmdDeckSetVisibility(cmd.GetExtension(Command_DeckSetVisibility::ext), rc);
|
||||||
|
case SessionCommand::DECK_DOWNLOAD_PUBLIC:
|
||||||
|
return cmdDeckDownloadPublic(cmd.GetExtension(Command_DeckDownloadPublic::ext), rc);
|
||||||
case SessionCommand::DECK_NEW_DIR:
|
case SessionCommand::DECK_NEW_DIR:
|
||||||
return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc);
|
return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc);
|
||||||
case SessionCommand::DECK_DEL_DIR:
|
case SessionCommand::DECK_DEL_DIR:
|
||||||
|
|
@ -244,6 +261,12 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
|
||||||
return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc);
|
return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc);
|
||||||
case SessionCommand::SET_CARD_ART_PARAMS:
|
case SessionCommand::SET_CARD_ART_PARAMS:
|
||||||
return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc);
|
return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc);
|
||||||
|
case SessionCommand::DECK_SHARE_CREATE:
|
||||||
|
return cmdDeckShareCreate(cmd.GetExtension(Command_DeckShareCreate::ext), rc);
|
||||||
|
case SessionCommand::DECK_SHARE_LIST:
|
||||||
|
return cmdDeckShareList(cmd.GetExtension(Command_DeckShareList::ext), rc);
|
||||||
|
case SessionCommand::DECK_SHARE_DOWNLOAD:
|
||||||
|
return cmdDeckShareDownload(cmd.GetExtension(Command_DeckShareDownload::ext), rc);
|
||||||
case SessionCommand::ACCOUNT_PASSWORD:
|
case SessionCommand::ACCOUNT_PASSWORD:
|
||||||
return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc);
|
return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc);
|
||||||
case SessionCommand::REQUEST_PASSWORD_SALT:
|
case SessionCommand::REQUEST_PASSWORD_SALT:
|
||||||
|
|
@ -470,46 +493,70 @@ int AbstractServerSocketInterface::getDeckPathId(const QString &path)
|
||||||
return getDeckPathId(0, path.split("/"));
|
return getDeckPathId(0, path.split("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder)
|
bool AbstractServerSocketInterface::deckListHelper(int folderId,
|
||||||
|
ServerInfo_DeckStorage_Folder *folder,
|
||||||
|
int userId,
|
||||||
|
bool inheritedPublic,
|
||||||
|
bool publicOnly)
|
||||||
{
|
{
|
||||||
QSqlQuery *query = sqlInterface->prepareQuery(
|
QSqlQuery *query = sqlInterface->prepareQuery("select id, name, is_public from {prefix}_decklist_folders where "
|
||||||
"select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user");
|
"id_parent = :id_parent and id_user = :id_user");
|
||||||
query->bindValue(":id_parent", folderId);
|
query->bindValue(":id_parent", folderId);
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userId);
|
||||||
if (!sqlInterface->execSqlQuery(query)) {
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMap<int, QString> results;
|
QList<std::pair<int, std::pair<QString, bool>>> folderRows;
|
||||||
while (query->next()) {
|
while (query->next()) {
|
||||||
results[query->value(0).toInt()] = query->value(1).toString();
|
folderRows.append({query->value(0).toInt(), {query->value(1).toString(), query->value(2).toBool()}});
|
||||||
}
|
}
|
||||||
|
std::sort(folderRows.begin(), folderRows.end(), [](const auto &a, const auto &b) { return a.first < b.first; });
|
||||||
|
|
||||||
|
for (const auto &[folderIdValue, folderInfo] : folderRows) {
|
||||||
|
const QString name = folderInfo.first;
|
||||||
|
const bool ownPublic = folderInfo.second;
|
||||||
|
const bool effectivePublic = inheritedPublic || ownPublic;
|
||||||
|
if (publicOnly && !effectivePublic) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (int key : results.keys()) {
|
|
||||||
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
|
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
|
||||||
newItem->set_id(key);
|
newItem->set_id(folderIdValue);
|
||||||
newItem->set_name(results.value(key).toStdString());
|
newItem->set_name(name.toStdString());
|
||||||
|
newItem->mutable_folder()->set_is_public(ownPublic);
|
||||||
|
|
||||||
if (!deckListHelper(newItem->id(), newItem->mutable_folder())) {
|
if (!deckListHelper(newItem->id(), newItem->mutable_folder(), userId, effectivePublic, publicOnly)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query = sqlInterface->prepareQuery("select id, name, upload_time from {prefix}_decklist_files where id_folder = "
|
query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, "
|
||||||
":id_folder and id_user = :id_user");
|
"banner_card_provider, color_identity, tags from {prefix}_decklist_files where "
|
||||||
|
"id_folder = :id_folder and id_user = :id_user");
|
||||||
query->bindValue(":id_folder", folderId);
|
query->bindValue(":id_folder", folderId);
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userId);
|
||||||
if (!sqlInterface->execSqlQuery(query)) {
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (query->next()) {
|
while (query->next()) {
|
||||||
|
const bool ownPublic = query->value(3).toBool();
|
||||||
|
if (publicOnly && !(inheritedPublic || ownPublic)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
|
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
|
||||||
newItem->set_id(query->value(0).toInt());
|
newItem->set_id(query->value(0).toInt());
|
||||||
newItem->set_name(query->value(1).toString().toStdString());
|
newItem->set_name(query->value(1).toString().toStdString());
|
||||||
|
|
||||||
ServerInfo_DeckStorage_File *newFile = newItem->mutable_file();
|
ServerInfo_DeckStorage_File *newFile = newItem->mutable_file();
|
||||||
newFile->set_creation_time(query->value(2).toDateTime().toSecsSinceEpoch());
|
newFile->set_creation_time(query->value(2).toDateTime().toSecsSinceEpoch());
|
||||||
|
newFile->set_is_public(ownPublic);
|
||||||
|
newFile->set_banner_card_name(query->value(4).toString().toStdString());
|
||||||
|
newFile->set_banner_card_provider(query->value(5).toString().toStdString());
|
||||||
|
newFile->set_color_identity(query->value(6).toString().toStdString());
|
||||||
|
newFile->set_tags(query->value(7).toString().toStdString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -530,7 +577,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_
|
||||||
Response_DeckList *re = new Response_DeckList;
|
Response_DeckList *re = new Response_DeckList;
|
||||||
ServerInfo_DeckStorage_Folder *root = re->mutable_root();
|
ServerInfo_DeckStorage_Folder *root = re->mutable_root();
|
||||||
|
|
||||||
if (!deckListHelper(0, root)) {
|
if (!deckListHelper(0, root, userInfo->id(), false, false)) {
|
||||||
return Response::RespContextError;
|
return Response::RespContextError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -538,6 +585,156 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_
|
||||||
return Response::RespOk;
|
return Response::RespOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd,
|
||||||
|
ResponseContainer &rc)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
const QString userName = nameFromStdString(cmd.user_name());
|
||||||
|
const int userId = sqlInterface->getUserIdInDB(userName);
|
||||||
|
if (userId == -1) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response_DeckList *re = new Response_DeckList;
|
||||||
|
ServerInfo_DeckStorage_Folder *root = re->mutable_root();
|
||||||
|
|
||||||
|
if (!deckListHelper(0, root, userId, false, true)) {
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.setResponseExtension(re);
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
int AbstractServerSocketInterface::getDeckOwnerId(int deckId)
|
||||||
|
{
|
||||||
|
QSqlQuery *query = sqlInterface->prepareQuery("select id_user from {prefix}_decklist_files where id = :id");
|
||||||
|
query->bindValue(":id", deckId);
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!query->next()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return query->value(0).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AbstractServerSocketInterface::isDeckEffectivelyPublic(int deckId)
|
||||||
|
{
|
||||||
|
QSqlQuery *query =
|
||||||
|
sqlInterface->prepareQuery("select is_public, id_folder from {prefix}_decklist_files where id = :id");
|
||||||
|
query->bindValue(":id", deckId);
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!query->next()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (query->value(0).toBool()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int folderId = query->value(1).toInt();
|
||||||
|
int guard = 0;
|
||||||
|
while (folderId != 0 && guard < 100) {
|
||||||
|
QSqlQuery *folderQuery =
|
||||||
|
sqlInterface->prepareQuery("select is_public, id_parent from {prefix}_decklist_folders where id = :id");
|
||||||
|
folderQuery->bindValue(":id", folderId);
|
||||||
|
if (!sqlInterface->execSqlQuery(folderQuery)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!folderQuery->next()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (folderQuery->value(0).toBool()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
folderId = folderQuery->value(1).toInt();
|
||||||
|
++guard;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd,
|
||||||
|
ResponseContainer & /*rc*/)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
if (cmd.has_deck_id()) {
|
||||||
|
QSqlQuery *query =
|
||||||
|
sqlInterface->prepareQuery("select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user");
|
||||||
|
query->bindValue(":id", cmd.deck_id());
|
||||||
|
query->bindValue(":id_user", userInfo->id());
|
||||||
|
sqlInterface->execSqlQuery(query);
|
||||||
|
if (!query->next()) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
query = sqlInterface->prepareQuery("update {prefix}_decklist_files set is_public = :is_public where id = :id");
|
||||||
|
query->bindValue(":is_public", cmd.is_public() ? 1 : 0);
|
||||||
|
query->bindValue(":id", cmd.deck_id());
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
} else if (cmd.has_folder_path()) {
|
||||||
|
const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path()));
|
||||||
|
if (folderId == -1 || folderId == 0) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSqlQuery *query =
|
||||||
|
sqlInterface->prepareQuery("update {prefix}_decklist_folders set is_public = :is_public where id = :id");
|
||||||
|
query->bindValue(":is_public", cmd.is_public() ? 1 : 0);
|
||||||
|
query->bindValue(":id", folderId);
|
||||||
|
if (!sqlInterface->execSqlQuery(query)) {
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Response::RespInvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd,
|
||||||
|
ResponseContainer &rc)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
const int deckId = cmd.deck_id();
|
||||||
|
const int ownerId = getDeckOwnerId(deckId);
|
||||||
|
if (ownerId == -1 || !isDeckEffectivelyPublic(deckId)) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckList *deck;
|
||||||
|
try {
|
||||||
|
deck = sqlInterface->getDeckFromDatabase(deckId, ownerId);
|
||||||
|
} catch (Response::ResponseCode &r) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response_DeckDownload *re = new Response_DeckDownload;
|
||||||
|
re->set_deck(deck->writeToString_Native().toStdString());
|
||||||
|
rc.setResponseExtension(re);
|
||||||
|
delete deck;
|
||||||
|
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd,
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd,
|
||||||
ResponseContainer & /*rc*/)
|
ResponseContainer & /*rc*/)
|
||||||
{
|
{
|
||||||
|
|
@ -678,11 +875,18 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
||||||
|
|
||||||
QSqlQuery *query =
|
QSqlQuery *query =
|
||||||
sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, "
|
sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, "
|
||||||
"content) values(:id_folder, :id_user, :name, NOW(), :content)");
|
"content, is_public, banner_card_name, banner_card_provider, color_identity, "
|
||||||
|
"tags) values(:id_folder, :id_user, :name, NOW(), :content, :is_public, "
|
||||||
|
":banner_card_name, :banner_card_provider, :color_identity, :tags)");
|
||||||
query->bindValue(":id_folder", folderId);
|
query->bindValue(":id_folder", folderId);
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userInfo->id());
|
||||||
query->bindValue(":name", deckName);
|
query->bindValue(":name", deckName);
|
||||||
query->bindValue(":content", deckStr);
|
query->bindValue(":content", deckStr);
|
||||||
|
query->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0);
|
||||||
|
query->bindValue(":banner_card_name", nameFromStdString(cmd.banner_card_name()));
|
||||||
|
query->bindValue(":banner_card_provider", nameFromStdString(cmd.banner_card_provider()));
|
||||||
|
query->bindValue(":color_identity", nameFromStdString(cmd.color_identity()));
|
||||||
|
query->bindValue(":tags", nameFromStdString(cmd.tags()));
|
||||||
sqlInterface->execSqlQuery(query);
|
sqlInterface->execSqlQuery(query);
|
||||||
|
|
||||||
Response_DeckUpload *re = new Response_DeckUpload;
|
Response_DeckUpload *re = new Response_DeckUpload;
|
||||||
|
|
@ -690,26 +894,42 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
||||||
fileInfo->set_id(query->lastInsertId().toInt());
|
fileInfo->set_id(query->lastInsertId().toInt());
|
||||||
fileInfo->set_name(deckName.toStdString());
|
fileInfo->set_name(deckName.toStdString());
|
||||||
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch());
|
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch());
|
||||||
|
fileInfo->mutable_file()->set_is_public(cmd.has_is_public() && cmd.is_public());
|
||||||
rc.setResponseExtension(re);
|
rc.setResponseExtension(re);
|
||||||
} else if (cmd.has_deck_id()) {
|
} else if (cmd.has_deck_id()) {
|
||||||
QSqlQuery *query =
|
QSqlQuery *query =
|
||||||
sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), "
|
sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), "
|
||||||
"content=:content where id = :id_deck and id_user = :id_user");
|
"content=:content, banner_card_name=:banner_card_name, "
|
||||||
|
"banner_card_provider=:banner_card_provider, color_identity=:color_identity, "
|
||||||
|
"tags=:tags where id = :id_deck and id_user = :id_user");
|
||||||
query->bindValue(":id_deck", cmd.deck_id());
|
query->bindValue(":id_deck", cmd.deck_id());
|
||||||
query->bindValue(":id_user", userInfo->id());
|
query->bindValue(":id_user", userInfo->id());
|
||||||
query->bindValue(":name", deckName);
|
query->bindValue(":name", deckName);
|
||||||
query->bindValue(":content", deckStr);
|
query->bindValue(":content", deckStr);
|
||||||
|
query->bindValue(":banner_card_name", nameFromStdString(cmd.banner_card_name()));
|
||||||
|
query->bindValue(":banner_card_provider", nameFromStdString(cmd.banner_card_provider()));
|
||||||
|
query->bindValue(":color_identity", nameFromStdString(cmd.color_identity()));
|
||||||
|
query->bindValue(":tags", nameFromStdString(cmd.tags()));
|
||||||
sqlInterface->execSqlQuery(query);
|
sqlInterface->execSqlQuery(query);
|
||||||
|
|
||||||
if (query->numRowsAffected() == 0) {
|
if (query->numRowsAffected() == 0) {
|
||||||
return Response::RespNameNotFound;
|
return Response::RespNameNotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSqlQuery *visibilityQuery =
|
||||||
|
sqlInterface->prepareQuery("select is_public from {prefix}_decklist_files where id = :id and "
|
||||||
|
"id_user = :id_user");
|
||||||
|
visibilityQuery->bindValue(":id", cmd.deck_id());
|
||||||
|
visibilityQuery->bindValue(":id_user", userInfo->id());
|
||||||
|
sqlInterface->execSqlQuery(visibilityQuery);
|
||||||
|
const bool isPublic = visibilityQuery->next() && visibilityQuery->value(0).toBool();
|
||||||
|
|
||||||
Response_DeckUpload *re = new Response_DeckUpload;
|
Response_DeckUpload *re = new Response_DeckUpload;
|
||||||
ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file();
|
ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file();
|
||||||
fileInfo->set_id(cmd.deck_id());
|
fileInfo->set_id(cmd.deck_id());
|
||||||
fileInfo->set_name(deckName.toStdString());
|
fileInfo->set_name(deckName.toStdString());
|
||||||
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch());
|
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch());
|
||||||
|
fileInfo->mutable_file()->set_is_public(isPublic);
|
||||||
rc.setResponseExtension(re);
|
rc.setResponseExtension(re);
|
||||||
} else {
|
} else {
|
||||||
return Response::RespInvalidData;
|
return Response::RespInvalidData;
|
||||||
|
|
@ -740,6 +960,179 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Comm
|
||||||
return Response::RespOk;
|
return Response::RespOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** @brief Builds a cryptographically random, URL-safe share token. */
|
||||||
|
QString generateShareToken()
|
||||||
|
{
|
||||||
|
QByteArray bytes(32, Qt::Uninitialized);
|
||||||
|
QRandomGenerator::system()->fillRange(reinterpret_cast<quint32 *>(bytes.data()), bytes.size() / sizeof(quint32));
|
||||||
|
return QString::fromLatin1(bytes.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Extracts the share metadata for a deck, materializing its content. */
|
||||||
|
DeckShareItemRecord makeShareItemFromDeck(const DeckList &deck, const QString &colorIdentity)
|
||||||
|
{
|
||||||
|
DeckShareItemRecord item;
|
||||||
|
item.name = deck.getName();
|
||||||
|
if (item.name.isEmpty()) {
|
||||||
|
item.name = "Unnamed deck";
|
||||||
|
}
|
||||||
|
item.tags = deck.getTags();
|
||||||
|
item.bannerCard = deck.getBannerCard().name;
|
||||||
|
item.gameFormat = deck.getGameFormat();
|
||||||
|
QString sanitizedColorIdentity;
|
||||||
|
for (const QChar &color : colorIdentity) {
|
||||||
|
const QChar upper = color.toUpper();
|
||||||
|
if (QStringLiteral("WUBRG").contains(upper) && !sanitizedColorIdentity.contains(upper)) {
|
||||||
|
sanitizedColorIdentity.append(upper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.colorIdentity = sanitizedColorIdentity;
|
||||||
|
item.content = deck.writeToString_Native();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const Command_DeckShareCreate &cmd,
|
||||||
|
ResponseContainer &rc)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
QList<DeckShareItemRecord> items;
|
||||||
|
if (cmd.items_size() > 0) {
|
||||||
|
for (const DeckShareItem &shareItem : cmd.items()) {
|
||||||
|
if (shareItem.has_deck_list()) {
|
||||||
|
DeckList deck;
|
||||||
|
if (!deck.loadFromString_Native(fileFromStdString(shareItem.deck_list()))) {
|
||||||
|
return Response::RespContextError;
|
||||||
|
}
|
||||||
|
items.append(makeShareItemFromDeck(deck, nameFromStdString(shareItem.color_identity())));
|
||||||
|
} else if (shareItem.has_deck_id()) {
|
||||||
|
DeckList *deck;
|
||||||
|
try {
|
||||||
|
deck = sqlInterface->getDeckFromDatabase(shareItem.deck_id(), userInfo->id());
|
||||||
|
} catch (Response::ResponseCode &r) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
items.append(makeShareItemFromDeck(*deck, nameFromStdString(shareItem.color_identity())));
|
||||||
|
delete deck;
|
||||||
|
} else {
|
||||||
|
return Response::RespInvalidData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (cmd.has_folder_path()) {
|
||||||
|
const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path()));
|
||||||
|
if (folderId == -1) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id_folder = "
|
||||||
|
":id_folder and id_user = :id_user");
|
||||||
|
query->bindValue(":id_folder", folderId);
|
||||||
|
query->bindValue(":id_user", userInfo->id());
|
||||||
|
sqlInterface->execSqlQuery(query);
|
||||||
|
while (query->next()) {
|
||||||
|
DeckList *deck;
|
||||||
|
try {
|
||||||
|
deck = sqlInterface->getDeckFromDatabase(query->value(0).toInt(), userInfo->id());
|
||||||
|
} catch (Response::ResponseCode &r) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
items.append(makeShareItemFromDeck(*deck, QString()));
|
||||||
|
delete deck;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Response::RespInvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return Response::RespInvalidData;
|
||||||
|
}
|
||||||
|
const int maxItems = servatrice->getDeckShareMaxDecksPerShare();
|
||||||
|
if (items.size() > maxItems) {
|
||||||
|
return Response::RespTooManyRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString shareName = nameFromStdString(cmd.name());
|
||||||
|
if (shareName.isEmpty()) {
|
||||||
|
shareName = "Shared decks";
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString token = generateShareToken();
|
||||||
|
if (!sqlInterface->createDeckShare(token, shareName, userInfo->id(), items, servatrice->getDeckShareExpiryDays())) {
|
||||||
|
return Response::RespInvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response_DeckShareCreate *re = new Response_DeckShareCreate;
|
||||||
|
re->set_token(token.toStdString());
|
||||||
|
re->set_expires_at(
|
||||||
|
QDateTime::currentDateTimeUtc().addDays(servatrice->getDeckShareExpiryDays()).toSecsSinceEpoch());
|
||||||
|
re->set_item_count(items.size());
|
||||||
|
rc.setResponseExtension(re);
|
||||||
|
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareList(const Command_DeckShareList &cmd,
|
||||||
|
ResponseContainer &rc)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
QString name;
|
||||||
|
qint64 expiresAt = 0;
|
||||||
|
QList<DeckShareItemRecord> items;
|
||||||
|
if (!sqlInterface->getDeckShareList(nameFromStdString(cmd.token()), name, expiresAt, items)) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response_DeckShareList *re = new Response_DeckShareList;
|
||||||
|
re->set_name(name.toStdString());
|
||||||
|
re->set_expires_at(expiresAt);
|
||||||
|
for (const DeckShareItemRecord &item : items) {
|
||||||
|
ServerInfo_DeckShareItem *itemInfo = re->add_items();
|
||||||
|
itemInfo->set_id(item.id);
|
||||||
|
itemInfo->set_name(item.name.toStdString());
|
||||||
|
for (const QString &tag : item.tags) {
|
||||||
|
itemInfo->add_tags(tag.toStdString());
|
||||||
|
}
|
||||||
|
itemInfo->set_banner_card(item.bannerCard.toStdString());
|
||||||
|
itemInfo->set_game_format(item.gameFormat.toStdString());
|
||||||
|
itemInfo->set_color_identity(item.colorIdentity.toStdString());
|
||||||
|
}
|
||||||
|
rc.setResponseExtension(re);
|
||||||
|
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareDownload(const Command_DeckShareDownload &cmd,
|
||||||
|
ResponseContainer &rc)
|
||||||
|
{
|
||||||
|
if (authState != PasswordRight) {
|
||||||
|
return Response::RespFunctionNotAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlInterface->checkSql();
|
||||||
|
|
||||||
|
QString content;
|
||||||
|
if (!sqlInterface->getDeckShareItem(nameFromStdString(cmd.token()), cmd.item_id(), content)) {
|
||||||
|
return Response::RespNameNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response_DeckShareDownload *re = new Response_DeckShareDownload;
|
||||||
|
re->set_deck(content.toStdString());
|
||||||
|
rc.setResponseExtension(re);
|
||||||
|
|
||||||
|
return Response::RespOk;
|
||||||
|
}
|
||||||
|
|
||||||
Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/,
|
Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/,
|
||||||
ResponseContainer &rc)
|
ResponseContainer &rc)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,17 @@ class ServerInfo_DeckStorage_Folder;
|
||||||
class Command_AddToList;
|
class Command_AddToList;
|
||||||
class Command_RemoveFromList;
|
class Command_RemoveFromList;
|
||||||
class Command_DeckList;
|
class Command_DeckList;
|
||||||
|
class Command_DeckListOtherUser;
|
||||||
class Command_DeckNewDir;
|
class Command_DeckNewDir;
|
||||||
class Command_DeckDelDir;
|
class Command_DeckDelDir;
|
||||||
class Command_DeckDel;
|
class Command_DeckDel;
|
||||||
class Command_DeckDownload;
|
class Command_DeckDownload;
|
||||||
|
class Command_DeckDownloadPublic;
|
||||||
class Command_DeckUpload;
|
class Command_DeckUpload;
|
||||||
|
class Command_DeckSetVisibility;
|
||||||
|
class Command_DeckShareCreate;
|
||||||
|
class Command_DeckShareList;
|
||||||
|
class Command_DeckShareDownload;
|
||||||
class Command_ReplayList;
|
class Command_ReplayList;
|
||||||
class Command_ReplayDownload;
|
class Command_ReplayDownload;
|
||||||
class Command_ReplayModifyMatch;
|
class Command_ReplayModifyMatch;
|
||||||
|
|
@ -95,8 +101,16 @@ private:
|
||||||
Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc);
|
||||||
int getDeckPathId(int basePathId, QStringList path);
|
int getDeckPathId(int basePathId, QStringList path);
|
||||||
int getDeckPathId(const QString &path);
|
int getDeckPathId(const QString &path);
|
||||||
bool deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder);
|
bool deckListHelper(int folderId,
|
||||||
|
ServerInfo_DeckStorage_Folder *folder,
|
||||||
|
int userId,
|
||||||
|
bool inheritedPublic,
|
||||||
|
bool publicOnly);
|
||||||
|
int getDeckOwnerId(int deckId);
|
||||||
|
bool isDeckEffectivelyPublic(int deckId);
|
||||||
Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckList(const Command_DeckList &cmd, ResponseContainer &rc);
|
||||||
|
Response::ResponseCode cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd, ResponseContainer &rc);
|
||||||
|
Response::ResponseCode cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer &rc);
|
||||||
void deckDelDirHelper(int basePathId);
|
void deckDelDirHelper(int basePathId);
|
||||||
void sendServerMessage(const QString userName, const QString message);
|
void sendServerMessage(const QString userName, const QString message);
|
||||||
|
|
@ -105,6 +119,10 @@ private:
|
||||||
Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc);
|
||||||
DeckList *getDeckFromDatabase(int deckId);
|
DeckList *getDeckFromDatabase(int deckId);
|
||||||
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
|
||||||
|
Response::ResponseCode cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc);
|
||||||
|
Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc);
|
||||||
|
Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc);
|
||||||
|
Response::ResponseCode cmdDeckShareDownload(const Command_DeckShareDownload &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdReplayList(const Command_ReplayList &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer &rc);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue