mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-08-20 15:55:12 -05:00
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:
@@ -1,38 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- CORE BUNDLE METADATA -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
|
||||
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
|
||||
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
|
||||
<key>CFBundleLongVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleName</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
|
||||
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
<key>LSRequiresCarbon</key>
|
||||
<true/>
|
||||
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- FILE TYPE (.cod) SUPPORT -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.cockatrice.deck</string>
|
||||
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Cockatrice Deck</string>
|
||||
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>cod</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Cockatrice Deck</string>
|
||||
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.cockatrice.deck</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- URL SCHEME (cockatrice://) -->
|
||||
<!-- ========================= -->
|
||||
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Cockatrice URL Scheme</string>
|
||||
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>cockatrice</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -294,6 +294,20 @@ Section "Application" SecApplication
|
||||
SetShellVarContext all
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
${If} $PortableMode = 0
|
||||
|
||||
; --- Register .cod file type ---
|
||||
WriteRegStr HKCR ".cod" "" "Cockatrice"
|
||||
WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File"
|
||||
WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
|
||||
|
||||
; --- Register custom URI protocol ---
|
||||
WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol"
|
||||
WriteRegStr HKCR "cockatrice" "URL Protocol" ""
|
||||
WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"'
|
||||
|
||||
${EndIf}
|
||||
|
||||
${If} $PortableMode = 1
|
||||
${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
||||
; upgrade portable mode
|
||||
@@ -402,6 +416,18 @@ Section "un.Application" UnSecApplication
|
||||
RMDir "$SMPROGRAMS\Cockatrice"
|
||||
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
||||
|
||||
; Only remove the file/protocol associations if we registered them (i.e. the
|
||||
; install was not portable) and .cod is still owned by Cockatrice, so we don't
|
||||
; clobber a .cod association installed by another application.
|
||||
${If} Not ${FileExists} "$INSTDIR\portable.dat"
|
||||
ReadRegStr $0 HKCR ".cod" ""
|
||||
${If} $0 == "Cockatrice"
|
||||
DeleteRegKey HKCR ".cod"
|
||||
DeleteRegKey HKCR "Cockatrice"
|
||||
DeleteRegKey HKCR "cockatrice"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
; unselected because it is /o
|
||||
|
||||
@@ -135,6 +135,12 @@ set(cockatrice_SOURCES
|
||||
src/interface/card_picture_loader/card_picture_loader_worker.cpp
|
||||
src/interface/card_picture_loader/card_picture_loader_worker_work.cpp
|
||||
src/interface/card_picture_loader/card_picture_to_load.cpp
|
||||
src/interface/intents/intent.cpp
|
||||
src/interface/intents/intent.h
|
||||
src/interface/intents/intent_open_local_deck.cpp
|
||||
src/interface/intents/intent_open_local_deck.h
|
||||
src/interface/intents/intent_wait_for_database_load.cpp
|
||||
src/interface/intents/intent_wait_for_database_load.h
|
||||
src/interface/layouts/flow_layout.cpp
|
||||
src/interface/layouts/overlap_layout.cpp
|
||||
src/interface/widgets/utility/line_edit_completer.cpp
|
||||
@@ -293,6 +299,7 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
|
||||
src/interface/window_main.cpp
|
||||
src/main.cpp
|
||||
src/single_instance_manager.cpp
|
||||
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp
|
||||
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp
|
||||
@@ -360,6 +367,20 @@ set(cockatrice_SOURCES
|
||||
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
|
||||
src/interface/widgets/utility/compact_push_button.cpp
|
||||
src/interface/widgets/utility/compact_push_button.h
|
||||
src/single_instance_manager.h
|
||||
src/client/url_scheme_event_filter.h
|
||||
src/interface/intents/intent_connect_to_server.cpp
|
||||
src/interface/intents/intent_connect_to_server.h
|
||||
src/interface/intents/intent_disconnect_from_server.cpp
|
||||
src/interface/intents/intent_disconnect_from_server.h
|
||||
src/interface/intents/intent_join_server_game.cpp
|
||||
src/interface/intents/intent_join_server_game.h
|
||||
src/interface/intents/intent_join_server_room.cpp
|
||||
src/interface/intents/intent_join_server_room.h
|
||||
src/interface/intents/intent_login.cpp
|
||||
src/interface/intents/intent_login.h
|
||||
src/interface/intents/url_parser.cpp
|
||||
src/interface/intents/url_parser.h
|
||||
src/interface/widgets/server/user/user_info_popup.cpp
|
||||
src/interface/widgets/server/user/user_info_popup.h
|
||||
)
|
||||
@@ -419,6 +440,11 @@ set(DESKTOPDIR
|
||||
CACHE STRING "desktop file destination"
|
||||
)
|
||||
|
||||
set(MIMEDIR
|
||||
share/mime/packages
|
||||
CACHE STRING "mime file destination"
|
||||
)
|
||||
|
||||
set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
|
||||
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
|
||||
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
|
||||
@@ -503,6 +529,23 @@ if(UNIX)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps)
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR})
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR})
|
||||
|
||||
# Refresh the freedesktop databases so the file associations and scheme
|
||||
# handler register without requiring the user to run them manually. The
|
||||
# tools may be missing on minimal systems; that is fine, packaging systems
|
||||
# usually refresh these databases through their own triggers.
|
||||
find_program(UPDATE_MIME_DATABASE update-mime-database)
|
||||
if(UPDATE_MIME_DATABASE)
|
||||
install(CODE "execute_process(COMMAND \"${UPDATE_MIME_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/mime\")")
|
||||
endif()
|
||||
|
||||
find_program(UPDATE_DESKTOP_DATABASE update-desktop-database)
|
||||
if(UPDATE_DESKTOP_DATABASE)
|
||||
install(
|
||||
CODE "execute_process(COMMAND \"${UPDATE_DESKTOP_DATABASE}\" \"${CMAKE_INSTALL_PREFIX}/share/applications\")"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
elseif(WIN32)
|
||||
install(TARGETS cockatrice RUNTIME DESTINATION ./)
|
||||
|
||||
7
cockatrice/cockatrice-cod.xml
Normal file
7
cockatrice/cockatrice-cod.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/x-cockatrice">
|
||||
<comment>Cockatrice Deck File</comment>
|
||||
<glob pattern="*.cod"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
@@ -3,6 +3,8 @@
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Cockatrice
|
||||
Exec=cockatrice
|
||||
Exec=cockatrice %U
|
||||
Icon=cockatrice
|
||||
Categories=Game;CardGame;
|
||||
MimeType=application/x-cockatrice;
|
||||
X-Scheme-Handler/cockatrice=true
|
||||
|
||||
69
cockatrice/src/client/url_scheme_event_filter.h
Normal file
69
cockatrice/src/client/url_scheme_event_filter.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H
|
||||
#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H
|
||||
|
||||
#include <QEvent>
|
||||
#include <QFileOpenEvent>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
/**
|
||||
* @brief Event filter that catches QFileOpenEvent URLs matching a scheme and
|
||||
* re-emits them as urlReceived().
|
||||
*
|
||||
* On macOS, when the application is registered as a URL scheme handler, the
|
||||
* OS delivers incoming URLs via QFileOpenEvent on the QApplication object.
|
||||
* Install this filter on QApplication to intercept them:
|
||||
*
|
||||
* @code
|
||||
* UrlSchemeEventFilter filter(QStringList{QStringLiteral("cockatrice")});
|
||||
* QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived,
|
||||
* &mainWindow, &MainWindow::handleUrl);
|
||||
* app.installEventFilter(&filter);
|
||||
* @endcode
|
||||
*
|
||||
* Note: the strings are compared against QUrl::scheme(), so they must be
|
||||
* written without the "://" suffix (e.g. "cockatrice", not "cockatrice://").
|
||||
*/
|
||||
class UrlSchemeEventFilter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UrlSchemeEventFilter(const QStringList &schemes, QObject *parent = nullptr)
|
||||
: QObject(parent), prefixes(schemes)
|
||||
{
|
||||
}
|
||||
|
||||
signals:
|
||||
void urlReceived(const QString &url);
|
||||
|
||||
public:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override
|
||||
{
|
||||
if (event->type() == QEvent::FileOpen) {
|
||||
auto *fileEvent = static_cast<QFileOpenEvent *>(event);
|
||||
|
||||
const QUrl url = fileEvent->url();
|
||||
|
||||
for (const auto &prefix : prefixes) {
|
||||
if (url.scheme() == prefix) {
|
||||
emit urlReceived(url.toString());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (url.isLocalFile()) {
|
||||
emit urlReceived(url.toLocalFile());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
private:
|
||||
QStringList prefixes;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
48
cockatrice/src/interface/intents/intent.cpp
Normal file
48
cockatrice/src/interface/intents/intent.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
37
cockatrice/src/interface/intents/intent.h
Normal file
37
cockatrice/src/interface/intents/intent.h
Normal 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
|
||||
@@ -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));
|
||||
}
|
||||
29
cockatrice/src/interface/intents/intent_connect_to_server.h
Normal file
29
cockatrice/src/interface/intents/intent_connect_to_server.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
76
cockatrice/src/interface/intents/intent_join_server_game.cpp
Normal file
76
cockatrice/src/interface/intents/intent_join_server_game.cpp
Normal 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)); });
|
||||
}
|
||||
37
cockatrice/src/interface/intents/intent_join_server_game.h
Normal file
37
cockatrice/src/interface/intents/intent_join_server_game.h
Normal 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
|
||||
74
cockatrice/src/interface/intents/intent_join_server_room.cpp
Normal file
74
cockatrice/src/interface/intents/intent_join_server_room.cpp
Normal 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));
|
||||
}
|
||||
28
cockatrice/src/interface/intents/intent_join_server_room.h
Normal file
28
cockatrice/src/interface/intents/intent_join_server_room.h
Normal 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
|
||||
33
cockatrice/src/interface/intents/intent_login.cpp
Normal file
33
cockatrice/src/interface/intents/intent_login.cpp
Normal 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"));
|
||||
}
|
||||
23
cockatrice/src/interface/intents/intent_login.h
Normal file
23
cockatrice/src/interface/intents/intent_login.h
Normal 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
|
||||
34
cockatrice/src/interface/intents/intent_open_local_deck.cpp
Normal file
34
cockatrice/src/interface/intents/intent_open_local_deck.cpp
Normal 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);
|
||||
}
|
||||
27
cockatrice/src/interface/intents/intent_open_local_deck.h
Normal file
27
cockatrice/src/interface/intents/intent_open_local_deck.h
Normal 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
|
||||
@@ -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(); });
|
||||
}
|
||||
@@ -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
|
||||
89
cockatrice/src/interface/intents/url_parser.cpp
Normal file
89
cockatrice/src/interface/intents/url_parser.cpp
Normal 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();
|
||||
}
|
||||
20
cockatrice/src/interface/intents/url_parser.h
Normal file
20
cockatrice/src/interface/intents/url_parser.h
Normal 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
|
||||
@@ -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(©Link, &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(©Link);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -150,6 +150,11 @@ public:
|
||||
}
|
||||
~MainWindow() override;
|
||||
|
||||
RemoteClient *getRemoteClient() const
|
||||
{
|
||||
return connectionController->client();
|
||||
}
|
||||
|
||||
TabSupervisor *getTabSupervisor() const
|
||||
{
|
||||
return tabSupervisor;
|
||||
|
||||
@@ -23,12 +23,17 @@
|
||||
#include "client/network/update/card_spoiler/spoiler_background_updater.h"
|
||||
#include "client/settings/cache_settings.h"
|
||||
#include "client/sound_engine.h"
|
||||
#include "client/url_scheme_event_filter.h"
|
||||
#include "database/interface/settings_card_preference_provider.h"
|
||||
#include "interface/intents/intent_open_local_deck.h"
|
||||
#include "interface/intents/url_parser.h"
|
||||
#include "interface/logger.h"
|
||||
#include "interface/pixel_map_generator.h"
|
||||
#include "interface/theme_manager.h"
|
||||
#include "interface/widgets/dialogs/dlg_settings.h"
|
||||
#include "interface/widgets/tabs/tab_supervisor.h"
|
||||
#include "interface/window_main.h"
|
||||
#include "single_instance_manager.h"
|
||||
#include "version_string.h"
|
||||
|
||||
#include <QApplication>
|
||||
@@ -37,6 +42,7 @@
|
||||
#include <QDebug>
|
||||
#include <QLibraryInfo>
|
||||
#include <QLocale>
|
||||
#include <QMessageBox>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QTranslator>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
@@ -177,6 +183,7 @@ int main(int argc, char *argv[])
|
||||
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
|
||||
#endif
|
||||
|
||||
// Logging setup
|
||||
#ifdef Q_OS_APPLE
|
||||
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
|
||||
const QByteArray configPath = "../../../qtlogging.ini";
|
||||
@@ -194,15 +201,29 @@ int main(int argc, char *argv[])
|
||||
// Set the QT_LOGGING_CONF environment variable
|
||||
qputenv("QT_LOGGING_CONF", configPath);
|
||||
}
|
||||
|
||||
qSetMessagePattern(
|
||||
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} "
|
||||
"%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{"
|
||||
"endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]");
|
||||
QApplication app(argc, argv);
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice")});
|
||||
|
||||
QStringList pendingMacUrls;
|
||||
|
||||
const auto cocoaBufferConn =
|
||||
QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived,
|
||||
[&pendingMacUrls](const QString &url) { pendingMacUrls.append(url); });
|
||||
|
||||
app.installEventFilter(&cockatriceFilter);
|
||||
#endif
|
||||
|
||||
QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
|
||||
|
||||
qInstallMessageHandler(CockatriceLogger);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
app.addLibraryPath(app.applicationDirPath() + "/plugins");
|
||||
#endif
|
||||
@@ -218,6 +239,7 @@ int main(int argc, char *argv[])
|
||||
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
|
||||
#endif
|
||||
|
||||
// Translations
|
||||
#ifdef Q_OS_MAC
|
||||
translationPath = qApp->applicationDirPath() + "/../Resources/translations";
|
||||
#elif defined(Q_OS_WIN)
|
||||
@@ -226,6 +248,7 @@ int main(int argc, char *argv[])
|
||||
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
|
||||
#endif
|
||||
|
||||
// Command-line parser
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("Cockatrice");
|
||||
parser.addHelpOption();
|
||||
@@ -241,6 +264,35 @@ int main(int argc, char *argv[])
|
||||
Logger::getInstance().logToFile(true);
|
||||
}
|
||||
|
||||
// --- Handle files or URLs passed at startup ---
|
||||
// Only positional arguments are treated as files/URLs, so options like
|
||||
// --connect are never handed off to another instance.
|
||||
const QStringList startupFiles = parser.positionalArguments();
|
||||
const bool hasActivationFiles = !startupFiles.isEmpty();
|
||||
|
||||
SingleInstanceManager instance;
|
||||
|
||||
if (hasActivationFiles) {
|
||||
// Activation launch: hand off to the primary instance if one is
|
||||
// running, otherwise become the primary ourselves. Do this before
|
||||
// constructing the main window so a hand-off exits cheaply.
|
||||
if (!instance.tryRun(startupFiles)) {
|
||||
// Sent successfully → exit
|
||||
return 0;
|
||||
}
|
||||
// No primary instance → become server
|
||||
qInfo() << "No existing instance found, becoming primary instance";
|
||||
} else {
|
||||
// Plain launch: if another instance is running, run independently
|
||||
// instead of handing off and exiting.
|
||||
if (!instance.tryRun(QStringList())) {
|
||||
// Another instance is already running → just run independently
|
||||
qInfo() << "Another instance exists, running independently";
|
||||
} else {
|
||||
qInfo() << "No existing instance found, starting server";
|
||||
}
|
||||
}
|
||||
|
||||
rng = new RNG_SFMT;
|
||||
themeManager = new ThemeManager;
|
||||
soundEngine = new SoundEngine;
|
||||
@@ -272,6 +324,26 @@ int main(int argc, char *argv[])
|
||||
CardDatabaseManager::getInstance()->loadCardDatabases();
|
||||
|
||||
MainWindow ui;
|
||||
|
||||
auto handleActivation = [&ui](const QString &file) {
|
||||
if (file.startsWith("cockatrice://")) {
|
||||
auto urlParser = new IntentUrlParser(&ui, &ui);
|
||||
urlParser->handle(file);
|
||||
} else if (QFileInfo(file).exists()) {
|
||||
auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file);
|
||||
QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) {
|
||||
QMessageBox::warning(&ui, QObject::tr("Open deck"), reason);
|
||||
});
|
||||
openDeckIntent->execute();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
QObject::disconnect(cocoaBufferConn);
|
||||
|
||||
QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived,
|
||||
[&handleActivation](const QString &url) { handleActivation(url); });
|
||||
#endif
|
||||
if (parser.isSet("connect")) {
|
||||
ui.setConnectTo(parser.value("connect"));
|
||||
}
|
||||
@@ -297,7 +369,26 @@ int main(int argc, char *argv[])
|
||||
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
||||
app.setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
#endif
|
||||
app.exec();
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
for (const QString &url : pendingMacUrls) {
|
||||
handleActivation(url);
|
||||
}
|
||||
pendingMacUrls.clear();
|
||||
#endif
|
||||
|
||||
for (const QString &file : startupFiles) {
|
||||
handleActivation(file);
|
||||
}
|
||||
|
||||
// Connect to future file/URL events from other instances
|
||||
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
|
||||
for (const QString &file : files) {
|
||||
handleActivation(file);
|
||||
}
|
||||
});
|
||||
|
||||
int ret = app.exec();
|
||||
|
||||
qCInfo(MainLog) << "Event loop finished, terminating...";
|
||||
delete rng;
|
||||
@@ -305,5 +396,5 @@ int main(int argc, char *argv[])
|
||||
CountryPixmapGenerator::clear();
|
||||
UserLevelPixmapGenerator::clear();
|
||||
|
||||
return 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
126
cockatrice/src/single_instance_manager.cpp
Normal file
126
cockatrice/src/single_instance_manager.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "single_instance_manager.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool SingleInstanceManager::tryRun(const QStringList &filesToSend)
|
||||
{
|
||||
// Scope the socket name to the current user. On Linux the default abstract
|
||||
// namespace is system-wide, so a plain name would let one user's instance
|
||||
// hijack another user's session.
|
||||
QString userName = qEnvironmentVariable("USER");
|
||||
if (userName.isEmpty()) {
|
||||
userName = qEnvironmentVariable("USERNAME");
|
||||
}
|
||||
if (userName.isEmpty()) {
|
||||
userName = QDir::home().dirName();
|
||||
}
|
||||
serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName);
|
||||
|
||||
// Hand off to an already-running primary instance if one exists.
|
||||
if (forwardToPrimary(filesToSend)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No primary instance is currently reachable, so become the primary.
|
||||
server = new QLocalServer(this);
|
||||
connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::handleNewConnection);
|
||||
|
||||
if (server->listen(serverName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Another instance may have started while we were probing; hand off to it
|
||||
// instead of stealing its socket.
|
||||
if (forwardToPrimary(filesToSend)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The socket is stale (left over by a crashed instance): remove it and
|
||||
// retry. If that still fails, another instance just took the name.
|
||||
QLocalServer::removeServer(serverName);
|
||||
if (server->listen(serverName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
forwardToPrimary(filesToSend);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend)
|
||||
{
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(serverName);
|
||||
if (!socket.waitForConnected(200)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Serialize payload with length prefix
|
||||
QByteArray payload;
|
||||
QDataStream out(&payload, QIODevice::WriteOnly);
|
||||
out << filesToSend;
|
||||
|
||||
QByteArray message;
|
||||
QDataStream msgStream(&message, QIODevice::WriteOnly);
|
||||
msgStream << quint32(payload.size());
|
||||
message.append(payload);
|
||||
|
||||
socket.write(message);
|
||||
socket.flush();
|
||||
socket.waitForBytesWritten(1000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SingleInstanceManager::handleNewConnection()
|
||||
{
|
||||
QLocalSocket *socket = server->nextPendingConnection();
|
||||
|
||||
// Per-connection state. QSharedPointer keeps the buffers alive for as long
|
||||
// as the connection handler is attached to the socket.
|
||||
auto buffer = QSharedPointer<QByteArray>::create();
|
||||
auto expectedSize = QSharedPointer<quint32>::create(0);
|
||||
|
||||
connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, expectedSize]() {
|
||||
buffer->append(socket->readAll());
|
||||
|
||||
QDataStream stream(buffer.data(), QIODevice::ReadOnly);
|
||||
|
||||
while (true) {
|
||||
// Step 1: read size
|
||||
if (*expectedSize == 0) {
|
||||
if (buffer->size() < static_cast<int>(sizeof(quint32))) {
|
||||
return;
|
||||
}
|
||||
|
||||
stream >> *expectedSize;
|
||||
}
|
||||
|
||||
// Step 2: wait for full payload
|
||||
if (buffer->size() < static_cast<int>(sizeof(quint32) + *expectedSize)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: extract payload
|
||||
QByteArray payload = buffer->mid(sizeof(quint32), *expectedSize);
|
||||
|
||||
QDataStream payloadStream(&payload, QIODevice::ReadOnly);
|
||||
QStringList files;
|
||||
payloadStream >> files;
|
||||
|
||||
emit filesReceived(files);
|
||||
|
||||
// Reset buffer (single message use-case)
|
||||
buffer->clear();
|
||||
*expectedSize = 0;
|
||||
|
||||
socket->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater);
|
||||
}
|
||||
32
cockatrice/src/single_instance_manager.h
Normal file
32
cockatrice/src/single_instance_manager.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H
|
||||
#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
|
||||
class SingleInstanceManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SingleInstanceManager(QObject *parent = nullptr);
|
||||
|
||||
// Returns true if this process became the primary instance, false if
|
||||
// another instance is already running (and received our files).
|
||||
bool tryRun(const QStringList &initialFiles);
|
||||
|
||||
signals:
|
||||
void filesReceived(const QStringList &files);
|
||||
|
||||
private slots:
|
||||
void handleNewConnection();
|
||||
|
||||
private:
|
||||
bool forwardToPrimary(const QStringList &filesToSend);
|
||||
|
||||
QString serverName;
|
||||
QLocalServer *server = nullptr;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H
|
||||
@@ -122,6 +122,20 @@ public:
|
||||
return userName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the server address configured for the current connection.
|
||||
*
|
||||
* May be empty for clients that have no server counterpart (e.g. local test clients).
|
||||
*/
|
||||
virtual QString serverName() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
virtual quint16 serverPort() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PendingCommand *prepareSessionCommand(const ::google::protobuf::Message &cmd);
|
||||
static PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd, int roomId);
|
||||
static PendingCommand *prepareModeratorCommand(const ::google::protobuf::Message &cmd);
|
||||
|
||||
@@ -131,6 +131,22 @@ public:
|
||||
return socket->peerName();
|
||||
}
|
||||
}
|
||||
quint16 peerPort() const
|
||||
{
|
||||
if (usingWebSocket) {
|
||||
return websocket->peerPort();
|
||||
} else {
|
||||
return socket->peerPort();
|
||||
}
|
||||
}
|
||||
QString serverName() const override
|
||||
{
|
||||
return lastHostname;
|
||||
}
|
||||
quint16 serverPort() const override
|
||||
{
|
||||
return static_cast<quint16>(lastPort);
|
||||
}
|
||||
void
|
||||
connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password);
|
||||
void registerToServer(const QString &hostname,
|
||||
|
||||
@@ -293,3 +293,48 @@ bool ServersSettings::updateExistingServer(QString saveName,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int ServersSettings::findServerIndex(const QString &host, const QString &port) const
|
||||
{
|
||||
int size = getValue("totalServers", "server", "server_details").toInt();
|
||||
|
||||
for (int i = 0; i <= size; ++i) {
|
||||
QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString();
|
||||
QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString();
|
||||
|
||||
if (storedHost == host && storedPort == port) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ServersSettings::hasUsername(const QString &host, const QString &port) const
|
||||
{
|
||||
int index = findServerIndex(host, port);
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString();
|
||||
return !user.isEmpty();
|
||||
}
|
||||
|
||||
bool ServersSettings::hasCredentials(const QString &host, const QString &port) const
|
||||
{
|
||||
int index = findServerIndex(host, port);
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool();
|
||||
QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString();
|
||||
|
||||
return save && !password.isEmpty();
|
||||
}
|
||||
|
||||
bool ServersSettings::hasLoginData(const QString &host, const QString &port) const
|
||||
{
|
||||
return hasUsername(host, port) && hasCredentials(host, port);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ public:
|
||||
QString password,
|
||||
bool savePassword,
|
||||
QString site = QString());
|
||||
int findServerIndex(const QString &host, const QString &port) const;
|
||||
bool hasUsername(const QString &host, const QString &port) const;
|
||||
bool hasCredentials(const QString &host, const QString &port) const;
|
||||
bool hasLoginData(const QString &host, const QString &port) const;
|
||||
|
||||
bool updateExistingServerWithoutLoss(QString saveName,
|
||||
QString serv = QString(),
|
||||
|
||||
Reference in New Issue
Block a user