diff --git a/CMakeLists.txt b/CMakeLists.txt
index 72ad1b3..2e31237 100755
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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)
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index ce7db77..8c7a91b 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -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
diff --git a/launcher/config.kcfg b/launcher/config.kcfg
index 7242e32..7138ba5 100644
--- a/launcher/config.kcfg
+++ b/launcher/config.kcfg
@@ -27,6 +27,11 @@ SPDX-License-Identifier: CC0-1.0
QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QDir::separator() + QStringLiteral("FFXIV")
+
+
+ false
+
+
false
diff --git a/launcher/include/charactersync.h b/launcher/include/charactersync.h
new file mode 100644
index 0000000..b7a2148
--- /dev/null
+++ b/launcher/include/charactersync.h
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2024 Joshua Goins
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+
+#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 sync();
+
+private:
+ QCoro::Task uploadCharacterData(const QDir &dir, const QString &id);
+ QCoro::Task downloadCharacterData(const QDir &dir, const QString &id, const QString &contentUri);
+
+ LauncherCore &launcher;
+ Account &m_account;
+};
diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h
index be2a257..599858c 100755
--- a/launcher/include/launchercore.h
+++ b/launcher/include/launchercore.h
@@ -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;
};
diff --git a/launcher/include/launchersettings.h b/launcher/include/launchersettings.h
index f076586..f545fb6 100644
--- a/launcher/include/launchersettings.h
+++ b/launcher/include/launchersettings.h
@@ -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;
diff --git a/launcher/include/syncmanager.h b/launcher/include/syncmanager.h
new file mode 100644
index 0000000..24096ea
--- /dev/null
+++ b/launcher/include/syncmanager.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2024 Joshua Goins
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+
+#include
+#include
+#include
+
+/**
+ * @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> getUploadedCharacterData(const QString &id);
+
+ /// Uploads character data for @p id from @p path (a file)
+ QCoro::Task uploadedCharacterData(const QString &id, const QString &path);
+
+ /// Downloads character data
+ QCoro::Task 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 findRoom();
+
+ Quotient::AccountRegistry m_accountRegistry;
+
+ Quotient::Room *m_currentRoom = nullptr;
+};
\ No newline at end of file
diff --git a/launcher/src/charactersync.cpp b/launcher/src/charactersync.cpp
new file mode 100644
index 0000000..39ac838
--- /dev/null
+++ b/launcher/src/charactersync.cpp
@@ -0,0 +1,131 @@
+// SPDX-FileCopyrightText: 2024 Joshua Goins
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "charactersync.h"
+
+#include
+#include
+#include
+
+#include "astra_log.h"
+#include "syncmanager.h"
+
+CharacterSync::CharacterSync(Account &account, LauncherCore &launcher, QObject *parent)
+ : launcher(launcher)
+ , m_account(account)
+{
+}
+
+QCoro::Task 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 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 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 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"
\ No newline at end of file
diff --git a/launcher/src/launchercore.cpp b/launcher/src/launchercore.cpp
index 645c1bd..e6d5c22 100755
--- a/launcher/src/launchercore.cpp
+++ b/launcher/src/launchercore.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 auth;
if (!info.profile->isBenchmark()) {
if (info.profile->account()->isSapphire()) {
diff --git a/launcher/src/launchersettings.cpp b/launcher/src/launchersettings.cpp
index 9386198..fd9f5e9 100644
--- a/launcher/src/launchersettings.cpp
+++ b/launcher/src/launchersettings.cpp
@@ -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;
diff --git a/launcher/src/syncmanager.cpp b/launcher/src/syncmanager.cpp
new file mode 100644
index 0000000..30c6e7b
--- /dev/null
+++ b/launcher/src/syncmanager.cpp
@@ -0,0 +1,253 @@
+// SPDX-FileCopyrightText: 2024 Joshua Goins
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "syncmanager.h"
+#include "astra_log.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+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 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()) {
+ 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> 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 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 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"
diff --git a/launcher/ui/Pages/LoginPage.qml b/launcher/ui/Pages/LoginPage.qml
index d406d27..0e3f97e 100644
--- a/launcher/ui/Pages/LoginPage.qml
+++ b/launcher/ui/Pages/LoginPage.qml
@@ -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, "", "", "")
}
}
}
diff --git a/launcher/ui/Settings/SettingsPage.qml b/launcher/ui/Settings/SettingsPage.qml
index 7ca1591..2bccc86 100644
--- a/launcher/ui/Settings/SettingsPage.qml
+++ b/launcher/ui/Settings/SettingsPage.qml
@@ -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")
diff --git a/launcher/ui/Settings/SyncSettings.qml b/launcher/ui/Settings/SyncSettings.qml
new file mode 100644
index 0000000..998b069
--- /dev/null
+++ b/launcher/ui/Settings/SyncSettings.qml
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: 2024 Joshua Goins
+// 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 Matrix.")
+ }
+
+ 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
+ }
+}
\ No newline at end of file