#include "launcherwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include "settingswindow.h" #include "squareboot.h" #include "squarelauncher.h" #include "sapphirelauncher.h" #include "assetupdater.h" #include "headline.h" #include "config.h" #include "aboutwindow.h" #include "gameinstaller.h" #include "bannerwidget.h" #include "encryptedarg.h" LauncherWindow::LauncherWindow(LauncherCore& core, QWidget* parent) : QMainWindow(parent), core(core) { setWindowTitle("Astra"); connect(&core, &LauncherCore::settingsChanged, this, &LauncherWindow::reloadControls); QMenu* toolsMenu = menuBar()->addMenu("Tools"); launchOfficial = toolsMenu->addAction("Open Official Launcher..."); launchOfficial->setIcon(QIcon::fromTheme("application-x-executable")); connect(launchOfficial, &QAction::triggered, [=] { struct Argument { QString key, value; }; QString executeArg("%1%2%3%4"); QDateTime dateTime = QDateTime::currentDateTime(); executeArg = executeArg.arg(dateTime.date().month() + 1, 2, 10, QLatin1Char('0')); executeArg = executeArg.arg(dateTime.date().day(), 2, 10, QLatin1Char('0')); executeArg = executeArg.arg(dateTime.time().hour(), 2, 10, QLatin1Char('0')); executeArg = executeArg.arg(dateTime.time().minute(), 2, 10, QLatin1Char('0')); QList arguments; arguments.push_back({"ExecuteArg", executeArg}); // find user path QString userPath; // TODO: don't put this here QString searchDir; #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) searchDir = currentProfile().winePrefixPath + "/drive_c/Users"; #else searchDir = "C:/Users"; #endif QDirIterator it(searchDir); while (it.hasNext()) { QString dir = it.next(); QFileInfo fi(dir); QString fileName = fi.fileName(); // FIXME: is there no easier way to filter out these in Qt? if(fi.fileName() != "Public" && fi.fileName() != "." && fi.fileName() != "..") { userPath = fileName; } } arguments.push_back({"UserPath", QString(R"(C:\Users\%1\My Documents\My Games\FINAL FANTASY XIV - A Realm Reborn)").arg(userPath)}); const QString argFormat = " /%1 =%2"; QString argJoined; for(auto& arg : arguments) { argJoined += argFormat.arg(arg.key, arg.value.replace(" ", " ")); } QString finalArg = encryptGameArg(argJoined); this->core.launchExecutable(currentProfile(), {currentProfile().gamePath + "/boot/ffxivlauncher64.exe", finalArg}); }); launchSysInfo = toolsMenu->addAction("Open System Info..."); launchSysInfo->setIcon(QIcon::fromTheme("application-x-executable")); connect(launchSysInfo, &QAction::triggered, [=] { this->core.launchExecutable(currentProfile(), {currentProfile().gamePath + "/boot/ffxivsysinfo64.exe"}); }); launchCfgBackup = toolsMenu->addAction("Open Config Backup..."); launchCfgBackup->setIcon(QIcon::fromTheme("application-x-executable")); connect(launchCfgBackup, &QAction::triggered, [=] { this->core.launchExecutable(currentProfile(), {currentProfile().gamePath + "/boot/ffxivconfig64.exe"}); }); toolsMenu->addSeparator(); openGameDir = toolsMenu->addAction("Open Game Directory..."); openGameDir->setIcon(QIcon::fromTheme("document-open")); connect(openGameDir, &QAction::triggered, [=] { openPath(currentProfile().gamePath); }); QMenu* gameMenu = menuBar()->addMenu("Game"); auto installGameAction = gameMenu->addAction("Install game..."); connect(installGameAction, &QAction::triggered, [this] { // TODO: lol duplication auto messageBox = new QMessageBox(this); messageBox->setIcon(QMessageBox::Icon::Question); messageBox->setText("Warning"); messageBox->setInformativeText("FFXIV will be installed to your selected game directory."); QString detailedText = QString("Astra will install FFXIV for you at '%1'").arg(this->currentProfile().gamePath); detailedText.append("\n\nIf you do not wish to install it to this location, please change your profile settings."); messageBox->setDetailedText(detailedText); messageBox->setWindowModality(Qt::WindowModal); auto installButton = messageBox->addButton("Install Game", QMessageBox::YesRole); connect(installButton, &QPushButton::clicked, [this, messageBox] { installGame(this->core, this->currentProfile(), [this, messageBox] { this->core.readGameVersion(); messageBox->close(); }); }); messageBox->addButton(QMessageBox::StandardButton::No); messageBox->setDefaultButton(installButton); messageBox->exec(); }); QMenu* fileMenu = menuBar()->addMenu("Settings"); QAction* settingsAction = fileMenu->addAction("Configure Astra..."); settingsAction->setIcon(QIcon::fromTheme("configure")); settingsAction->setMenuRole(QAction::MenuRole::PreferencesRole); connect(settingsAction, &QAction::triggered, [=] { auto window = new SettingsWindow(0, *this, this->core, this); connect(&this->core, &LauncherCore::settingsChanged, window, &SettingsWindow::reloadControls); window->show(); }); QAction* profilesAction = fileMenu->addAction("Configure Profiles..."); profilesAction->setIcon(QIcon::fromTheme("configure")); profilesAction->setMenuRole(QAction::MenuRole::NoRole); connect(profilesAction, &QAction::triggered, [=] { auto window = new SettingsWindow(1, *this, this->core, this); connect(&this->core, &LauncherCore::settingsChanged, window, &SettingsWindow::reloadControls); window->show(); }); #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) fileMenu->addSeparator(); wineCfg = fileMenu->addAction("Configure Wine..."); wineCfg->setMenuRole(QAction::MenuRole::NoRole); wineCfg->setIcon(QIcon::fromTheme("configure")); connect(wineCfg, &QAction::triggered, [=] { this->core.launchExternalTool(currentProfile(), {"regedit.exe"}); }); #endif QMenu* helpMenu = menuBar()->addMenu("Help"); QAction* showAbout = helpMenu->addAction("About Astra"); showAbout->setIcon(QIcon::fromTheme("help-about")); connect(showAbout, &QAction::triggered, [=] { auto window = new AboutWindow(this); window->show(); }); QAction* showAboutQt = helpMenu->addAction("About Qt"); showAboutQt->setIcon(QIcon::fromTheme("help-about")); connect(showAboutQt, &QAction::triggered, [=] { QMessageBox::aboutQt(this); }); layout = new QGridLayout(); bannerScrollArea = new QScrollArea(); bannerLayout = new QHBoxLayout(); bannerLayout->setContentsMargins(0, 0, 0, 0); bannerLayout->setSpacing(0); bannerLayout->setSizeConstraint(QLayout::SizeConstraint::SetMinAndMaxSize); bannerParentWidget = new QWidget(); bannerParentWidget->setFixedHeight(250); bannerScrollArea->setFixedWidth(640); bannerScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); bannerScrollArea->verticalScrollBar()->setEnabled(false); bannerScrollArea->horizontalScrollBar()->setEnabled(false); bannerScrollArea->setWidget(bannerParentWidget); bannerParentWidget->setLayout(bannerLayout); newsListView = new QTreeWidget(); newsListView->setColumnCount(2); newsListView->setHeaderLabels({"Title", "Date"}); connect(newsListView, &QTreeWidget::itemClicked, [](QTreeWidgetItem* item, int column) { auto url = item->data(0, Qt::UserRole).toUrl(); qInfo() << "clicked" << url; QDesktopServices::openUrl(url); }); loginLayout = new QFormLayout(); layout->addLayout(loginLayout, 0, 1, 1, 1); profileSelect = new QComboBox(); connect(profileSelect, static_cast(&QComboBox::currentIndexChanged), [=](int index) { reloadControls(); }); loginLayout->addRow("Profile", profileSelect); usernameEdit = new QLineEdit(); loginLayout->addRow("Username", usernameEdit); rememberUsernameBox = new QCheckBox(); connect(rememberUsernameBox, &QCheckBox::stateChanged, [=](int) { currentProfile().rememberUsername = rememberUsernameBox->isChecked(); this->core.saveSettings(); }); loginLayout->addRow("Remember Username?", rememberUsernameBox); passwordEdit = new QLineEdit(); passwordEdit->setEchoMode(QLineEdit::EchoMode::Password); loginLayout->addRow("Password", passwordEdit); rememberPasswordBox = new QCheckBox(); connect(rememberPasswordBox, &QCheckBox::stateChanged, [=](int) { currentProfile().rememberPassword = rememberPasswordBox->isChecked(); this->core.saveSettings(); }); loginLayout->addRow("Remember Password?", rememberPasswordBox); otpEdit = new QLineEdit(); loginButton = new QPushButton("Login"); registerButton = new QPushButton("Register"); connect(otpEdit, &QLineEdit::returnPressed, [this] { this->core.assetUpdater->update(currentProfile()); }); connect(passwordEdit, &QLineEdit::returnPressed, [this] { this->core.assetUpdater->update(currentProfile()); }); auto emptyWidget = new QWidget(); emptyWidget->setLayout(layout); setCentralWidget(emptyWidget); connect(core.assetUpdater, &AssetUpdater::finishedUpdating, [=] { auto info = LoginInformation{¤tProfile(), usernameEdit->text(), passwordEdit->text(), otpEdit->text()}; #ifndef QT_DEBUG if(currentProfile().rememberUsername) { auto job = new QKeychain::WritePasswordJob("LauncherWindow"); job->setTextData(usernameEdit->text()); job->setKey(currentProfile().name + "-username"); job->start(); } #endif #ifndef QT_DEBUG if(currentProfile().rememberPassword) { auto job = new QKeychain::WritePasswordJob("LauncherWindow"); job->setTextData(passwordEdit->text()); job->setKey(currentProfile().name + "-password"); job->start(); } #endif if(currentProfile().isSapphire) { //this->core.sapphireLauncher->login(currentProfile().lobbyURL, info); } else { this->core.squareBoot->bootCheck(info); } }); connect(loginButton, &QPushButton::released, [=] { // update the assets first if needed, then it calls the slot above :-) this->core.assetUpdater->update(currentProfile()); }); connect(registerButton, &QPushButton::released, [=] { if(currentProfile().isSapphire) { auto info = LoginInformation{¤tProfile(), usernameEdit->text(), passwordEdit->text(), otpEdit->text()}; this->core.sapphireLauncher->registerAccount(currentProfile().lobbyURL, info); } }); connect(&core, &LauncherCore::successfulLaunch, [&] { if(core.appSettings.closeWhenLaunched) hide(); }); connect(&core, &LauncherCore::gameClosed, [&] { if(core.appSettings.closeWhenLaunched) QCoreApplication::quit(); }); getHeadline(core,[&](Headline headline) { this->headline = headline; reloadNews(); }); reloadControls(); } ProfileSettings LauncherWindow::currentProfile() const { return core.getProfile(profileSelect->currentIndex()); } ProfileSettings& LauncherWindow::currentProfile() { return core.getProfile(profileSelect->currentIndex()); } void LauncherWindow::reloadControls() { if(currentlyReloadingControls) return; currentlyReloadingControls = true; const int oldIndex = profileSelect->currentIndex(); profileSelect->clear(); for(const auto& profile : core.profileList()) { profileSelect->addItem(profile); } profileSelect->setCurrentIndex(oldIndex); if(profileSelect->currentIndex() == -1) { profileSelect->setCurrentIndex(core.defaultProfileIndex); } rememberUsernameBox->setChecked(currentProfile().rememberUsername); #ifndef QT_DEBUG if(currentProfile().rememberUsername) { auto job = new QKeychain::ReadPasswordJob("LauncherWindow"); job->setKey(currentProfile().name + "-username"); job->start(); connect(job, &QKeychain::ReadPasswordJob::finished, [=](QKeychain::Job* j) { usernameEdit->setText(job->textData()); }); } #endif rememberPasswordBox->setChecked(currentProfile().rememberPassword); #ifndef QT_DEBUG if(currentProfile().rememberPassword) { auto job = new QKeychain::ReadPasswordJob("LauncherWindow"); job->setKey(currentProfile().name + "-password"); job->start(); connect(job, &QKeychain::ReadPasswordJob::finished, [=](QKeychain::Job* j) { passwordEdit->setText(job->textData()); }); } #endif bool canLogin = true; if(currentProfile().isSapphire) { if(currentProfile().lobbyURL.isEmpty()) { loginButton->setText("Login (Lobby URL is invalid)"); canLogin = false; } } else { if(!core.squareLauncher->isGateOpen) { loginButton->setText("Login (Maintenance is in progress)"); canLogin = false; } } #if defined(Q_OS_LINUX) || defined(Q_OS_MAC) if(!currentProfile().isWineInstalled()) { loginButton->setText("Login (Wine is not installed)"); canLogin = false; } #endif if(!currentProfile().isGameInstalled()) { loginButton->setText("Login (Game is not installed)"); canLogin = false; } if(canLogin) loginButton->setText("Login"); launchOfficial->setEnabled(currentProfile().isGameInstalled()); launchSysInfo->setEnabled(currentProfile().isGameInstalled()); launchCfgBackup->setEnabled(currentProfile().isGameInstalled()); openGameDir->setEnabled(currentProfile().isGameInstalled()); #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) wineCfg->setEnabled(currentProfile().isWineInstalled()); #endif layout->removeWidget(bannerScrollArea); bannerScrollArea->hide(); layout->removeWidget(newsListView); newsListView->hide(); auto field = loginLayout->labelForField(otpEdit); if(field != nullptr) field->deleteLater(); loginLayout->takeRow(otpEdit); otpEdit->hide(); if(currentProfile().useOneTimePassword && !currentProfile().isSapphire) { loginLayout->addRow("One-Time Password", otpEdit); otpEdit->show(); } loginLayout->takeRow(loginButton); loginButton->setEnabled(canLogin); registerButton->setEnabled(canLogin); loginLayout->addRow(loginButton); loginLayout->takeRow(registerButton); registerButton->hide(); if(currentProfile().isSapphire) { loginLayout->addRow(registerButton); registerButton->show(); } reloadNews(); currentlyReloadingControls = false; } void LauncherWindow::reloadNews() { if(core.appSettings.showBanners || core.appSettings.showNewsList) { for(auto widget : bannerWidgets) { bannerLayout->removeWidget(widget); } bannerWidgets.clear(); int totalRow = 0; if(core.appSettings.showBanners) { bannerScrollArea->show(); layout->addWidget(bannerScrollArea, totalRow++, 0); } if(core.appSettings.showNewsList) { newsListView->show(); layout->addWidget(newsListView, totalRow++, 0); } newsListView->clear(); if (!headline.banner.empty()) { if(core.appSettings.showBanners) { for(auto banner : headline.banner) { auto request = QNetworkRequest(banner.bannerImage); core.buildRequest(currentProfile(), request); auto reply = core.mgr->get(request); connect(reply, &QNetworkReply::finished, [=] { auto bannerImageView = new BannerWidget(); bannerImageView->setUrl(banner.link); QPixmap pixmap; pixmap.loadFromData(reply->readAll()); bannerImageView->setPixmap(pixmap); bannerLayout->addWidget(bannerImageView); bannerWidgets.push_back(bannerImageView); }); } if(bannerTimer == nullptr) { bannerTimer = new QTimer(); connect(bannerTimer, &QTimer::timeout, this, [=] { if (currentBanner >= headline.banner.size()) currentBanner = 0; bannerScrollArea->ensureVisible( 640 * (currentBanner + 1), 0, 0, 0); currentBanner++; }); bannerTimer->start(5000); } } else { if(bannerTimer != nullptr) { bannerTimer->stop(); bannerTimer->deleteLater(); bannerTimer = nullptr; } } if(core.appSettings.showNewsList) { QTreeWidgetItem* newsItem = new QTreeWidgetItem( (QTreeWidgetItem*)nullptr, QStringList("News")); for (auto news : headline.news) { QTreeWidgetItem* item = new QTreeWidgetItem(); item->setText(0, news.title); item->setText(1, QLocale().toString( news.date, QLocale::ShortFormat)); item->setData(0, Qt::UserRole, news.url); newsItem->addChild(item); } QTreeWidgetItem* pinnedItem = new QTreeWidgetItem( (QTreeWidgetItem*)nullptr, QStringList("Pinned")); for (auto pinned : headline.pinned) { QTreeWidgetItem* item = new QTreeWidgetItem(); item->setText(0, pinned.title); item->setText(1, QLocale().toString(pinned.date, QLocale::ShortFormat)); item->setData(0, Qt::UserRole, pinned.url); pinnedItem->addChild(item); } QTreeWidgetItem* topicsItem = new QTreeWidgetItem( (QTreeWidgetItem*)nullptr, QStringList("Topics")); for (auto news : headline.topics) { QTreeWidgetItem* item = new QTreeWidgetItem(); item->setText(0, news.title); item->setText(1, QLocale().toString( news.date, QLocale::ShortFormat)); item->setData(0, Qt::UserRole, news.url); topicsItem->addChild(item); } newsListView->insertTopLevelItems( 0, QList( {newsItem, pinnedItem, topicsItem})); for (int i = 0; i < 3; i++) { newsListView->expandItem(newsListView->topLevelItem(i)); newsListView->resizeColumnToContents(i); } } } } } void LauncherWindow::openPath(const QString path) { #if defined(Q_OS_WIN) // for some reason, windows requires special treatment (what else is new?) const QFileInfo fileInfo(path); QProcess::startDetached("explorer.exe", QStringList(QDir::toNativeSeparators(fileInfo.canonicalFilePath()))); #else QDesktopServices::openUrl("file://" + path); #endif }