mirror of
https://github.com/redstrate/Astra.git
synced 2025-04-20 19:57:45 +00:00
WIP Sync
This commit is contained in:
parent
3b95982dcf
commit
3ec355e79e
14 changed files with 683 additions and 4 deletions
|
@ -51,6 +51,7 @@ find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami I18n Config Core
|
||||||
find_package(KF6KirigamiAddons 1.2.1 REQUIRED)
|
find_package(KF6KirigamiAddons 1.2.1 REQUIRED)
|
||||||
find_package(QCoro6 REQUIRED COMPONENTS Core Network Qml)
|
find_package(QCoro6 REQUIRED COMPONENTS Core Network Qml)
|
||||||
qcoro_enable_coroutines()
|
qcoro_enable_coroutines()
|
||||||
|
find_package(QuotientQt6 REQUIRED)
|
||||||
|
|
||||||
qt_policy(SET QTP0001 NEW)
|
qt_policy(SET QTP0001 NEW)
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ target_sources(astra PRIVATE
|
||||||
include/accountmanager.h
|
include/accountmanager.h
|
||||||
include/assetupdater.h
|
include/assetupdater.h
|
||||||
include/benchmarkinstaller.h
|
include/benchmarkinstaller.h
|
||||||
|
include/charactersync.h
|
||||||
include/compatibilitytoolinstaller.h
|
include/compatibilitytoolinstaller.h
|
||||||
include/encryptedarg.h
|
include/encryptedarg.h
|
||||||
include/existinginstallmodel.h
|
include/existinginstallmodel.h
|
||||||
|
@ -62,12 +63,14 @@ target_sources(astra PRIVATE
|
||||||
include/sapphirelogin.h
|
include/sapphirelogin.h
|
||||||
include/squareenixlogin.h
|
include/squareenixlogin.h
|
||||||
include/steamapi.h
|
include/steamapi.h
|
||||||
|
include/syncmanager.h
|
||||||
include/utility.h
|
include/utility.h
|
||||||
|
|
||||||
src/account.cpp
|
src/account.cpp
|
||||||
src/accountmanager.cpp
|
src/accountmanager.cpp
|
||||||
src/assetupdater.cpp
|
src/assetupdater.cpp
|
||||||
src/benchmarkinstaller.cpp
|
src/benchmarkinstaller.cpp
|
||||||
|
src/charactersync.cpp
|
||||||
src/compatibilitytoolinstaller.cpp
|
src/compatibilitytoolinstaller.cpp
|
||||||
src/encryptedarg.cpp
|
src/encryptedarg.cpp
|
||||||
src/existinginstallmodel.cpp
|
src/existinginstallmodel.cpp
|
||||||
|
@ -86,6 +89,7 @@ target_sources(astra PRIVATE
|
||||||
src/sapphirelogin.cpp
|
src/sapphirelogin.cpp
|
||||||
src/squareenixlogin.cpp
|
src/squareenixlogin.cpp
|
||||||
src/steamapi.cpp
|
src/steamapi.cpp
|
||||||
|
src/syncmanager.cpp
|
||||||
src/utility.cpp
|
src/utility.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,6 +112,7 @@ qt_target_qml_sources(astra
|
||||||
ui/Settings/ProfileSettings.qml
|
ui/Settings/ProfileSettings.qml
|
||||||
ui/Settings/ProfilesPage.qml
|
ui/Settings/ProfilesPage.qml
|
||||||
ui/Settings/SettingsPage.qml
|
ui/Settings/SettingsPage.qml
|
||||||
|
ui/Settings/SyncSettings.qml
|
||||||
ui/Setup/AccountSetup.qml
|
ui/Setup/AccountSetup.qml
|
||||||
ui/Setup/AddSapphire.qml
|
ui/Setup/AddSapphire.qml
|
||||||
ui/Setup/AddSquareEnix.qml
|
ui/Setup/AddSquareEnix.qml
|
||||||
|
@ -151,7 +156,8 @@ target_link_libraries(astra PRIVATE
|
||||||
KF6::Archive
|
KF6::Archive
|
||||||
QCoro::Core
|
QCoro::Core
|
||||||
QCoro::Network
|
QCoro::Network
|
||||||
QCoro::Qml)
|
QCoro::Qml
|
||||||
|
QuotientQt6)
|
||||||
if (BUILD_WEBVIEW)
|
if (BUILD_WEBVIEW)
|
||||||
target_link_libraries(astra PRIVATE
|
target_link_libraries(astra PRIVATE
|
||||||
Qt6::WebView
|
Qt6::WebView
|
||||||
|
|
|
@ -27,6 +27,11 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
<default code="true">QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QDir::separator() + QStringLiteral("FFXIV")</default>
|
<default code="true">QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QDir::separator() + QStringLiteral("FFXIV")</default>
|
||||||
</entry>
|
</entry>
|
||||||
</group>
|
</group>
|
||||||
|
<group name="Sync">
|
||||||
|
<entry name="EnableSync" type="bool">
|
||||||
|
<default>false</default>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
<group name="Developer">
|
<group name="Developer">
|
||||||
<entry name="KeepPatches" type="bool">
|
<entry name="KeepPatches" type="bool">
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
|
|
33
launcher/include/charactersync.h
Normal file
33
launcher/include/charactersync.h
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qcorotask.h>
|
||||||
|
|
||||||
|
#include "launchercore.h"
|
||||||
|
|
||||||
|
class LauncherCore;
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Works in tandem with @c SyncManager to synchronizes character data.
|
||||||
|
*/
|
||||||
|
class CharacterSync : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CharacterSync(Account &account, LauncherCore &launcher, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/// Checks and synchronizes character files as necessary.
|
||||||
|
/// \return False if the synchronization failed.
|
||||||
|
QCoro::Task<bool> sync();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QCoro::Task<void> uploadCharacterData(const QDir &dir, const QString &id);
|
||||||
|
QCoro::Task<void> downloadCharacterData(const QDir &dir, const QString &id, const QString &contentUri);
|
||||||
|
|
||||||
|
LauncherCore &launcher;
|
||||||
|
Account &m_account;
|
||||||
|
};
|
|
@ -21,6 +21,7 @@ class GameInstaller;
|
||||||
class CompatibilityToolInstaller;
|
class CompatibilityToolInstaller;
|
||||||
class GameRunner;
|
class GameRunner;
|
||||||
class BenchmarkInstaller;
|
class BenchmarkInstaller;
|
||||||
|
class SyncManager;
|
||||||
|
|
||||||
class LoginInformation : public QObject
|
class LoginInformation : public QObject
|
||||||
{
|
{
|
||||||
|
@ -67,7 +68,7 @@ class LauncherCore : public QObject
|
||||||
Q_PROPERTY(Headline *headline READ headline NOTIFY newsChanged)
|
Q_PROPERTY(Headline *headline READ headline NOTIFY newsChanged)
|
||||||
Q_PROPERTY(Profile *currentProfile READ currentProfile WRITE setCurrentProfile NOTIFY currentProfileChanged)
|
Q_PROPERTY(Profile *currentProfile READ currentProfile WRITE setCurrentProfile NOTIFY currentProfileChanged)
|
||||||
Q_PROPERTY(Profile *autoLoginProfile READ autoLoginProfile WRITE setAutoLoginProfile NOTIFY autoLoginProfileChanged)
|
Q_PROPERTY(Profile *autoLoginProfile READ autoLoginProfile WRITE setAutoLoginProfile NOTIFY autoLoginProfileChanged)
|
||||||
Q_PROPERTY(QString cachedLogoImage READ cachedLogoImage NOTIFY cachedLogoImageChanged)
|
Q_PROPERTY(SyncManager *syncManager READ syncManager CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
LauncherCore();
|
LauncherCore();
|
||||||
|
@ -123,6 +124,7 @@ public:
|
||||||
[[nodiscard]] AccountManager *accountManager();
|
[[nodiscard]] AccountManager *accountManager();
|
||||||
[[nodiscard]] Headline *headline() const;
|
[[nodiscard]] Headline *headline() const;
|
||||||
[[nodiscard]] QString cachedLogoImage() const;
|
[[nodiscard]] QString cachedLogoImage() const;
|
||||||
|
[[nodiscard]] SyncManager *syncManager() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void loadingFinished();
|
void loadingFinished();
|
||||||
|
@ -164,6 +166,7 @@ private:
|
||||||
LauncherSettings *m_settings = nullptr;
|
LauncherSettings *m_settings = nullptr;
|
||||||
GameRunner *m_runner = nullptr;
|
GameRunner *m_runner = nullptr;
|
||||||
QString m_cachedLogoImage;
|
QString m_cachedLogoImage;
|
||||||
|
SyncManager *m_syncManager = nullptr;
|
||||||
|
|
||||||
int m_currentProfileIndex = 0;
|
int m_currentProfileIndex = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,7 @@ class LauncherSettings : public QObject
|
||||||
Q_PROPERTY(QString screenshotDir READ screenshotDir WRITE setScreenshotDir NOTIFY screenshotDirChanged)
|
Q_PROPERTY(QString screenshotDir READ screenshotDir WRITE setScreenshotDir NOTIFY screenshotDirChanged)
|
||||||
Q_PROPERTY(bool argumentsEncrypted READ argumentsEncrypted WRITE setArgumentsEncrypted NOTIFY encryptedArgumentsChanged)
|
Q_PROPERTY(bool argumentsEncrypted READ argumentsEncrypted WRITE setArgumentsEncrypted NOTIFY encryptedArgumentsChanged)
|
||||||
Q_PROPERTY(bool enableRenderDocCapture READ enableRenderDocCapture WRITE setEnableRenderDocCapture NOTIFY enableRenderDocCaptureChanged)
|
Q_PROPERTY(bool enableRenderDocCapture READ enableRenderDocCapture WRITE setEnableRenderDocCapture NOTIFY enableRenderDocCaptureChanged)
|
||||||
|
Q_PROPERTY(bool enableSync READ enableSync WRITE setEnableSync NOTIFY enableSyncChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LauncherSettings(QObject *parent = nullptr);
|
explicit LauncherSettings(QObject *parent = nullptr);
|
||||||
|
@ -74,6 +75,9 @@ public:
|
||||||
[[nodiscard]] QString currentProfile() const;
|
[[nodiscard]] QString currentProfile() const;
|
||||||
void setCurrentProfile(const QString &value);
|
void setCurrentProfile(const QString &value);
|
||||||
|
|
||||||
|
[[nodiscard]] bool enableSync() const;
|
||||||
|
void setEnableSync(bool enabled);
|
||||||
|
|
||||||
Config *config();
|
Config *config();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
|
@ -89,6 +93,7 @@ Q_SIGNALS:
|
||||||
void screenshotDirChanged();
|
void screenshotDirChanged();
|
||||||
void encryptedArgumentsChanged();
|
void encryptedArgumentsChanged();
|
||||||
void enableRenderDocCaptureChanged();
|
void enableRenderDocCaptureChanged();
|
||||||
|
void enableSyncChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Config *m_config = nullptr;
|
Config *m_config = nullptr;
|
||||||
|
|
79
launcher/include/syncmanager.h
Normal file
79
launcher/include/syncmanager.h
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <Quotient/accountregistry.h>
|
||||||
|
#include <Quotient/connection.h>
|
||||||
|
#include <Task>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles setting up the connection to Matrix and all of the fun things needed to do for that.
|
||||||
|
* Does NOT handle the actual synchronization process, see @c CharacterSync. That handles determining the files to sync and whatnot.
|
||||||
|
*/
|
||||||
|
class SyncManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
||||||
|
Q_PROPERTY(QString userId READ userId NOTIFY userIdChanged)
|
||||||
|
Q_PROPERTY(Quotient::Connection *connection READ connection NOTIFY connectionChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SyncManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in to a connection
|
||||||
|
* @param matrixId user id in the form @user:server.tld
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void login(const QString &matrixId, const QString &password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log out of the connection
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void logout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single sync. We're not syncing constantly, since we typically don't need it and it consumes a lot of data
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void sync();
|
||||||
|
|
||||||
|
bool connected() const;
|
||||||
|
QString userId() const;
|
||||||
|
Quotient::Connection *connection() const;
|
||||||
|
|
||||||
|
/// If we're ready to begin downloading or uploading data
|
||||||
|
bool isReady() const;
|
||||||
|
|
||||||
|
struct PreviousCharacterData {
|
||||||
|
QString mxcUri;
|
||||||
|
QString hostname;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns a content repo URI, or nullopt if there's existing character data or not respectively
|
||||||
|
QCoro::Task<std::optional<PreviousCharacterData>> getUploadedCharacterData(const QString &id);
|
||||||
|
|
||||||
|
/// Uploads character data for @p id from @p path (a file)
|
||||||
|
QCoro::Task<bool> uploadedCharacterData(const QString &id, const QString &path);
|
||||||
|
|
||||||
|
/// Downloads character data
|
||||||
|
QCoro::Task<bool> downloadCharacterData(const QString &mxcUri, const QString &destPath);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void connectedChanged();
|
||||||
|
void userIdChanged();
|
||||||
|
void connectionChanged();
|
||||||
|
void isReadyChanged();
|
||||||
|
void loginError(const QString &message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString roomId() const;
|
||||||
|
void setRoomId(const QString &roomId);
|
||||||
|
QCoro::Task<void> findRoom();
|
||||||
|
|
||||||
|
Quotient::AccountRegistry m_accountRegistry;
|
||||||
|
|
||||||
|
Quotient::Room *m_currentRoom = nullptr;
|
||||||
|
};
|
131
launcher/src/charactersync.cpp
Normal file
131
launcher/src/charactersync.cpp
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "charactersync.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KZip>
|
||||||
|
#include <QCoro>
|
||||||
|
|
||||||
|
#include "astra_log.h"
|
||||||
|
#include "syncmanager.h"
|
||||||
|
|
||||||
|
CharacterSync::CharacterSync(Account &account, LauncherCore &launcher, QObject *parent)
|
||||||
|
: launcher(launcher)
|
||||||
|
, m_account(account)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<bool> CharacterSync::sync()
|
||||||
|
{
|
||||||
|
if (!launcher.settings()->enableSync()) {
|
||||||
|
co_return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
qInfo() << "A";
|
||||||
|
|
||||||
|
auto syncManager = launcher.syncManager();
|
||||||
|
if (!syncManager->connected()) {
|
||||||
|
qInfo() << "B";
|
||||||
|
// TODO: provide an option to continue in the UI
|
||||||
|
Q_EMIT launcher.loginError(i18n("Failed to connect to sync server! Please check your sync settings."));
|
||||||
|
co_return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qInfo() << "C";
|
||||||
|
|
||||||
|
if (!syncManager->isReady()) {
|
||||||
|
Q_EMIT launcher.stageChanged(i18n("Waiting for sync connection..."));
|
||||||
|
|
||||||
|
// NOTE: probably does not handle errors well?
|
||||||
|
co_await qCoro(syncManager, &SyncManager::isReadyChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT launcher.stageChanged(i18n("Synchronizing character data..."));
|
||||||
|
|
||||||
|
// so first, we need to list the character folders
|
||||||
|
// we sync each one separately
|
||||||
|
QList<QFileInfo> characterDirs;
|
||||||
|
|
||||||
|
const QDir configPath = m_account.getConfigPath();
|
||||||
|
qCDebug(ASTRA_LOG) << "Searching for characters in" << configPath;
|
||||||
|
|
||||||
|
QDirIterator configIterator(configPath.absolutePath());
|
||||||
|
while (configIterator.hasNext()) {
|
||||||
|
const auto fileInfo = configIterator.nextFileInfo();
|
||||||
|
if (fileInfo.isDir() && fileInfo.fileName().startsWith(QStringLiteral("FFXIV_"))) {
|
||||||
|
characterDirs.append(fileInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(ASTRA_LOG) << "Character directories:" << characterDirs;
|
||||||
|
|
||||||
|
for (const auto &dir : characterDirs) {
|
||||||
|
const QString id = dir.fileName(); // FFXIV_CHR0040000001000001 for example
|
||||||
|
const auto previousData = co_await syncManager->getUploadedCharacterData(id);
|
||||||
|
if (!previousData.has_value()) {
|
||||||
|
// if we didn't upload character data yet, upload it now
|
||||||
|
co_await uploadCharacterData(dir.absoluteFilePath(), id);
|
||||||
|
} else {
|
||||||
|
// otherwise, download it
|
||||||
|
|
||||||
|
// but check first if it's our hostname
|
||||||
|
if (QSysInfo::machineHostName() == previousData->hostname) {
|
||||||
|
qCDebug(ASTRA_LOG) << "Skipping! We uploaded this data.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
co_await downloadCharacterData(dir.absoluteFilePath(), id, previousData->mxcUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> CharacterSync::uploadCharacterData(const QDir &dir, const QString &id)
|
||||||
|
{
|
||||||
|
qCDebug(ASTRA_LOG) << "Uploading" << dir << id;
|
||||||
|
QTemporaryDir tempDir;
|
||||||
|
|
||||||
|
auto tempZipPath = tempDir.filePath(QStringLiteral("%1.zip").arg(id));
|
||||||
|
|
||||||
|
KZip *zip = new KZip(tempZipPath);
|
||||||
|
zip->setCompression(KZip::DeflateCompression);
|
||||||
|
zip->open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
QFile gearsetFile(dir.filePath(QStringLiteral("GEARSET.DAT")));
|
||||||
|
gearsetFile.open(QFile::ReadOnly);
|
||||||
|
|
||||||
|
zip->writeFile(QStringLiteral("GEARSET.DAT"), gearsetFile.readAll());
|
||||||
|
zip->close();
|
||||||
|
|
||||||
|
co_await launcher.syncManager()->uploadedCharacterData(id, tempZipPath);
|
||||||
|
// TODO: error handling
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> CharacterSync::downloadCharacterData(const QDir &dir, const QString &id, const QString &contentUri)
|
||||||
|
{
|
||||||
|
QTemporaryDir tempDir;
|
||||||
|
|
||||||
|
auto tempZipPath = tempDir.filePath(QStringLiteral("%1.zip").arg(id));
|
||||||
|
|
||||||
|
co_await launcher.syncManager()->downloadCharacterData(contentUri, tempZipPath);
|
||||||
|
|
||||||
|
KZip *zip = new KZip(tempZipPath);
|
||||||
|
zip->setCompression(KZip::DeflateCompression);
|
||||||
|
zip->open(QIODevice::ReadOnly);
|
||||||
|
|
||||||
|
qCDebug(ASTRA_LOG) << "contents:" << zip->directory()->entries();
|
||||||
|
|
||||||
|
zip->directory()->file(QStringLiteral("GEARSET.DAT"))->copyTo(dir.absolutePath());
|
||||||
|
|
||||||
|
qCDebug(ASTRA_LOG) << "Extracted character data!";
|
||||||
|
|
||||||
|
zip->close();
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_charactersync.cpp"
|
|
@ -15,11 +15,13 @@
|
||||||
#include "assetupdater.h"
|
#include "assetupdater.h"
|
||||||
#include "astra_log.h"
|
#include "astra_log.h"
|
||||||
#include "benchmarkinstaller.h"
|
#include "benchmarkinstaller.h"
|
||||||
|
#include "charactersync.h"
|
||||||
#include "compatibilitytoolinstaller.h"
|
#include "compatibilitytoolinstaller.h"
|
||||||
#include "gamerunner.h"
|
#include "gamerunner.h"
|
||||||
#include "launchercore.h"
|
#include "launchercore.h"
|
||||||
#include "sapphirelogin.h"
|
#include "sapphirelogin.h"
|
||||||
#include "squareenixlogin.h"
|
#include "squareenixlogin.h"
|
||||||
|
#include "syncmanager.h"
|
||||||
#include "utility.h"
|
#include "utility.h"
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
|
@ -34,6 +36,7 @@ LauncherCore::LauncherCore()
|
||||||
m_profileManager = new ProfileManager(*this, this);
|
m_profileManager = new ProfileManager(*this, this);
|
||||||
m_accountManager = new AccountManager(*this, this);
|
m_accountManager = new AccountManager(*this, this);
|
||||||
m_runner = new GameRunner(*this, this);
|
m_runner = new GameRunner(*this, this);
|
||||||
|
m_syncManager = new SyncManager(this);
|
||||||
|
|
||||||
m_profileManager->load();
|
m_profileManager->load();
|
||||||
m_accountManager->load();
|
m_accountManager->load();
|
||||||
|
@ -354,6 +357,11 @@ QString LauncherCore::cachedLogoImage() const
|
||||||
return m_cachedLogoImage;
|
return m_cachedLogoImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SyncManager *LauncherCore::syncManager() const
|
||||||
|
{
|
||||||
|
return m_syncManager;
|
||||||
|
}
|
||||||
|
|
||||||
QCoro::Task<> LauncherCore::beginLogin(LoginInformation &info)
|
QCoro::Task<> LauncherCore::beginLogin(LoginInformation &info)
|
||||||
{
|
{
|
||||||
// Hmm, I don't think we're set up for this yet?
|
// Hmm, I don't think we're set up for this yet?
|
||||||
|
@ -361,6 +369,11 @@ QCoro::Task<> LauncherCore::beginLogin(LoginInformation &info)
|
||||||
info.profile->account()->updateConfig();
|
info.profile->account()->updateConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto characterSync = new CharacterSync(*info.profile->account(), *this, this);
|
||||||
|
if (!co_await characterSync->sync()) {
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<LoginAuth> auth;
|
std::optional<LoginAuth> auth;
|
||||||
if (!info.profile->isBenchmark()) {
|
if (!info.profile->isBenchmark()) {
|
||||||
if (info.profile->account()->isSapphire()) {
|
if (info.profile->account()->isSapphire()) {
|
||||||
|
|
|
@ -214,6 +214,20 @@ void LauncherSettings::setCurrentProfile(const QString &value)
|
||||||
stateConfig->sync();
|
stateConfig->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool LauncherSettings::enableSync() const
|
||||||
|
{
|
||||||
|
return m_config->enableSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LauncherSettings::setEnableSync(const bool enabled)
|
||||||
|
{
|
||||||
|
if (m_config->enableSync() != enabled) {
|
||||||
|
m_config->setEnableSync(enabled);
|
||||||
|
m_config->save();
|
||||||
|
Q_EMIT enableSyncChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Config *LauncherSettings::config()
|
Config *LauncherSettings::config()
|
||||||
{
|
{
|
||||||
return m_config;
|
return m_config;
|
||||||
|
|
253
launcher/src/syncmanager.cpp
Normal file
253
launcher/src/syncmanager.cpp
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "syncmanager.h"
|
||||||
|
#include "astra_log.h"
|
||||||
|
|
||||||
|
#include <Quotient/accountregistry.h>
|
||||||
|
#include <Quotient/csapi/content-repo.h>
|
||||||
|
#include <Quotient/csapi/room_state.h>
|
||||||
|
#include <Quotient/events/stateevent.h>
|
||||||
|
#include <Quotient/jobs/downloadfilejob.h>
|
||||||
|
#include <Quotient/room.h>
|
||||||
|
#include <Quotient/settings.h>
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCoro>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
|
||||||
|
const QString roomType = QStringLiteral("zone.xiv.astra-sync");
|
||||||
|
const QString syncEventType = QStringLiteral("zone.xiv.astra.sync");
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
SyncManager::SyncManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
m_accountRegistry.invokeLogin(); // TODO: port from invokeLogin
|
||||||
|
connect(&m_accountRegistry, &AccountRegistry::rowsInserted, this, [this]() {
|
||||||
|
connection()->setCacheState(false);
|
||||||
|
connection()->setLazyLoading(false);
|
||||||
|
connection()->setDirectChatEncryptionDefault(false);
|
||||||
|
connection()->setEncryptionDefault(false);
|
||||||
|
|
||||||
|
Q_EMIT connectedChanged();
|
||||||
|
Q_EMIT userIdChanged();
|
||||||
|
Q_EMIT connectionChanged();
|
||||||
|
sync();
|
||||||
|
});
|
||||||
|
connect(&m_accountRegistry, &AccountRegistry::rowsRemoved, this, [this]() {
|
||||||
|
Q_EMIT connectedChanged();
|
||||||
|
Q_EMIT userIdChanged();
|
||||||
|
Q_EMIT connectionChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncManager::login(const QString &matrixId, const QString &password)
|
||||||
|
{
|
||||||
|
auto connection = new Connection(this);
|
||||||
|
connection->resolveServer(matrixId);
|
||||||
|
connect(
|
||||||
|
connection,
|
||||||
|
&Connection::loginFlowsChanged,
|
||||||
|
this,
|
||||||
|
[connection, matrixId, password]() {
|
||||||
|
connection->loginWithPassword(matrixId, password, qAppName(), {});
|
||||||
|
},
|
||||||
|
Qt::SingleShotConnection);
|
||||||
|
|
||||||
|
connect(connection, &Connection::connected, this, [this, connection] {
|
||||||
|
qCDebug(ASTRA_LOG) << "Connected!";
|
||||||
|
|
||||||
|
// TODO: store somewhere else, not their QSettings
|
||||||
|
AccountSettings account(connection->userId());
|
||||||
|
account.setKeepLoggedIn(true);
|
||||||
|
account.setHomeserver(connection->homeserver());
|
||||||
|
account.setDeviceId(connection->deviceId());
|
||||||
|
account.setDeviceName(qAppName());
|
||||||
|
account.sync();
|
||||||
|
|
||||||
|
m_accountRegistry.add(connection);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(connection, &Connection::loginError, this, &SyncManager::loginError);
|
||||||
|
connect(connection, &Connection::resolveError, this, &SyncManager::loginError);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SyncManager::connected() const
|
||||||
|
{
|
||||||
|
return !m_accountRegistry.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SyncManager::userId() const
|
||||||
|
{
|
||||||
|
return !m_accountRegistry.empty() ? m_accountRegistry.accounts().first()->userId() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncManager::logout()
|
||||||
|
{
|
||||||
|
m_accountRegistry.accounts().first()->logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
Quotient::Connection *SyncManager::connection() const
|
||||||
|
{
|
||||||
|
if (!m_accountRegistry.empty()) {
|
||||||
|
return m_accountRegistry.accounts().first();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncManager::sync()
|
||||||
|
{
|
||||||
|
auto connection = m_accountRegistry.accounts().first();
|
||||||
|
connection->sync();
|
||||||
|
connect(
|
||||||
|
connection,
|
||||||
|
&Connection::syncDone,
|
||||||
|
this,
|
||||||
|
[this]() {
|
||||||
|
m_accountRegistry.accounts().first()->stopSync();
|
||||||
|
|
||||||
|
qCDebug(ASTRA_LOG) << "Done with sync.";
|
||||||
|
|
||||||
|
// Find the room we need to sync with
|
||||||
|
findRoom();
|
||||||
|
},
|
||||||
|
Qt::SingleShotConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<void> SyncManager::findRoom()
|
||||||
|
{
|
||||||
|
qCDebug(ASTRA_LOG) << "Time to find the sync room!";
|
||||||
|
|
||||||
|
const QString roomId = this->roomId();
|
||||||
|
|
||||||
|
qCDebug(ASTRA_LOG) << "Stored room id:" << roomId;
|
||||||
|
|
||||||
|
// If we have no room id set, we need to find the correct room type
|
||||||
|
const bool needsFirstTimeRoom = roomId.isEmpty();
|
||||||
|
|
||||||
|
// Try to find our room
|
||||||
|
auto rooms = m_accountRegistry.accounts().first()->rooms(Quotient::JoinState::Join);
|
||||||
|
for (auto room : rooms) {
|
||||||
|
if (!room->currentState().contains<RoomCreateEvent>()) {
|
||||||
|
co_await qCoro(room, &Room::baseStateLoaded);
|
||||||
|
qCDebug(ASTRA_LOG) << "Loaded base state...";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsFirstTimeRoom) {
|
||||||
|
const QJsonObject createEvent = room->currentState().eventsOfType(QStringLiteral("m.room.create")).first()->fullJson();
|
||||||
|
auto contentJson = createEvent[QStringLiteral("content")].toObject();
|
||||||
|
if (contentJson.contains(QStringLiteral("type"))) {
|
||||||
|
if (contentJson[QStringLiteral("type")] == roomType) {
|
||||||
|
qCDebug(ASTRA_LOG) << room << "matches!";
|
||||||
|
m_currentRoom = room;
|
||||||
|
setRoomId(room->id());
|
||||||
|
Q_EMIT isReadyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (room->id() == roomId) {
|
||||||
|
qCDebug(ASTRA_LOG) << "Found pre-existing room!";
|
||||||
|
|
||||||
|
m_currentRoom = room;
|
||||||
|
Q_EMIT isReadyChanged();
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We failed to find a room, and we need to create one
|
||||||
|
if (needsFirstTimeRoom && !m_currentRoom) {
|
||||||
|
qCDebug(ASTRA_LOG) << "Need to create room!";
|
||||||
|
|
||||||
|
auto job = connection()->createRoom(Quotient::Connection::RoomVisibility::UnpublishRoom,
|
||||||
|
QString{},
|
||||||
|
i18n("Astra Sync"),
|
||||||
|
i18n("Room used to sync Astra between devices"),
|
||||||
|
QStringList{},
|
||||||
|
QString{},
|
||||||
|
QString::number(10),
|
||||||
|
false,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
QJsonObject{{QStringLiteral("type"), roomType}});
|
||||||
|
co_await qCoro(job, &BaseJob::finished);
|
||||||
|
|
||||||
|
setRoomId(job->roomId());
|
||||||
|
qCDebug(ASTRA_LOG) << "Created sync room at" << job->roomId();
|
||||||
|
|
||||||
|
// re-run sync to get the new room
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
co_return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SyncManager::roomId() const
|
||||||
|
{
|
||||||
|
return KSharedConfig::openStateConfig()->group(QStringLiteral("Sync")).readEntry(QStringLiteral("RoomId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncManager::setRoomId(const QString &roomId)
|
||||||
|
{
|
||||||
|
auto stateConfig = KSharedConfig::openStateConfig();
|
||||||
|
stateConfig->group(QStringLiteral("Sync")).writeEntry(QStringLiteral("RoomId"), roomId);
|
||||||
|
stateConfig->sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SyncManager::isReady() const
|
||||||
|
{
|
||||||
|
return connected() && m_currentRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<std::optional<SyncManager::PreviousCharacterData>> SyncManager::getUploadedCharacterData(const QString &id)
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_currentRoom);
|
||||||
|
|
||||||
|
const auto syncEvent = m_currentRoom->currentState().contentJson(syncEventType, id);
|
||||||
|
if (syncEvent.isEmpty()) {
|
||||||
|
qCDebug(ASTRA_LOG) << "No previous sync for" << id;
|
||||||
|
co_return std::nullopt;
|
||||||
|
} else {
|
||||||
|
qCDebug(ASTRA_LOG) << "previous sync event:" << syncEvent;
|
||||||
|
co_return PreviousCharacterData{.mxcUri = syncEvent[QStringLiteral("content-uri")].toString(),
|
||||||
|
.hostname = syncEvent[QStringLiteral("hostname")].toString()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<bool> SyncManager::uploadedCharacterData(const QString &id, const QString &path)
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_currentRoom);
|
||||||
|
|
||||||
|
auto uploadFileJob = connection()->uploadFile(path);
|
||||||
|
co_await qCoro(uploadFileJob, &BaseJob::finished);
|
||||||
|
|
||||||
|
// TODO: error handling
|
||||||
|
|
||||||
|
const QUrl contentUri = uploadFileJob->contentUri();
|
||||||
|
|
||||||
|
auto syncSetState = m_currentRoom->setState(
|
||||||
|
syncEventType,
|
||||||
|
id,
|
||||||
|
QJsonObject{{{QStringLiteral("content-uri"), contentUri.toString()}, {QStringLiteral("hostname"), QSysInfo::machineHostName()}}});
|
||||||
|
co_await qCoro(syncSetState, &BaseJob::finished);
|
||||||
|
|
||||||
|
co_return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCoro::Task<bool> SyncManager::downloadCharacterData(const QString &mxcUri, const QString &destPath)
|
||||||
|
{
|
||||||
|
auto job = connection()->downloadFile(QUrl::fromUserInput(mxcUri), destPath);
|
||||||
|
co_await qCoro(job, &BaseJob::finished);
|
||||||
|
|
||||||
|
// TODO: error handling
|
||||||
|
|
||||||
|
co_return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_syncmanager.cpp"
|
|
@ -274,8 +274,8 @@ QQC2.Control {
|
||||||
icon.name: "unlock"
|
icon.name: "unlock"
|
||||||
enabled: page.isLoginValid
|
enabled: page.isLoginValid
|
||||||
onClicked: {
|
onClicked: {
|
||||||
LauncherCore.login(LauncherCore.currentProfile, usernameField.text, passwordField.text, otpField.text)
|
|
||||||
page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "StatusPage"))
|
page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "StatusPage"))
|
||||||
|
LauncherCore.login(LauncherCore.currentProfile, usernameField.text, passwordField.text, otpField.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,8 +308,8 @@ QQC2.Control {
|
||||||
|
|
||||||
text: i18n("Launch Benchmark")
|
text: i18n("Launch Benchmark")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
LauncherCore.login(LauncherCore.currentProfile, "", "", "")
|
|
||||||
page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "StatusPage"))
|
page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "StatusPage"))
|
||||||
|
LauncherCore.login(LauncherCore.currentProfile, "", "", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,12 @@ KirigamiSettings.CategorizedSettings {
|
||||||
icon.name: "preferences-system-users"
|
icon.name: "preferences-system-users"
|
||||||
page: Qt.resolvedUrl("/qt/qml/zone/xiv/astra/ui/Settings/AccountsPage.qml")
|
page: Qt.resolvedUrl("/qt/qml/zone/xiv/astra/ui/Settings/AccountsPage.qml")
|
||||||
},
|
},
|
||||||
|
KirigamiSettings.SettingAction {
|
||||||
|
actionName: "sync"
|
||||||
|
text: i18n("Sync")
|
||||||
|
icon.name: "state-sync-symbolic"
|
||||||
|
page: Qt.resolvedUrl("/qt/qml/zone/xiv/astra/ui/Settings/SyncSettings.qml")
|
||||||
|
},
|
||||||
KirigamiSettings.SettingAction {
|
KirigamiSettings.SettingAction {
|
||||||
actionName: "compattool"
|
actionName: "compattool"
|
||||||
text: i18n("Compatibility Tool")
|
text: i18n("Compatibility Tool")
|
||||||
|
|
130
launcher/ui/Settings/SyncSettings.qml
Normal file
130
launcher/ui/Settings/SyncSettings.qml
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
|
||||||
|
import zone.xiv.astra
|
||||||
|
|
||||||
|
import "../Components"
|
||||||
|
|
||||||
|
FormCard.FormCardPage {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
title: i18nc("@title:window", "Sync")
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
id: infoCard
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
id: infoDelegate
|
||||||
|
|
||||||
|
text: i18n("Sync character data between devices using <a href='https://matrix.org'>Matrix</a>.")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {
|
||||||
|
above: infoDelegate
|
||||||
|
below: enableSyncDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormCheckDelegate {
|
||||||
|
id: enableSyncDelegate
|
||||||
|
|
||||||
|
text: i18n("Enable Sync")
|
||||||
|
description: i18n("Syncing will occur before login, and after the game exits.")
|
||||||
|
checked: LauncherCore.settings.enableSync
|
||||||
|
onCheckedChanged: LauncherCore.settings.enableSync = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
id: loginCard
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
visible: !LauncherCore.syncManager.connected
|
||||||
|
enabled: LauncherCore.settings.enableSync
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: usernameDelegate
|
||||||
|
|
||||||
|
label: i18n("Username:")
|
||||||
|
placeholderText: "@username:domain.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {
|
||||||
|
above: usernameDelegate
|
||||||
|
below: passwordDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: passwordDelegate
|
||||||
|
|
||||||
|
label: i18n("Password:")
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {
|
||||||
|
above: passwordDelegate
|
||||||
|
below: loginButton
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
id: loginButton
|
||||||
|
|
||||||
|
text: i18n("Login")
|
||||||
|
|
||||||
|
onClicked: LauncherCore.syncManager.login(usernameDelegate.text, passwordDelegate.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
id: logoutCard
|
||||||
|
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
visible: LauncherCore.syncManager.connected
|
||||||
|
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
id: usernameLabelDelegate
|
||||||
|
|
||||||
|
text: i18n("Logged in as %1", LauncherCore.syncManager.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {
|
||||||
|
above: usernameLabelDelegate
|
||||||
|
below: logoutDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
id: logoutDelegate
|
||||||
|
|
||||||
|
text: i18n("Log Out")
|
||||||
|
onClicked: LauncherCore.syncManager.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: LauncherCore.syncManager
|
||||||
|
|
||||||
|
function onLoginError(message: string): void {
|
||||||
|
errorDialog.subtitle = message;
|
||||||
|
errorDialog.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PromptDialog {
|
||||||
|
id: errorDialog
|
||||||
|
title: i18n("Login Error")
|
||||||
|
|
||||||
|
showCloseButton: false
|
||||||
|
standardButtons: Kirigami.Dialog.Ok
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue