diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index d3e8573..9351f31 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -105,7 +105,6 @@ qt_target_qml_sources(astra ui/Setup/AccountSetup.qml ui/Setup/AddSapphire.qml ui/Setup/AddSquareEnix.qml - ui/Setup/DownloadSetup.qml ui/Setup/ExistingSetup.qml ui/Setup/InstallProgress.qml ui/Setup/SetupPage.qml diff --git a/launcher/include/gameinstaller.h b/launcher/include/gameinstaller.h index c02376a..c240e3d 100644 --- a/launcher/include/gameinstaller.h +++ b/launcher/include/gameinstaller.h @@ -18,14 +18,18 @@ class GameInstaller : public QObject public: GameInstaller(LauncherCore &launcher, Profile &profile, QObject *parent = nullptr); + GameInstaller(LauncherCore &launcher, Profile &profile, const QString &filePath, QObject *parent = nullptr); - Q_INVOKABLE void installGame(); + Q_INVOKABLE void start(); Q_SIGNALS: void installFinished(); void error(QString message); private: + void installGame(); + LauncherCore &m_launcher; Profile &m_profile; + QString m_localInstallerPath; }; \ No newline at end of file diff --git a/launcher/include/launchercore.h b/launcher/include/launchercore.h index bd27329..374fe32 100755 --- a/launcher/include/launchercore.h +++ b/launcher/include/launchercore.h @@ -86,6 +86,7 @@ public: Q_INVOKABLE bool autoLogin(Profile *profile); Q_INVOKABLE GameInstaller *createInstaller(Profile *profile); + Q_INVOKABLE GameInstaller *createInstallerFromExisting(Profile *profile, const QString &filePath); Q_INVOKABLE CompatibilityToolInstaller *createCompatInstaller(); Q_INVOKABLE void clearAvatarCache(); @@ -121,6 +122,7 @@ Q_SIGNALS: void gameClosed(); void loginError(QString message); void dalamudError(QString message); + void miscError(QString message); void stageChanged(QString message, QString explanation = {}); void stageIndeterminate(); void stageDeterminate(int min, int max, int value); diff --git a/launcher/src/gameinstaller.cpp b/launcher/src/gameinstaller.cpp index b05726b..75334e7 100644 --- a/launcher/src/gameinstaller.cpp +++ b/launcher/src/gameinstaller.cpp @@ -24,45 +24,63 @@ GameInstaller::GameInstaller(LauncherCore &launcher, Profile &profile, QObject * { } +GameInstaller::GameInstaller(LauncherCore &launcher, Profile &profile, const QString &filePath, QObject *parent) + : GameInstaller(launcher, profile, parent) +{ + m_localInstallerPath = filePath; +} + +void GameInstaller::start() +{ + if (m_localInstallerPath.isEmpty()) { + const QNetworkRequest request = QNetworkRequest(QUrl(installerUrl)); + Utility::printRequest(QStringLiteral("GET"), request); + + auto reply = m_launcher.mgr()->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply] { + if (reply->error() != QNetworkReply::NetworkError::NoError) { + Q_EMIT error(reply->errorString()); + return; + } + + const QDir dataDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + + const QByteArray data = reply->readAll(); + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(data); + + if (hash.result() != installerSha256) { + Q_EMIT error(i18n("The installer failed the integrity check!")); + return; + } + + QFile file(dataDir.absoluteFilePath(QStringLiteral("ffxivsetup.exe"))); + file.open(QIODevice::WriteOnly); + file.write(data); + file.close(); + + m_localInstallerPath = file.fileName(); + installGame(); + }); + } else { + installGame(); + } +} + void GameInstaller::installGame() { const QDir installDirectory = m_profile.gamePath(); - const QNetworkRequest request = QNetworkRequest(QUrl(installerUrl)); - Utility::printRequest(QStringLiteral("GET"), request); + const std::string installDirectoryStd = installDirectory.absolutePath().toStdString(); + const std::string fileNameStd = m_localInstallerPath.toStdString(); - auto reply = m_launcher.mgr()->get(request); + physis_install_game(fileNameStd.c_str(), installDirectoryStd.c_str()); - QObject::connect(reply, &QNetworkReply::finished, [this, reply, installDirectory] { - if (reply->error() != QNetworkReply::NetworkError::NoError) { - Q_EMIT error(i18n("An error has occurred when downloading the installer.\n\n%1", reply->errorString())); - return; - } + m_profile.readGameVersion(); - const QDir dataDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); - - const QByteArray data = reply->readAll(); - QCryptographicHash hash(QCryptographicHash::Sha256); - hash.addData(data); - - // TODO: turn into a proper error - Q_ASSERT(hash.result() == installerSha256); - - QFile file(dataDir.absoluteFilePath(QStringLiteral("ffxivsetup.exe"))); - file.open(QIODevice::WriteOnly); - file.write(data); - file.close(); - - const std::string installDirectoryStd = installDirectory.absolutePath().toStdString(); - const std::string fileNameStd = file.fileName().toStdString(); - - physis_install_game(fileNameStd.c_str(), installDirectoryStd.c_str()); - - m_profile.readGameVersion(); - - Q_EMIT installFinished(); - qInfo(ASTRA_LOG) << "Installed game in" << installDirectory; - }); + Q_EMIT installFinished(); + qInfo(ASTRA_LOG) << "Installed game in" << installDirectory; } #include "moc_gameinstaller.cpp" \ No newline at end of file diff --git a/launcher/src/launchercore.cpp b/launcher/src/launchercore.cpp index 4996ceb..cd7828c 100755 --- a/launcher/src/launchercore.cpp +++ b/launcher/src/launchercore.cpp @@ -105,6 +105,13 @@ GameInstaller *LauncherCore::createInstaller(Profile *profile) return new GameInstaller(*this, *profile, this); } +GameInstaller *LauncherCore::createInstallerFromExisting(Profile *profile, const QString &filePath) +{ + Q_ASSERT(profile != nullptr); + + return new GameInstaller(*this, *profile, filePath, this); +} + CompatibilityToolInstaller *LauncherCore::createCompatInstaller() { return new CompatibilityToolInstaller(*this, this); diff --git a/launcher/ui/Setup/AccountSetup.qml b/launcher/ui/Setup/AccountSetup.qml index d0fe051..3392a95 100644 --- a/launcher/ui/Setup/AccountSetup.qml +++ b/launcher/ui/Setup/AccountSetup.qml @@ -25,7 +25,7 @@ FormCard.FormCardPage { FormCard.FormTextDelegate { id: helpTextDelegate - text: i18n("Select an account to use for '%1'", LauncherCore.currentProfile.name) + text: i18n("Select an account to use for '%1'.", LauncherCore.currentProfile.name) } } @@ -39,16 +39,6 @@ FormCard.FormCardPage { Layout.fillWidth: true - FormCard.FormTextDelegate { - id: existingHelpDelegate - - text: i18n("You can select an existing account.") - } - - FormCard.FormDelegateSeparator { - above: existingHelpDelegate - } - Repeater { model: LauncherCore.accountManager @@ -65,6 +55,11 @@ FormCard.FormCardPage { } } + FormCard.FormHeader { + title: i18n("Create New Account") + visible: LauncherCore.accountManager.hasAnyAccounts() + } + FormCard.FormCard { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true @@ -72,7 +67,8 @@ FormCard.FormCardPage { FormCard.FormButtonDelegate { id: addSquareEnixButton - text: i18n("Add Square Enix Account") + text: i18n("Add Square Enix Account…") + description: i18n("Used for logging into the official game servers.") icon.name: "list-add-symbolic" onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "AddSquareEnix"), { profile: page.profile @@ -87,7 +83,8 @@ FormCard.FormCardPage { FormCard.FormButtonDelegate { id: addSapphireButton - text: i18n("Add Sapphire Account") + text: i18n("Add Sapphire Account…") + description: i18n("Only used for Sapphire servers, don't select this if unless you plan to connect to one.") icon.name: "list-add-symbolic" onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "AddSapphire"), { profile: page.profile diff --git a/launcher/ui/Setup/DownloadSetup.qml b/launcher/ui/Setup/DownloadSetup.qml deleted file mode 100644 index 56e15ad..0000000 --- a/launcher/ui/Setup/DownloadSetup.qml +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2023 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 - -FormCard.FormCardPage { - id: page - - property var profile - - title: i18n("Download Game") - - FormCard.FormCard { - Layout.topMargin: Kirigami.Units.largeSpacing - Layout.fillWidth: true - - FormCard.FormTextDelegate { - id: helpTextDelegate - - text: i18n("Press the button below to download and setup the game.") - description: i18n("This only installs the base files required for the initial update. Astra can only download patches with a legitimate Square Enix account.") - } - - FormCard.FormDelegateSeparator { - above: helpTextDelegate - below: buttonDelegate - } - - FormCard.FormButtonDelegate { - id: buttonDelegate - - text: i18n("Begin installation") - icon.name: "cloud-download" - onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "InstallProgress"), { - gameInstaller: LauncherCore.createInstaller(page.profile) - }) - } - } -} \ No newline at end of file diff --git a/launcher/ui/Setup/InstallProgress.qml b/launcher/ui/Setup/InstallProgress.qml index 32d9309..7de2c86 100644 --- a/launcher/ui/Setup/InstallProgress.qml +++ b/launcher/ui/Setup/InstallProgress.qml @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick +import QtQuick.Window import QtQuick.Layouts import org.kde.kirigami as Kirigami @@ -18,32 +19,33 @@ Kirigami.Page { Kirigami.LoadingPlaceholder { anchors.centerIn: parent - text: i18n("Installing...") + text: i18n("Installing…") } Kirigami.PromptDialog { id: errorDialog - title: i18n("Install error") + title: i18n("Installation Error") showCloseButton: false standardButtons: Kirigami.Dialog.Ok - onAccepted: applicationWindow().pageStack.layers.pop() - onRejected: applicationWindow().pageStack.layers.pop() + onAccepted: page.Window.window.pageStack.layers.pop() + onRejected: page.Window.window.pageStack.layers.pop() } - Component.onCompleted: gameInstaller.installGame() + Component.onCompleted: gameInstaller.start() Connections { target: page.gameInstaller function onInstallFinished() { - applicationWindow().checkSetup(); + // Prevents it from failing to push the page if the install happens too quickly. + Qt.callLater(() => applicationWindow().checkSetup()); } function onError(message) { - errorDialog.subtitle = message - errorDialog.open() + errorDialog.subtitle = i18n("An error has occurred while installing the game:\n\n%1", message); + errorDialog.open(); } } } \ No newline at end of file diff --git a/launcher/ui/Setup/SetupPage.qml b/launcher/ui/Setup/SetupPage.qml index c73e49d..fd51b4c 100644 --- a/launcher/ui/Setup/SetupPage.qml +++ b/launcher/ui/Setup/SetupPage.qml @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2023 Joshua Goins // SPDX-License-Identifier: GPL-3.0-or-later +import QtCore import QtQuick +import QtQuick.Dialogs import QtQuick.Layouts import org.kde.kirigami as Kirigami @@ -35,14 +37,14 @@ FormCard.FormCardPage { if (page.isInitialSetup) { return i18n("You must have a legitimate installation of the FFXIV to continue."); } else { - return i18n("You must select a legitimate installation of FFXIV for '%1'", page.profile.name); + return i18n("Select a game installation of FFXIV for '%1'.", page.profile.name); } } } } FormCard.FormHeader { - title: i18n("Existing Installations") + title: i18n("Existing Installation") visible: LauncherCore.profileManager.hasAnyExistingInstallations() } @@ -54,7 +56,7 @@ FormCard.FormCardPage { FormCard.FormTextDelegate { id: existingHelpDelegate - text: i18n("You can select an existing installation from another profile.") + text: i18n("You can select an existing game installation from another profile.") } FormCard.FormDelegateSeparator { @@ -77,6 +79,26 @@ FormCard.FormCardPage { } } } + + FormCard.FormDelegateSeparator { + below: importDelegate + } + + FormCard.FormButtonDelegate { + id: importDelegate + + text: i18n("Import Existing Installation…") + description: i18n("Select an existing installation on disk or import from another launcher.") + icon.name: "document-import-symbolic" + onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "ExistingSetup"), { + profile: page.profile + }) + } + } + + FormCard.FormHeader { + title: i18n("Install Game") + visible: LauncherCore.accountManager.hasAnyAccounts() } FormCard.FormCard { @@ -84,28 +106,43 @@ FormCard.FormCardPage { Layout.fillWidth: true FormCard.FormButtonDelegate { - id: findExistingDelegate + id: downloadDelegate - text: i18n("Find Existing Installation") - icon.name: "edit-find" - onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "ExistingSetup"), { - profile: page.profile + text: i18n("Download & Install Game") + description: i18n("Download the retail installer online from Square Enix.") + icon.name: "cloud-download" + onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "InstallProgress"), { + gameInstaller: LauncherCore.createInstaller(page.profile) }) } FormCard.FormDelegateSeparator { - above: findExistingDelegate - below: downloadDelegate + above: downloadDelegate + below: selectInstallDelegate } FormCard.FormButtonDelegate { - id: downloadDelegate + id: selectInstallDelegate - text: i18n("Download Game") - icon.name: "cloud-download" - onClicked: page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "DownloadSetup"), { - profile: page.profile - }) + text: i18n("Select Existing Installer…") + description: i18n("Use a previously downloaded installer. Useful if offline or can't otherwise access the official servers.") + icon.name: "edit-find" + + FileDialog { + id: dialog + + currentFolder: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] + nameFilters: [i18n("Windows executable (*.exe)")] + + onAccepted: { + const url = decodeURIComponent(selectedFile.toString().replace("file://", "")); + page.Window.window.pageStack.layers.push(Qt.createComponent("zone.xiv.astra", "InstallProgress"), { + gameInstaller: LauncherCore.createInstallerFromExisting(page.profile, url) + }); + } + } + + onClicked: dialog.open() } } } \ No newline at end of file