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

@ -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();