Define Cockatrice as an editor/handler for .cod files and cockatrice:// protocol on all platforms (#6775)

* [Application] Add single instance guard and mime types.

Took 2 hours 39 minutes

Took 18 minutes

Took 5 minutes

Took 12 seconds


Took 11 seconds

* Rework

Took 30 minutes


Took 50 seconds

* Only enforce single instance if launched with arguments.

Took 5 minutes

* Prototype intents

Took 53 minutes

Took 6 seconds

* Connect/disconnect and join game/room intents.

Took 3 hours 14 minutes

Took 2 seconds

Took 15 seconds

* Fix include.

Took 1 minute


Took 23 seconds

Took 2 seconds

* Mac handling.

Took 10 minutes

Took 12 seconds

Took 3 minutes

* Lint.

Took 3 minutes

* Rebase.

Took 3 minutes

Took 17 seconds

* Implement UrlSchemeEventFilter

Took 10 minutes

Took 7 seconds

* Qt Moc

Took 3 minutes

* Modern PList.

Took 21 minutes

Took 1 minute

* Debug output.

Took 6 minutes

Took 19 minutes

* Watch file:// prefix.

Took 15 minutes

Took 7 seconds

* Better handler.

Took 6 minutes

* Don't store reference in member

Took 5 minutes

* Move impl to cpp, fix lifetime issues.

Took 11 minutes

Took 2 minutes

* Better single-instance handoff, url intent harded

copy game link context-menu
Polish for installers

Took 35 minutes

Took 8 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-08 19:02:19 +02:00 committed by GitHub
parent 27fb5e51de
commit 59d90db3c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1372 additions and 13 deletions

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H
#include <QString>
struct ContextConnectToServer
{
QString hostname;
QString port;
QString username;
QString password;
};
#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H

View file

@ -0,0 +1,11 @@
#ifndef COCKATRICE_CONTEXT_JOIN_GAME_H
#define COCKATRICE_CONTEXT_JOIN_GAME_H
#include "context_join_room.h"
struct ContextJoinGame
{
ContextJoinRoom roomContext;
int gameId;
};
#endif // COCKATRICE_CONTEXT_JOIN_GAME_H

View file

@ -0,0 +1,14 @@
#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H
#define COCKATRICE_CONTEXT_JOIN_ROOM_H
#include "context_connect_to_server.h"
#include <QString>
struct ContextJoinRoom
{
ContextConnectToServer serverContext;
int roomId;
};
#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H

View file

@ -0,0 +1,48 @@
#include "intent.h"
Intent::Intent(QObject *parent) : QObject(parent)
{
// An intent is done as soon as it reports success or failure. Deleting it
// also tears down its dependency chain and disconnects any signal wiring.
connect(this, &Intent::finished, this, &QObject::deleteLater);
connect(this, &Intent::failed, this, &QObject::deleteLater);
}
Intent::~Intent() = default;
void Intent::execute()
{
if (checkPrecondition()) {
onPreconditionSatisfied();
} else {
onPreconditionNotSatisfied();
}
}
void Intent::runDependency(Intent *dependency)
{
dependency->setParent(this);
connect(dependency, &Intent::finished, this, [this]() {
// Re-check after dependency finishes
this->execute();
});
connect(dependency, &Intent::failed, this, &Intent::failed);
dependency->execute();
}
void Intent::emitFinished()
{
if (!completed) {
completed = true;
emit finished();
}
}
void Intent::emitFailed(const QString &reason)
{
if (!completed) {
completed = true;
emit failed(reason);
}
}

View file

@ -0,0 +1,37 @@
#ifndef COCKATRICE_INTENT_H
#define COCKATRICE_INTENT_H
#include <QObject>
class Intent : public QObject
{
Q_OBJECT
public:
explicit Intent(QObject *parent = nullptr);
~Intent() override;
void execute();
signals:
void finished();
void failed(QString reason);
protected:
// --- Subclasses must implement these ---
virtual bool checkPrecondition() const = 0;
virtual void onPreconditionSatisfied() = 0;
virtual void onPreconditionNotSatisfied() = 0;
// Helper to chain another intent
void runDependency(Intent *dependency);
// Emit the outcome exactly once; ignore late signals after the intent is done.
void emitFinished();
void emitFailed(const QString &reason);
private:
bool completed = false;
};
#endif // COCKATRICE_INTENT_H

View file

@ -0,0 +1,46 @@
#include "intent_connect_to_server.h"
#include "intent_disconnect_from_server.h"
#include <QTimer>
IntentConnectToServer::IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context)
: Intent(), remoteClient(_remoteClient), context(_context)
{
}
bool IntentConnectToServer::checkPrecondition() const
{
return remoteClient->getStatus() == ClientStatus::StatusDisconnected;
}
void IntentConnectToServer::onPreconditionSatisfied()
{
remoteClient->connectToServer(context->hostname, context->port.toUInt(), context->username, context->password);
connect(remoteClient, &RemoteClient::statusChanged, this, &IntentConnectToServer::onStatusChanged);
connect(remoteClient, &RemoteClient::socketError, this, &IntentConnectToServer::onSocketError);
connect(
remoteClient, &RemoteClient::loginError, this,
[this](Response::ResponseCode, const QString &reason, quint32, const QList<QString> &) { emitFailed(reason); });
QTimer::singleShot(15000, this, [this]() {
emitFailed(tr("Timed out while connecting to %1:%2").arg(context->hostname, context->port));
});
}
void IntentConnectToServer::onPreconditionNotSatisfied()
{
runDependency(new IntentDisconnectFromServer(remoteClient));
}
void IntentConnectToServer::onStatusChanged(ClientStatus status)
{
if (status == ClientStatus::StatusLoggedIn) {
emitFinished();
}
}
void IntentConnectToServer::onSocketError(const QString &errorString)
{
emitFailed(tr("Failed to connect to %1:%2: %3").arg(context->hostname, context->port, errorString));
}

View file

@ -0,0 +1,29 @@
#ifndef COCKATRICE_INTENT_CONNECT_TO_SERVER_H
#define COCKATRICE_INTENT_CONNECT_TO_SERVER_H
#include "contexts/context_connect_to_server.h"
#include "intent.h"
#include "remote_client.h"
class IntentConnectToServer : public Intent
{
Q_OBJECT
public:
IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
RemoteClient *remoteClient;
ContextConnectToServer *context;
private slots:
void onStatusChanged(ClientStatus status);
void onSocketError(const QString &errorString);
};
#endif // COCKATRICE_INTENT_CONNECT_TO_SERVER_H

View file

@ -0,0 +1,29 @@
#include "intent_disconnect_from_server.h"
IntentDisconnectFromServer::IntentDisconnectFromServer(RemoteClient *_remoteClient)
: Intent(), remoteClient(_remoteClient)
{
}
bool IntentDisconnectFromServer::checkPrecondition() const
{
return remoteClient->getStatus() == ClientStatus::StatusDisconnected;
}
void IntentDisconnectFromServer::onPreconditionSatisfied()
{
emitFinished();
}
void IntentDisconnectFromServer::onPreconditionNotSatisfied()
{
connect(remoteClient, &RemoteClient::statusChanged, this, &IntentDisconnectFromServer::onStatusChanged);
remoteClient->disconnectFromServer();
}
void IntentDisconnectFromServer::onStatusChanged(ClientStatus status)
{
if (status == ClientStatus::StatusDisconnected) {
emitFinished();
}
}

View file

@ -0,0 +1,26 @@
#ifndef COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H
#define COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H
#include "intent.h"
#include "remote_client.h"
class IntentDisconnectFromServer : public Intent
{
Q_OBJECT
public:
IntentDisconnectFromServer(RemoteClient *_remoteClient);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
RemoteClient *remoteClient;
private slots:
void onStatusChanged(ClientStatus status);
};
#endif // COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H

View file

