1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-20 19:57:45 +00:00
This commit is contained in:
Joshua Goins 2024-07-28 11:17:30 -04:00
parent 3b95982dcf
commit 3ec355e79e
14 changed files with 683 additions and 4 deletions

View file

@ -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)

View file

@ -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

View file

@ -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>

View 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;
};

View file

@ -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;
}; };

View file

@ -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;

View 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;
};

View 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"

View file

@ -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()) {

View file

@ -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;

View 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"

View file

@ -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, "", "", "")
} }
} }
} }

View file

@ -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")

View 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
}
}