From 3ec355e79e5101611d42e386467d351d369b6f91 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 28 Jul 2024 11:17:30 -0400 Subject: [PATCH] WIP Sync --- CMakeLists.txt | 1 + launcher/CMakeLists.txt | 8 +- launcher/config.kcfg | 5 + launcher/include/charactersync.h | 33 ++++ launcher/include/launchercore.h | 5 +- launcher/include/launchersettings.h | 5 + launcher/include/syncmanager.h | 79 ++++++++ launcher/src/charactersync.cpp | 131 +++++++++++++ launcher/src/launchercore.cpp | 13 ++ launcher/src/launchersettings.cpp | 14 ++ launcher/src/syncmanager.cpp | 253 ++++++++++++++++++++++++++ launcher/ui/Pages/LoginPage.qml | 4 +- launcher/ui/Settings/SettingsPage.qml | 6 + launcher/ui/Settings/SyncSettings.qml | 130 +++++++++++++ 14 files changed, 683 insertions(+), 4 deletions(-) create mode 100644 launcher/include/charactersync.h create mode 100644 launcher/include/syncmanager.h create mode 100644 launcher/src/charactersync.cpp create mode 100644 launcher/src/syncmanager.cpp create mode 100644 launcher/ui/Settings/SyncSettings.qml 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