@ -0,0 +1,76 @@
#include "intent_join_server_game.h"
#include "../widgets/server/game_selector.h"
#include "../widgets/tabs/tab_room.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_join_server_room.h"
#include <QTimer>
IntentJoinServerGame::IntentJoinServerGame(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinGame> _context)
: Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release())
{
}
bool IntentJoinServerGame::checkPrecondition() const
{
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
return false;
}
// peerPort() reflects the actual TCP peer, which may differ from the
// configured server port (e.g. when connecting through a proxy), so only
// the hostname is compared here.
if (remoteClient->peerName() != context->roomContext.serverContext.hostname) {
return false;
}
if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) {
return false;
}
if (!tabSupervisor->getRoomTabs().contains(context->roomContext.roomId)) {
return false;
}
return true;
}
void IntentJoinServerGame::onPreconditionSatisfied()
{
TabRoom *room = tabSupervisor->getRoomTabs().value(context->roomContext.roomId);
if (!tryJoinGame(room)) {
waitForGame(room);
}
}
void IntentJoinServerGame::onPreconditionNotSatisfied()
{
runDependency(new IntentJoinServerRoom(tabSupervisor, remoteClient, &context->roomContext));
}
bool IntentJoinServerGame::tryJoinGame(TabRoom *room)
{
if (!room) {
return false;
}
if (room->getGameSelector()->joinGameById(context->gameId)) {
emitFinished();
return true;
}
return false;
}
void IntentJoinServerGame::waitForGame(TabRoom *room)
{
connect(room, &TabRoom::gameListUpdated, this, [this]() {
TabRoom *updatedRoom = tabSupervisor->getRoomTabs().value(context->roomContext.roomId);
if (updatedRoom) {
tryJoinGame(updatedRoom);
}
});
QTimer::singleShot(15000, this, [this]() { emitFailed(tr("Game %1 not found in the room").arg(context->gameId)); });
}

View file

@ -0,0 +1,37 @@
#ifndef COCKATRICE_INTENT_JOIN_SERVER_GAME_H
#define COCKATRICE_INTENT_JOIN_SERVER_GAME_H
#include "contexts/context_join_game.h"
#include "intent.h"
#include "remote_client.h"
#include <QScopedPointer>
#include <memory>
class TabRoom;
class TabSupervisor;
class IntentJoinServerGame : public Intent
{
Q_OBJECT
public:
IntentJoinServerGame(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinGame> _context);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
bool tryJoinGame(TabRoom *room);
void waitForGame(TabRoom *room);
TabSupervisor *tabSupervisor;
RemoteClient *remoteClient;
QScopedPointer<ContextJoinGame> context;
};
#endif // COCKATRICE_INTENT_JOIN_SERVER_GAME_H

View file

@ -0,0 +1,74 @@
#include "intent_join_server_room.h"
#include "../widgets/tabs/tab_room.h"
#include "../widgets/tabs/tab_server.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_connect_to_server.h"
#include <QTimer>
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
IntentJoinServerRoom::IntentJoinServerRoom(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
ContextJoinRoom *_context)
: Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context)
{
}
bool IntentJoinServerRoom::checkPrecondition() const
{
if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) {
return false;
}
// peerPort() reflects the actual TCP peer, which may differ from the
// configured server port (e.g. when connecting through a proxy), so only
// the hostname is compared here.
if (remoteClient->peerName() != context->serverContext.hostname) {
return false;
}
if (QString::number(remoteClient->peerPort()) != context->serverContext.port) {
return false;
}
return true;
}
void IntentJoinServerRoom::onPreconditionSatisfied()
{
if (tabSupervisor->getRoomTabs().contains(context->roomId)) {
tabSupervisor->setCurrentWidget(tabSupervisor->getRoomTabs().value(context->roomId));
emitFinished();
return;
}
TabServer *tabServer = tabSupervisor->getTabServer();
if (!tabServer) {
tabSupervisor->openTabServer();
tabServer = tabSupervisor->getTabServer();
}
if (!tabServer) {
emitFailed(tr("No server tab available"));
return;
}
const int roomId = context->roomId;
tabServer->joinRoom(roomId, true);
connect(tabServer, &TabServer::roomJoined, this, [this, roomId](const ServerInfo_Room &info, bool) {
if (info.room_id() == roomId) {
emitFinished();
}
});
connect(tabServer, &TabServer::roomJoinFailed, this, [this, roomId](int failedRoomId) {
if (failedRoomId == roomId) {
emitFailed(tr("Failed to join the server room %1").arg(roomId));
}
});
QTimer::singleShot(15000, this,
[this, roomId]() { emitFailed(tr("Timed out while joining the server room %1").arg(roomId)); });
}
void IntentJoinServerRoom::onPreconditionNotSatisfied()
{
runDependency(new IntentConnectToServer(remoteClient, &context->serverContext));
}

View file

@ -0,0 +1,28 @@
#ifndef COCKATRICE_INTENT_JOIN_SERVER_ROOM_H
#define COCKATRICE_INTENT_JOIN_SERVER_ROOM_H
#include "contexts/context_join_room.h"
#include "intent.h"
#include "remote_client.h"
class TabSupervisor;
class IntentJoinServerRoom : public Intent
{
Q_OBJECT
public:
IntentJoinServerRoom(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinRoom *_context);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
TabSupervisor *tabSupervisor;
RemoteClient *remoteClient;
ContextJoinRoom *context;
};
#endif // COCKATRICE_INTENT_JOIN_SERVER_ROOM_H

