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(QCoro6 REQUIRED COMPONENTS Core Network Qml)
|
||||
qcoro_enable_coroutines()
|
||||
find_package(QuotientQt6 REQUIRED)
|
||||
|
||||
qt_policy(SET QTP0001 NEW)
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ target_sources(astra PRIVATE
|
|||
include/accountmanager.h
|
||||
include/assetupdater.h
|
||||
include/benchmarkinstaller.h
|
||||
include/charactersync.h
|
||||
include/compatibilitytoolinstaller.h
|
||||
include/encryptedarg.h
|
||||
include/existinginstallmodel.h
|
||||
|
@ -62,12 +63,14 @@ target_sources(astra PRIVATE
|
|||
include/sapphirelogin.h
|
||||
include/squareenixlogin.h
|
||||
include/steamapi.h
|
||||
include/syncmanager.h
|
||||
include/utility.h
|
||||
|
||||
src/account.cpp
|
||||
src/accountmanager.cpp
|
||||
src/assetupdater.cpp
|
||||
src/benchmarkinstaller.cpp
|
||||
src/charactersync.cpp
|
||||
src/compatibilitytoolinstaller.cpp
|
||||
src/encryptedarg.cpp
|
||||
src/existinginstallmodel.cpp
|
||||
|
@ -86,6 +89,7 @@ target_sources(astra PRIVATE
|
|||
src/sapphirelogin.cpp
|
||||
src/squareenixlogin.cpp
|
||||
src/steamapi.cpp
|
||||
src/syncmanager.cpp
|
||||
src/utility.cpp
|
||||
)
|
||||
|
||||
|
@ -108,6 +112,7 @@ qt_target_qml_sources(astra
|
|||
ui/Settings/ProfileSettings.qml
|
||||
ui/Settings/ProfilesPage.qml
|
||||
ui/Settings/SettingsPage.qml
|
||||
ui/Settings/SyncSettings.qml
|
||||
ui/Setup/AccountSetup.qml
|
||||
ui/Setup/AddSapphire.qml
|
||||
ui/Setup/AddSquareEnix.qml
|
||||
|
@ -151,7 +156,8 @@ target_link_libraries(astra PRIVATE
|
|||
KF6::Archive
|
||||
QCoro::Core
|
||||
QCoro::Network
|
||||
QCoro::Qml)
|
||||
QCoro::Qml
|
||||
QuotientQt6)
|
||||
if (BUILD_WEBVIEW)
|
||||
target_link_libraries(astra PRIVATE
|
||||
Qt6::WebView
|
||||
|
|
|
@ -27,6 +27,11 @@ SPDX-License-Identifier: CC0-1.0
|
|||
<default code="true">QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QDir::separator() + QStringLiteral("FFXIV")</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Sync">
|
||||
<entry name="EnableSync" type="bool">
|
||||
<default>false</default>
|
||||
</entry>
|
||||
</group>
|
||||
<group name="Developer">
|
||||
<entry name="KeepPatches" type="bool">
|
||||
<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 GameRunner;
|
||||
class BenchmarkInstaller;
|
||||
class SyncManager;
|
||||
|
||||
class LoginInformation : public QObject
|
||||
{
|
||||
|
@ -67,7 +68,7 @@ class LauncherCore : public QObject
|
|||
Q_PROPERTY(Headline *headline READ headline NOTIFY newsChanged)
|
||||
Q_PROPERTY(Profile *currentProfile READ currentProfile WRITE setCurrentProfile NOTIFY currentProfileChanged)
|
||||
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:
|
||||
LauncherCore();
|
||||
|
@ -123,6 +124,7 @@ public:
|
|||
[[nodiscard]] AccountManager *accountManager();
|
||||
[[nodiscard]] Headline *headline() const;
|
||||
[[nodiscard]] QString cachedLogoImage() const;
|
||||
[[nodiscard]] SyncManager *syncManager() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void loadingFinished();
|
||||
|
@ -164,6 +166,7 @@ private:
|
|||
LauncherSettings *m_settings = nullptr;
|
||||
GameRunner *m_runner = nullptr;
|
||||
QString m_cachedLogoImage;
|
||||
SyncManager *m_syncManager = nullptr;
|
||||
|
||||
int m_currentProfileIndex = 0;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ class LauncherSettings : public QObject
|
|||
Q_PROPERTY(QString screenshotDir READ screenshotDir WRITE setScreenshotDir NOTIFY screenshotDirChanged)
|
||||
Q_PROPERTY(bool argumentsEncrypted READ argumentsEncrypted WRITE setArgumentsEncrypted NOTIFY encryptedArgumentsChanged)
|
||||
Q_PROPERTY(bool enableRenderDocCapture READ enableRenderDocCapture WRITE setEnableRenderDocCapture NOTIFY enableRenderDocCaptureChanged)
|
||||
Q_PROPERTY(bool enableSync READ enableSync WRITE setEnableSync NOTIFY enableSyncChanged)
|
||||
|
||||
public:
|
||||
explicit LauncherSettings(QObject *parent = nullptr);
|
||||
|
@ -74,6 +75,9 @@ public:
|
|||
[[nodiscard]] QString currentProfile() const;
|
||||
void setCurrentProfile(const QString &value);
|
||||
|
||||
[[nodiscard]] bool enableSync() const;
|
||||
void setEnableSync(bool enabled);
|
||||
|
||||
Config *config();
|
||||
|
||||
Q_SIGNALS:
|
||||
|
@ -89,6 +93,7 @@ Q_SIGNALS:
|
|||
void screenshotDirChanged();
|
||||
void encryptedArgumentsChanged();
|
||||
void enableRenderDocCaptureChanged();
|
||||
void enableSyncChanged();
|
||||
|
||||
private:
|
||||
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 "astra_log.h"
|
||||
#include "benchmarkinstaller.h"
|
||||
#include "charactersync.h"
|
||||
#include "compatibilitytoolinstaller.h"
|
||||
#include "gamerunner.h"
|
||||
#include "launchercore.h"
|
||||
#include "sapphirelogin.h"
|
||||
#include "squareenixlogin.h"
|
||||
#include "syncmanager.h"
|
||||
#include "utility.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
@ -34,6 +36,7 @@ LauncherCore::LauncherCore()
|
|||
m_profileManager = new ProfileManager(*this, this);
|
||||
m_accountManager = new AccountManager(*this, this);
|
||||
m_runner = new GameRunner(*this, this);
|
||||
m_syncManager = new SyncManager(this);
|
||||
|
||||
m_profileManager->load();
|
||||
m_accountManager->load();
|
||||
|
@ -354,6 +357,11 @@ QString LauncherCore::cachedLogoImage() const
|
|||
return m_cachedLogoImage;
|
||||
}
|
||||
|
||||
SyncManager *LauncherCore::syncManager() const
|
||||
{
|
||||
return m_syncManager;
|
||||
}
|
||||
|
||||
QCoro::Task<> LauncherCore::beginLogin(LoginInformation &info)
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
|
||||
const auto characterSync = new CharacterSync(*info.profile->account(), *this, this);
|
||||
if (!co_await characterSync->sync()) {
|
||||
co_return;
|
||||
}
|
||||
|
||||
std::optional<LoginAuth> auth;
|
||||
if (!info.profile->isBenchmark()) {
|
||||
if (info.profile->account()->isSapphire()) {
|
||||
|
|
|
@ -214,6 +214,20 @@ void LauncherSettings::setCurrentProfile(const QString &value)
|
|||
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()
|
||||
{
|
||||
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"
|
||||
enabled: page.isLoginValid
|
||||
onClicked: {
|
||||
LauncherCore.login(LauncherCore.currentProfile, usernameField.text, passwordField.text, otpField.text)
|
||||
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")
|
||||
onClicked: {
|
||||
LauncherCore.login(LauncherCore.currentProfile, "", "", "")
|
||||
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"
|
||||
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 {
|
||||
actionName: "compattool"
|
||||
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