View file

@ -0,0 +1,33 @@
#include "intent_login.h"
#include "../../client/settings/cache_settings.h"
#include "libcockatrice/settings/servers_settings.h"
IntentGetLoginCredentials::IntentGetLoginCredentials(ContextConnectToServer *_context) : Intent(), context(_context)
{
}
bool IntentGetLoginCredentials::checkPrecondition() const
{
ServersSettings &servers = SettingsCache::instance().servers();
return servers.hasLoginData(context->hostname, context->port);
}
void IntentGetLoginCredentials::onPreconditionSatisfied()
{
ServersSettings &servers = SettingsCache::instance().servers();
const int index = servers.findServerIndex(context->hostname, context->port);
if (index >= 0) {
context->username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString();
context->password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString();
emitFinished();
} else {
emitFailed(tr("No saved credentials for this server"));
}
}
void IntentGetLoginCredentials::onPreconditionNotSatisfied()
{
emitFailed(tr("No saved credentials for this server"));
}

View file

@ -0,0 +1,23 @@
#ifndef COCKATRICE_INTENT_LOGIN_H
#define COCKATRICE_INTENT_LOGIN_H
#include "contexts/context_connect_to_server.h"
#include "intent.h"
class IntentGetLoginCredentials : public Intent
{
Q_OBJECT
public:
IntentGetLoginCredentials(ContextConnectToServer *_context);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
ContextConnectToServer *context;
};
#endif // COCKATRICE_INTENT_LOGIN_H

View file

@ -0,0 +1,34 @@
#include "intent_open_local_deck.h"
#include "../deck_loader/deck_file_format.h"
#include "../deck_loader/deck_loader.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_wait_for_database_load.h"
#include <libcockatrice/card/database/card_database_manager.h>
IntentOpenLocalDeck::IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file)
: Intent(), tabSupervisor(_tabSupervisor), file(_file)
{
}
bool IntentOpenLocalDeck::checkPrecondition() const
{
return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok;
}
void IntentOpenLocalDeck::onPreconditionSatisfied()
{
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true);
if (deckOpt) {
tabSupervisor->openDeckInNewTab(deckOpt.value());
emitFinished();
} else {
emitFailed(tr("Unable to load deck file %1").arg(file));
}
}
void IntentOpenLocalDeck::onPreconditionNotSatisfied()
{
runDependency(new IntentWaitForDatabaseLoad);
}

View file

@ -0,0 +1,27 @@
#ifndef COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
#define COCKATRICE_INTENT_OPEN_LOCAL_DECK_H
#include "intent.h"
#include <QString>
class TabSupervisor;
class IntentOpenLocalDeck : public Intent
{
Q_OBJECT
public:
IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file);
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
private:
TabSupervisor *tabSupervisor;
QString file;
};
#endif // COCKATRICE_INTENT_OPEN_LOCAL_DECK_H

View file

@ -0,0 +1,19 @@
#include "intent_wait_for_database_load.h"
#include <libcockatrice/card/database/card_database_manager.h>
bool IntentWaitForDatabaseLoad::checkPrecondition() const
{
return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok;
}
void IntentWaitForDatabaseLoad::onPreconditionSatisfied()
{
emitFinished();
}
void IntentWaitForDatabaseLoad::onPreconditionNotSatisfied()
{
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
[this]() { emitFinished(); });
}

View file

@ -0,0 +1,16 @@
#ifndef COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H
#define COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H
#include "intent.h"
class IntentWaitForDatabaseLoad : public Intent
{
Q_OBJECT
protected:
bool checkPrecondition() const override;
void onPreconditionSatisfied() override;
void onPreconditionNotSatisfied() override;
};
#endif // COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H

View file

@ -0,0 +1,89 @@
#include "url_parser.h"
#include "../window_main.h"
#include "contexts/context_join_game.h"
#include "intent_join_server_game.h"
#include "intent_login.h"
#include <QDebug>
#include <QMessageBox>
#include <QUrl>
#include <QUrlQuery>
#include <memory>
IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow)
{
}
void IntentUrlParser::handle(const QString &urlStr)
{
QUrl url(urlStr);
if (url.scheme() != "cockatrice") {
return;
}
const QString action = url.host();
QUrlQuery query(url);
if (action == "joingame") {
handleJoinGame(query);
} else if (action == "opendeck") {
// handleOpenDeck(query);
} else {
qWarning() << "Unknown intent:" << action;
}
}
void IntentUrlParser::handleJoinGame(const QUrlQuery &query)
{
auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); };
auto ctx = std::make_unique<ContextJoinGame>();
ctx->roomContext.serverContext.hostname = query.queryItemValue("hostname");
ctx->roomContext.serverContext.port = query.queryItemValue("port");
if (ctx->roomContext.serverContext.hostname.isEmpty()) {
showError(tr("Missing or empty hostname in the game link"));
return;
}
bool ok = false;
ctx->roomContext.serverContext.port.toUShort(&ok);
if (!ok) {
showError(tr("Invalid or missing port in the game link"));
return;
}
ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok);
if (!ok) {
showError(tr("Invalid or missing room id in the game link"));
return;
}
ok = false;
ctx->gameId = query.queryItemValue("gameid").toInt(&ok);
if (!ok) {
showError(tr("Invalid or missing game id in the game link"));
return;
}
// The join game intent owns the context and the credential lookup; once the
// chain finishes (or fails) it deletes the whole tree.
ContextConnectToServer *serverContext = &ctx->roomContext.serverContext;
auto joinGameIntent =
new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), std::move(ctx));
joinGameIntent->setParent(this);
auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext);
getLoginCredentialsIntent->setParent(joinGameIntent);
connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute);
connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed);
connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); });
getLoginCredentialsIntent->execute();
}

View file

@ -0,0 +1,20 @@
#ifndef COCKATRICE_URL_PARSER_H
#define COCKATRICE_URL_PARSER_H
#include <QObject>
#include <QUrlQuery>
class MainWindow;
class IntentUrlParser : public QObject
{
Q_OBJECT
public:
IntentUrlParser(QObject *parent, MainWindow *mainWindow);
void handle(const QString &urlStr);
void handleJoinGame(const QUrlQuery &query);
private:
MainWindow *mainWindow;
};
#endif // COCKATRICE_URL_PARSER_H

View file

@ -10,12 +10,16 @@
#include "games_model.h"
#include "user/user_list_manager.h"
#include <QClipboard>
#include <QDebug>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QPushButton>
#include <QTreeView>
#include <QUrl>
#include <QUrlQuery>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h>
@ -315,6 +319,21 @@ void GameSelector::customContextMenu(const QPoint &point)
dlg.exec();
});
QAction copyLink(tr("Copy Game Link"));
connect(&copyLink, &QAction::triggered, this, [=, this]() {
const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt());
QUrl url;
url.setScheme("cockatrice");
url.setHost("joingame");
QUrlQuery query;
query.addQueryItem("hostname", client->serverName());
query.addQueryItem("port", QString::number(client->serverPort()));
query.addQueryItem("roomid", QString::number(gameInfo.room_id()));
query.addQueryItem("gameid", QString::number(gameInfo.game_id()));
url.setQuery(query);
QGuiApplication::clipboard()->setText(url.toString(QUrl::FullyEncoded));
});
QMenu menu;
menu.addAction(&joinGame);
@ -332,6 +351,11 @@ void GameSelector::customContextMenu(const QPoint &point)
menu.addAction(&spectateGame);
menu.addAction(&getGameInfo);
if (!client->serverName().isEmpty()) {
menu.addAction(&copyLink);
}
menu.exec(gameListView->mapToGlobal(point));
}
@ -379,6 +403,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
disableButtons();
}
bool GameSelector::joinGameById(int gameId)
{
auto *model = gameListView->model();
for (int row = 0; row < model->rowCount(); ++row) {
QModelIndex idx = model->index(row, 0);
const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt());
if (game.game_id() == gameId) {
gameListView->setCurrentIndex(idx);
joinGame();
return true;
}
}
qWarning() << "Game" << gameId << "not found";
return false;
}
void GameSelector::disableButtons()
{
if (createButton) {

View file

@ -202,6 +202,7 @@ public:
* @param info The ServerInfo_Game object containing information about the game to update.
*/
void processGameInfo(const ServerInfo_Game &info);
bool joinGameById(int gameId);
};
#endif

View file

@ -280,6 +280,7 @@ void TabRoom::processListGamesEvent(const Event_ListGames &event)
for (int i = 0; i < gameListSize; ++i) {
gameSelector->processGameInfo(event.game_list(i));
}
emit gameListUpdated();
}
void TabRoom::processJoinRoomEvent(const Event_JoinRoom &event)

View file

@ -78,6 +78,7 @@ signals:
void openMessageDialog(const QString &userName, bool focus);
void maximizeClient();
void notIdle();
void gameListUpdated();
private slots:
void sendMessage();
void sayFinished(const Response &response);
@ -127,6 +128,10 @@ public:
{
return ownUser;
}
[[nodiscard]] GameSelector *getGameSelector() const
{
return gameSelector;
}
PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd);
void sendRoomCommand(PendingCommand *pend);

View file

@ -191,7 +191,10 @@ void TabServer::joinRoom(int id, bool setCurrent)
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(setCurrent);
connect(pend, &PendingCommand::finished, this, &TabServer::joinRoomFinished);
connect(pend, &PendingCommand::finished, this,
[this, id](const Response &r, const CommandContainer &c, const QVariant &v) {
joinRoomFinished(r, c, v, id);
});
client->sendCommand(pend);
@ -205,7 +208,8 @@ void TabServer::joinRoom(int id, bool setCurrent)
void TabServer::joinRoomFinished(const Response &r,
const CommandContainer & /*commandContainer*/,
const QVariant &extraData)
const QVariant &extraData,
int roomId)
{
switch (r.response_code()) {
case Response::RespOk:
@ -213,21 +217,25 @@ void TabServer::joinRoomFinished(const Response &r,
case Response::RespNameNotFound:
QMessageBox::critical(this, tr("Error"),
tr("Failed to join the server room: it doesn't exist on the server."));
emit roomJoinFailed(roomId);
return;
case Response::RespContextError:
QMessageBox::critical(
this, tr("Error"),
tr("The server thinks you are in the server room but your client is unable to display it. "
"Try restarting your client."));
emit roomJoinFailed(roomId);
return;
case Response::RespUserLevelTooLow:
QMessageBox::critical(this, tr("Error"),
tr("You do not have the required permission to join this server room."));
emit roomJoinFailed(roomId);
return;
default:
QMessageBox::critical(
this, tr("Error"),
tr("Failed to join the server room due to an unknown error: %1.").arg(r.response_code()));
emit roomJoinFailed(roomId);
return;
}

View file

@ -49,10 +49,13 @@ class TabServer : public Tab
Q_OBJECT
signals:
void roomJoined(const ServerInfo_Room &info, bool setCurrent);
void roomJoinFailed(int roomId);
private slots:
void processServerMessageEvent(const Event_ServerMessage &event);
void joinRoom(int id, bool setCurrent);
void joinRoomFinished(const Response &resp, const CommandContainer &commandContainer, const QVariant &extraData);
void joinRoomFinished(const Response &resp,
const CommandContainer &commandContainer,
const QVariant &extraData,
int roomId);
private:
AbstractClient *client;
@ -62,6 +65,7 @@ private:
public:
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void joinRoom(int id, bool setCurrent);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{

View file

@ -587,6 +587,10 @@ void TabSupervisor::actTabServer(bool checked)
void TabSupervisor::openTabServer()
{
if (tabServer) {
return;
}
tabServer = new TabServer(this, client);
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
myAddTab(tabServer, aTabServer);

View file

@ -152,6 +152,10 @@ public:
{
return userListManager;
}
[[nodiscard]] TabServer *getTabServer() const
{
return tabServer;
}
[[nodiscard]] const QMap<int, TabRoom *> &getRoomTabs() const
{
return roomTabs;
@ -183,6 +187,7 @@ public slots:
void maximizeMainWindow();
void actTabVisualDeckStorage(bool checked);
void actTabReplays(bool checked);
void openTabServer();
private slots:
void refreshShortcuts();
@ -195,7 +200,6 @@ private slots:
void openTabVisualDeckStorage();
void openTabHome();
void openTabServer();
void openTabAccount();
void openTabDeckStorage();
void openTabReplays();

View file

@ -150,6 +150,11 @@ public:
}
~MainWindow() override;
RemoteClient *getRemoteClient() const
{
return connectionController->client();
}
TabSupervisor *getTabSupervisor() const
{
return tabSupervisor;