diff options
Diffstat (limited to 'archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp')
| -rw-r--r-- | archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp | 1297 |
1 files changed, 1297 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp b/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp new file mode 100644 index 0000000000..37101a0b7f --- /dev/null +++ b/archived/projt-launcher/launcher/ui/widgets/LauncherHubWidget.cpp @@ -0,0 +1,1297 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2026 Project Tick +// SPDX-FileContributor: Project Tick Team +/* + * ProjT Launcher - Minecraft Launcher + * Copyright (C) 2026 Project Tick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "LauncherHubWidget.h" + +#include <QApplication> +#include <QDateTime> +#include <QDesktopServices> +#include <QDir> +#include <QEvent> +#include <QFrame> +#include <QGridLayout> +#include <QHBoxLayout> +#include <QIcon> +#include <QLabel> +#include <QLineEdit> +#include <QLocale> +#include <QPainter> +#include <QPushButton> +#include <QScrollArea> +#include <QSignalBlocker> +#include <QSizePolicy> +#include <QStackedWidget> +#include <QTabBar> +#include <QTextDocumentFragment> +#include <QToolButton> +#include <QVBoxLayout> + +#include "Application.h" +#include "BaseInstance.h" +#include "BuildConfig.h" +#include "InstanceList.h" +#include "MMCTime.h" +#include "icons/IconList.hpp" +#include "news/NewsChecker.h" +#include "ui/widgets/CefHubView.h" +#include "ui/widgets/FallbackHubView.h" +#include "ui/widgets/HubSearchProvider.h" +#include "ui/widgets/WebView2Widget.h" +#include "ui/widgets/QtWebEngineHubView.h" + +#if defined(PROJT_DISABLE_LAUNCHER_HUB) +LauncherHubWidget::LauncherHubWidget(QWidget* parent) : QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(24, 24, 24, 24); + + auto* label = new QLabel(tr("Launcher Hub is not available in this build."), this); + label->setAlignment(Qt::AlignCenter); + label->setWordWrap(true); + layout->addWidget(label, 1); +} + +LauncherHubWidget::~LauncherHubWidget() = default; + +void LauncherHubWidget::ensureLoaded() +{} + +void LauncherHubWidget::loadHome() +{} + +void LauncherHubWidget::openUrl(const QUrl& url) +{ + if (url.isValid()) + QDesktopServices::openUrl(url); +} + +void LauncherHubWidget::newTab(const QUrl& url) +{ + openUrl(url); +} + +void LauncherHubWidget::setHomeUrl(const QUrl& url) +{ + m_homeUrl = url; +} + +QUrl LauncherHubWidget::homeUrl() const +{ + return m_homeUrl; +} + +void LauncherHubWidget::setSelectedInstanceId(const QString&) +{} + +void LauncherHubWidget::refreshCockpit() +{} + +void LauncherHubWidget::changeEvent(QEvent* event) +{ + QWidget::changeEvent(event); +} + +#else + +namespace +{ + QUrl defaultHubUrl() + { + if (!BuildConfig.HUB_HOME_URL.isEmpty()) + { + return QUrl(BuildConfig.HUB_HOME_URL); + } + return QUrl(QStringLiteral("https://projecttick.org/p/projt-launcher/")); + } + + void clearLayout(QLayout* layout) + { + if (!layout) + { + return; + } + + while (auto* item = layout->takeAt(0)) + { + if (auto* widget = item->widget()) + { + delete widget; + } + if (auto* childLayout = item->layout()) + { + clearLayout(childLayout); + delete childLayout; + } + delete item; + } + } + + QString relativeTimeLabel(qint64 timestamp) + { + if (timestamp <= 0) + { + return LauncherHubWidget::tr("Never launched"); + } + + const QDateTime launchedAt = QDateTime::fromMSecsSinceEpoch(timestamp); + const qint64 secondsAgo = launchedAt.secsTo(QDateTime::currentDateTime()); + if (secondsAgo < 60) + { + return LauncherHubWidget::tr("Just now"); + } + if (secondsAgo < 3600) + { + return LauncherHubWidget::tr("%1 min ago").arg(secondsAgo / 60); + } + if (secondsAgo < 86400) + { + return LauncherHubWidget::tr("%1 hr ago").arg(secondsAgo / 3600); + } + if (secondsAgo < 604800) + { + return LauncherHubWidget::tr("%1 day(s) ago").arg(secondsAgo / 86400); + } + return QLocale().toString(launchedAt, QLocale::ShortFormat); + } + + QString stripHtmlExcerpt(const QString& html, int maxLength = 120) + { + QString text = QTextDocumentFragment::fromHtml(html).toPlainText().simplified(); + if (text.size() <= maxLength) + { + return text; + } + return text.left(maxLength - 1) + QStringLiteral("..."); + } + + QString heroBadgeForInstance(const InstancePtr& instance) + { + if (!instance) + { + return LauncherHubWidget::tr("Cockpit"); + } + if (instance->isRunning()) + { + return LauncherHubWidget::tr("Now playing"); + } + if (instance->hasCrashed() || instance->hasVersionBroken()) + { + return LauncherHubWidget::tr("Needs attention"); + } + if (instance->hasUpdateAvailable()) + { + return LauncherHubWidget::tr("Update ready"); + } + return LauncherHubWidget::tr("Ready to launch"); + } + + QList<InstancePtr> sortedInstances() + { + QList<InstancePtr> instances; + if (!APPLICATION->instances()) + { + return instances; + } + + for (int i = 0; i < APPLICATION->instances()->count(); ++i) + { + instances.append(APPLICATION->instances()->at(i)); + } + + std::sort(instances.begin(), + instances.end(), + [](const InstancePtr& left, const InstancePtr& right) + { + if (left->lastLaunch() == right->lastLaunch()) + { + return left->name().localeAwareCompare(right->name()) < 0; + } + return left->lastLaunch() > right->lastLaunch(); + }); + return instances; + } + + QIcon tintedIcon(const QString& themeName, const QWidget* widget, const QSize& size = QSize(18, 18)) + { + QIcon source = QIcon::fromTheme(themeName); + if (source.isNull()) + { + return source; + } + + const qreal devicePixelRatio = widget ? widget->devicePixelRatioF() : qApp->devicePixelRatio(); + const QSize pixelSize = QSize(qMax(1, qRound(size.width() * devicePixelRatio)), + qMax(1, qRound(size.height() * devicePixelRatio))); + + auto colorizedPixmap = [&](QIcon::Mode mode) + { + QPixmap sourcePixmap = source.pixmap(pixelSize, devicePixelRatio, mode); + if (sourcePixmap.isNull()) + { + sourcePixmap = source.pixmap(pixelSize, devicePixelRatio, QIcon::Normal); + } + + QPixmap tinted(pixelSize); + tinted.fill(Qt::transparent); + + const QColor color = widget ? widget->palette().color(mode == QIcon::Disabled ? QPalette::Disabled + : QPalette::Active, + QPalette::ButtonText) + : QColor(Qt::white); + + QPainter painter(&tinted); + painter.drawPixmap(0, 0, sourcePixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(tinted.rect(), color); + painter.end(); + tinted.setDevicePixelRatio(devicePixelRatio); + return tinted; + }; + + QIcon icon; + icon.addPixmap(colorizedPixmap(QIcon::Normal), QIcon::Normal); + icon.addPixmap(colorizedPixmap(QIcon::Disabled), QIcon::Disabled); + icon.addPixmap(colorizedPixmap(QIcon::Active), QIcon::Active); + icon.addPixmap(colorizedPixmap(QIcon::Selected), QIcon::Selected); + return icon; + } + HubViewBase* createBrowserView(QWidget* parent) + { +#if defined(PROJT_USE_WEBVIEW2) + return new WebView2Widget(parent); +#elif defined(PROJT_USE_WEBENGINE) + return new QtWebEngineHubView(parent); +#elif defined(PROJT_USE_CEF) + return new CefHubView(parent); +#else + return new FallbackHubView(QObject::tr("This page opens in your browser on this platform."), parent); +#endif + } +} + +LauncherHubWidget::LauncherHubWidget(QWidget* parent) : QWidget(parent) +{ + m_homeUrl = defaultHubUrl(); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_tabsBarContainer = new QWidget(this); + m_tabsBarContainer->setObjectName("hubTabsBar"); + auto* tabsLayout = new QHBoxLayout(m_tabsBarContainer); + tabsLayout->setContentsMargins(10, 10, 10, 6); + + m_tabBar = new QTabBar(this); + m_tabBar->setMovable(true); + m_tabBar->setExpanding(false); + m_tabBar->setDocumentMode(true); + m_tabBar->setTabsClosable(true); + tabsLayout->addWidget(m_tabBar, 1); + + m_toolbarContainer = new QWidget(this); + m_toolbarContainer->setObjectName("hubToolbar"); + auto* toolbar = new QHBoxLayout(m_toolbarContainer); + toolbar->setContentsMargins(10, 8, 10, 10); + + m_backButton = new QToolButton(this); + m_backButton->setToolTip(tr("Back")); + m_backButton->setEnabled(false); + + m_forwardButton = new QToolButton(this); + m_forwardButton->setToolTip(tr("Forward")); + m_forwardButton->setEnabled(false); + + m_reloadButton = new QToolButton(this); + m_reloadButton->setToolTip(tr("Reload")); + + m_homeButton = new QToolButton(this); + m_homeButton->setToolTip(tr("Cockpit")); + + m_newTabButton = new QToolButton(this); + m_newTabButton->setToolTip(tr("New Tab")); + + m_addressBar = new QLineEdit(this); + m_addressBar->setPlaceholderText(tr("Search or enter address")); + m_addressBar->setClearButtonEnabled(true); + + m_goButton = new QToolButton(this); + m_goButton->setToolTip(tr("Go")); + + toolbar->addWidget(m_backButton); + toolbar->addWidget(m_forwardButton); + toolbar->addWidget(m_reloadButton); + toolbar->addWidget(m_homeButton); + toolbar->addWidget(m_newTabButton); + toolbar->addWidget(m_addressBar, 1); + toolbar->addWidget(m_goButton); + + m_stack = new QStackedWidget(this); + + layout->addWidget(m_tabsBarContainer); + layout->addWidget(m_toolbarContainer); + layout->addWidget(m_stack); + + connect(m_backButton, + &QToolButton::clicked, + this, + [this]() + { + if (auto* view = currentView()) + { + view->back(); + } + }); + connect(m_forwardButton, + &QToolButton::clicked, + this, + [this]() + { + if (auto* view = currentView()) + { + view->forward(); + } + }); + connect(m_reloadButton, + &QToolButton::clicked, + this, + [this]() + { + if (auto* view = currentView()) + { + view->reload(); + } + else + { + refreshCockpit(); + } + }); + connect(m_homeButton, &QToolButton::clicked, this, &LauncherHubWidget::loadHome); + connect(m_goButton, + &QToolButton::clicked, + this, + [this]() + { + const QString providerId = + APPLICATION->settings() ? APPLICATION->settings()->get("HubSearchEngine").toString() : QString(); + openUrl(resolveHubInput(m_addressBar->text(), providerId)); + }); + connect(m_addressBar, + &QLineEdit::returnPressed, + this, + [this]() + { + const QString providerId = + APPLICATION->settings() ? APPLICATION->settings()->get("HubSearchEngine").toString() : QString(); + openUrl(resolveHubInput(m_addressBar->text(), providerId)); + }); + connect(m_newTabButton, &QToolButton::clicked, this, [this]() { newTab(m_homeUrl); }); + + connect(m_tabBar, + &QTabBar::tabMoved, + this, + [this](int, int) + { + updateTabPerformanceState(); + updateNavigationState(); + }); + connect(m_tabBar, + &QTabBar::currentChanged, + this, + [this](int index) + { + if (auto* view = viewForTabIndex(index)) + { + m_stack->setCurrentWidget(view); + activatePendingForPage(view); + updateTabPerformanceState(); + updateNavigationState(); + } + }); + connect(m_tabBar, + &QTabBar::tabCloseRequested, + this, + [this](int index) + { + auto* view = viewForTabIndex(index); + if (!view) + { + return; + } + + m_stack->removeWidget(view); + m_tabBar->removeTab(index); + view->deleteLater(); + + if (m_tabBar->count() > 0) + { + const int newIndex = qMin(index, m_tabBar->count() - 1); + m_tabBar->setCurrentIndex(newIndex); + if (auto* nextView = viewForTabIndex(newIndex)) + { + m_stack->setCurrentWidget(nextView); + activatePendingForPage(nextView); + } + } + else + { + switchToPage(m_cockpitPage); + } + + syncTabsUi(); + updateTabPerformanceState(); + updateNavigationState(); + }); + + m_newsChecker = new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL); + m_newsChecker->setParent(this); + connect(m_newsChecker, &NewsChecker::newsLoaded, this, &LauncherHubWidget::rebuildNewsFeed); + connect(m_newsChecker, &NewsChecker::newsLoadingFailed, this, &LauncherHubWidget::rebuildNewsFeed); + + if (APPLICATION->instances()) + { + connect(APPLICATION->instances().get(), + &InstanceList::instancesChanged, + this, + &LauncherHubWidget::refreshCockpit); + connect(APPLICATION->instances().get(), + &InstanceList::dataChanged, + this, + [this](const QModelIndex&, const QModelIndex&, const QList<int>&) { refreshCockpit(); }); + } + if (APPLICATION->icons()) + { + connect(APPLICATION->icons().get(), + &projt::icons::IconList::iconUpdated, + this, + [this](const QString&) { refreshCockpit(); }); + } + + createCockpitTab(); + refreshToolbarIcons(); + + refreshCockpit(); + m_newsChecker->reloadNews(); + syncTabsUi(); +} + +LauncherHubWidget::~LauncherHubWidget() = default; + +HubViewBase* LauncherHubWidget::currentView() const +{ + if (!m_stack) + { + return nullptr; + } + return qobject_cast<HubViewBase*>(m_stack->currentWidget()); +} + +HubViewBase* LauncherHubWidget::viewForTabIndex(int index) const +{ + if (!m_tabBar || index < 0 || index >= m_tabBar->count()) + { + return nullptr; + } + + return qobject_cast<HubViewBase*>(m_tabBar->tabData(index).value<QObject*>()); +} + +int LauncherHubWidget::tabIndexForView(const HubViewBase* view) const +{ + if (!m_tabBar || !view) + { + return -1; + } + + for (int i = 0; i < m_tabBar->count(); ++i) + { + if (m_tabBar->tabData(i).value<QObject*>() == view) + { + return i; + } + } + return -1; +} + +void LauncherHubWidget::createCockpitTab() +{ + auto* scrollArea = new QScrollArea(m_stack); + scrollArea->setWidgetResizable(true); + scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + auto* content = new QWidget(scrollArea); + content->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + auto* pageLayout = new QVBoxLayout(content); + pageLayout->setContentsMargins(18, 18, 18, 18); + pageLayout->setSpacing(16); + + auto* heroCard = new QFrame(content); + heroCard->setObjectName("hubHeroCard"); + auto* heroLayout = new QVBoxLayout(heroCard); + heroLayout->setContentsMargins(20, 20, 20, 20); + heroLayout->setSpacing(14); + + auto* heroTop = new QHBoxLayout(); + heroTop->setSpacing(14); + m_cockpitIconLabel = new QLabel(heroCard); + m_cockpitIconLabel->setFixedSize(52, 52); + m_cockpitIconLabel->setAlignment(Qt::AlignCenter); + + auto* heroText = new QVBoxLayout(); + heroText->setSpacing(6); + m_cockpitBadgeLabel = new QLabel(heroCard); + m_cockpitBadgeLabel->setObjectName("hubBadge"); + m_cockpitBadgeLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + m_cockpitTitleLabel = new QLabel(heroCard); + m_cockpitTitleLabel->setObjectName("hubHeroTitle"); + m_cockpitTitleLabel->setWordWrap(true); + m_cockpitSubtitleLabel = new QLabel(heroCard); + m_cockpitSubtitleLabel->setObjectName("hubHeroSubtitle"); + m_cockpitSubtitleLabel->setWordWrap(true); + heroText->addWidget(m_cockpitBadgeLabel, 0, Qt::AlignLeft); + heroText->addWidget(m_cockpitTitleLabel); + heroText->addWidget(m_cockpitSubtitleLabel); + + heroTop->addWidget(m_cockpitIconLabel, 0, Qt::AlignTop); + heroTop->addLayout(heroText, 1); + heroLayout->addLayout(heroTop); + + auto* heroActions = new QHBoxLayout(); + heroActions->setSpacing(10); + m_playButton = new QPushButton(tr("Play"), heroCard); + m_playButton->setObjectName("hubPrimaryButton"); + m_editButton = new QPushButton(tr("Edit"), heroCard); + m_editButton->setObjectName("hubSecondaryButton"); + m_backupsButton = new QPushButton(tr("Backups"), heroCard); + m_backupsButton->setObjectName("hubSecondaryButton"); + m_folderButton = new QPushButton(tr("Open Folder"), heroCard); + m_folderButton->setObjectName("hubSecondaryButton"); + heroActions->addWidget(m_playButton); + heroActions->addWidget(m_editButton); + heroActions->addWidget(m_backupsButton); + heroActions->addWidget(m_folderButton); + heroActions->addStretch(1); + heroLayout->addLayout(heroActions); + pageLayout->addWidget(heroCard); + + auto* metricsLayout = new QGridLayout(); + metricsLayout->setHorizontalSpacing(12); + metricsLayout->setVerticalSpacing(12); + metricsLayout->setColumnStretch(0, 1); + metricsLayout->setColumnStretch(1, 1); + metricsLayout->setColumnStretch(2, 1); + auto makeMetricCard = [content](const QString& title, QLabel*& valueLabel, QLabel*& detailLabel) + { + auto* card = new QFrame(content); + card->setObjectName("hubMetricCard"); + auto* cardLayout = new QVBoxLayout(card); + cardLayout->setContentsMargins(16, 16, 16, 16); + cardLayout->setSpacing(6); + + auto* titleLabel = new QLabel(title, card); + titleLabel->setObjectName("hubPanelSubtitle"); + valueLabel = new QLabel(card); + valueLabel->setObjectName("hubMetricValue"); + detailLabel = new QLabel(card); + detailLabel->setObjectName("hubMetricDetail"); + detailLabel->setWordWrap(true); + + cardLayout->addWidget(titleLabel); + cardLayout->addWidget(valueLabel); + cardLayout->addWidget(detailLabel); + return card; + }; + + metricsLayout->addWidget(makeMetricCard(tr("Instances"), m_instancesValueLabel, m_instancesDetailLabel), 0, 0); + metricsLayout->addWidget(makeMetricCard(tr("Total Playtime"), m_playtimeValueLabel, m_playtimeDetailLabel), 0, 1); + metricsLayout->addWidget(makeMetricCard(tr("Needs Attention"), m_attentionValueLabel, m_attentionDetailLabel), + 0, + 2); + pageLayout->addLayout(metricsLayout); + + auto* lowerGrid = new QGridLayout(); + lowerGrid->setHorizontalSpacing(12); + lowerGrid->setVerticalSpacing(12); + lowerGrid->setColumnStretch(0, 1); + lowerGrid->setColumnStretch(1, 1); + + auto* recentPanel = new QFrame(content); + recentPanel->setObjectName("hubPanel"); + auto* recentPanelLayout = new QVBoxLayout(recentPanel); + recentPanelLayout->setContentsMargins(16, 16, 16, 16); + recentPanelLayout->setSpacing(10); + auto* recentTitle = new QLabel(tr("Continue Playing"), recentPanel); + recentTitle->setObjectName("hubPanelTitle"); + auto* recentSubtitle = new QLabel(tr("Jump back into your most recent worlds or packs."), recentPanel); + recentSubtitle->setObjectName("hubPanelSubtitle"); + recentSubtitle->setWordWrap(true); + recentPanelLayout->addWidget(recentTitle); + recentPanelLayout->addWidget(recentSubtitle); + m_recentInstancesLayout = new QVBoxLayout(); + m_recentInstancesLayout->setSpacing(8); + recentPanelLayout->addLayout(m_recentInstancesLayout); + recentPanelLayout->addStretch(1); + lowerGrid->addWidget(recentPanel, 0, 0); + + auto* newsPanel = new QFrame(content); + newsPanel->setObjectName("hubPanel"); + auto* newsPanelLayout = new QVBoxLayout(newsPanel); + newsPanelLayout->setContentsMargins(16, 16, 16, 16); + newsPanelLayout->setSpacing(10); + auto* newsTitle = new QLabel(tr("Community Pulse"), newsPanel); + newsTitle->setObjectName("hubPanelTitle"); + auto* newsSubtitle = new QLabel(tr("Latest launcher news without leaving the cockpit."), newsPanel); + newsSubtitle->setObjectName("hubPanelSubtitle"); + newsSubtitle->setWordWrap(true); + newsPanelLayout->addWidget(newsTitle); + newsPanelLayout->addWidget(newsSubtitle); + m_newsLayout = new QVBoxLayout(); + m_newsLayout->setSpacing(8); + newsPanelLayout->addLayout(m_newsLayout); + newsPanelLayout->addStretch(1); + lowerGrid->addWidget(newsPanel, 0, 1); + + auto* linksPanel = new QFrame(content); + linksPanel->setObjectName("hubPanel"); + auto* linksLayout = new QVBoxLayout(linksPanel); + linksLayout->setContentsMargins(16, 16, 16, 16); + linksLayout->setSpacing(10); + auto* linksTitle = new QLabel(tr("Quick Routes"), linksPanel); + linksTitle->setObjectName("hubPanelTitle"); + auto* linksSubtitle = new QLabel(tr("Open the spaces you reach for most while you play."), linksPanel); + linksSubtitle->setObjectName("hubPanelSubtitle"); + linksSubtitle->setWordWrap(true); + linksLayout->addWidget(linksTitle); + linksLayout->addWidget(linksSubtitle); + + auto addLinkButton = [this, linksPanel, linksLayout](const QString& label, const QUrl& url) + { + auto* button = new QPushButton(label, linksPanel); + button->setObjectName("hubSecondaryButton"); + connect(button, &QPushButton::clicked, this, [this, url]() { openUrl(url); }); + linksLayout->addWidget(button); + }; + + addLinkButton(tr("Open website"), m_homeUrl); + addLinkButton(tr("Read news"), QUrl(BuildConfig.NEWS_OPEN_URL)); + if (!BuildConfig.HUB_COMMUNITY_URL.isEmpty()) + { + addLinkButton(tr("Open community"), QUrl(BuildConfig.HUB_COMMUNITY_URL)); + } + addLinkButton(tr("Open help"), QUrl(BuildConfig.HELP_URL.arg(""))); + lowerGrid->addWidget(linksPanel, 1, 0, 1, 2); + + pageLayout->addLayout(lowerGrid); + pageLayout->addStretch(1); + + scrollArea->setWidget(content); + m_cockpitPage = scrollArea; + m_stack->addWidget(m_cockpitPage); + m_stack->setCurrentWidget(m_cockpitPage); + + connect(m_playButton, + &QPushButton::clicked, + this, + [this]() + { + const QString instanceId = activeInstanceId(); + if (!instanceId.isEmpty()) + { + emit launchInstanceRequested(instanceId); + } + }); + connect(m_editButton, + &QPushButton::clicked, + this, + [this]() + { + const QString instanceId = activeInstanceId(); + if (!instanceId.isEmpty()) + { + emit editInstanceRequested(instanceId); + } + }); + connect(m_backupsButton, + &QPushButton::clicked, + this, + [this]() + { + const QString instanceId = activeInstanceId(); + if (!instanceId.isEmpty()) + { + emit backupsRequested(instanceId); + } + }); + connect(m_folderButton, + &QPushButton::clicked, + this, + [this]() + { + const QString instanceId = activeInstanceId(); + if (!instanceId.isEmpty()) + { + emit openInstanceFolderRequested(instanceId); + } + }); +} + +void LauncherHubWidget::changeEvent(QEvent* event) +{ + QWidget::changeEvent(event); + if (!event) + { + return; + } + + if (event->type() == QEvent::PaletteChange || event->type() == QEvent::ApplicationPaletteChange) + { + refreshToolbarIcons(); + updateHero(); + } +} + +HubViewBase* LauncherHubWidget::createTab(const QUrl& url, const QString& label, bool switchTo) +{ + if (!m_stack || !m_tabBar) + { + return nullptr; + } + + auto* view = createBrowserView(m_stack); + + QWidget* previousPage = m_stack->currentWidget(); + const int previousTabIndex = m_tabBar->currentIndex(); + + m_stack->addWidget(view); + const QString initialLabel = label.isEmpty() ? tr("New Tab") : label; + int tabIndex = -1; + if (switchTo) + { + tabIndex = m_tabBar->addTab(initialLabel); + } + else + { + const QSignalBlocker blocker(m_tabBar); + tabIndex = m_tabBar->addTab(initialLabel); + m_tabBar->setCurrentIndex(previousTabIndex); + } + m_tabBar->setTabData(tabIndex, QVariant::fromValue(static_cast<QObject*>(view))); + + auto updateTitle = [this, view](const QString& title) + { + const int index = tabIndexForView(view); + if (index >= 0 && !title.isEmpty()) + { + m_tabBar->setTabText(index, title); + } + }; + + connect(view, &HubViewBase::titleChanged, this, updateTitle); + connect(view, + &HubViewBase::urlChanged, + this, + [this, view](const QUrl& urlChanged) + { + if (view == currentView()) + { + m_addressBar->setText(urlChanged.toString()); + updateNavigationState(); + } + }); + connect(view, + &HubViewBase::loadFinished, + this, + [this, view](bool) + { + if (view == currentView()) + { + updateNavigationState(); + } + }); + connect(view, &HubViewBase::navigationStateChanged, this, &LauncherHubWidget::updateNavigationState); + connect(view, + &HubViewBase::newTabRequested, + this, + [this](const QUrl& requestedUrl) + { + if (!requestedUrl.isValid()) + { + return; + } + + createTab(requestedUrl, QString(), true); + }); + + if (switchTo) + { + m_tabBar->setCurrentIndex(tabIndex); + m_stack->setCurrentWidget(view); + } + else if (previousPage) + { + m_stack->setCurrentWidget(previousPage); + } + + if (url.isValid()) + { + const bool shouldLoadNow = switchTo; + if (shouldLoadNow) + { + view->setUrl(url); + } + else + { + view->setProperty("hubPendingUrl", url); + } + } + + syncTabsUi(); + updateTabPerformanceState(); + return view; +} + +void LauncherHubWidget::switchToPage(QWidget* page) +{ + if (!m_stack || !m_tabBar || !page) + { + return; + } + + m_stack->setCurrentWidget(page); + if (page == m_cockpitPage) + { + updateTabPerformanceState(); + updateNavigationState(); + syncTabsUi(); + return; + } + + if (auto* view = qobject_cast<HubViewBase*>(page)) + { + const int index = tabIndexForView(view); + if (index >= 0) + { + m_tabBar->setCurrentIndex(index); + } + activatePendingForPage(view); + } + + updateTabPerformanceState(); + updateNavigationState(); + syncTabsUi(); +} + +void LauncherHubWidget::activatePendingForPage(QWidget* page) +{ + if (!page) + { + return; + } + if (auto* view = qobject_cast<HubViewBase*>(page)) + { + const QUrl pendingUrl = view->property("hubPendingUrl").toUrl(); + if (pendingUrl.isValid()) + { + view->setProperty("hubPendingUrl", QUrl()); + view->setUrl(pendingUrl); + } + } +} + +void LauncherHubWidget::updateNavigationState() +{ + auto* view = currentView(); + if (!view) + { + m_backButton->setEnabled(false); + m_forwardButton->setEnabled(false); + m_goButton->setEnabled(false); + m_addressBar->clear(); + m_addressBar->setEnabled(false); + m_addressBar->setPlaceholderText(tr("Launcher Hub Cockpit")); + return; + } + + m_goButton->setEnabled(true); + m_addressBar->setEnabled(true); + m_addressBar->setPlaceholderText(tr("Search or enter address")); + m_backButton->setEnabled(view->canGoBack()); + m_forwardButton->setEnabled(view->canGoForward()); + m_addressBar->setText(view->url().toString()); +} + +void LauncherHubWidget::syncTabsUi() +{ + if (m_tabsBarContainer && m_tabBar) + { + m_tabsBarContainer->setVisible(m_tabBar->count() > 0); + } +} + +void LauncherHubWidget::refreshToolbarIcons() +{ + if (m_backButton) + { + m_backButton->setIcon(tintedIcon(QStringLiteral("go-previous"), this)); + } + if (m_forwardButton) + { + m_forwardButton->setIcon(tintedIcon(QStringLiteral("go-next"), this)); + } + if (m_reloadButton) + { + m_reloadButton->setIcon(tintedIcon(QStringLiteral("view-refresh"), this)); + } + if (m_homeButton) + { + m_homeButton->setIcon(tintedIcon(QStringLiteral("go-home"), this)); + } + if (m_newTabButton) + { + m_newTabButton->setIcon(tintedIcon(QStringLiteral("list-add"), this)); + } + if (m_goButton) + { + m_goButton->setIcon(tintedIcon(QStringLiteral("system-search"), this)); + } +} + +void LauncherHubWidget::updateTabPerformanceState() +{ +#if defined(PROJT_USE_WEBENGINE) + if (!m_stack) + { + return; + } + + const int activeIndex = m_stack->currentIndex(); + for (int i = 0; i < m_stack->count(); ++i) + { + auto* view = qobject_cast<HubViewBase*>(m_stack->widget(i)); + if (!view) + { + continue; + } + view->setActive(i == activeIndex); + } +#endif +} + +void LauncherHubWidget::ensureLoaded() +{ + loadHome(); + m_loaded = true; +} + +void LauncherHubWidget::loadHome() +{ + refreshCockpit(); + switchToPage(m_cockpitPage); +} + +void LauncherHubWidget::newTab(const QUrl& url) +{ + createTab(url.isValid() ? url : m_homeUrl, QString(), true); + m_loaded = true; +} + +void LauncherHubWidget::openUrl(const QUrl& url) +{ + if (!url.isValid()) + { + return; + } + + auto* view = currentView(); + if (!view) + { + createTab(url, QString(), true); + updateTabPerformanceState(); + updateNavigationState(); + m_loaded = true; + return; + } + + view->setUrl(url); + updateTabPerformanceState(); + m_loaded = true; +} + +void LauncherHubWidget::setHomeUrl(const QUrl& url) +{ + m_homeUrl = url; + m_loaded = false; +} + +QUrl LauncherHubWidget::homeUrl() const +{ + return m_homeUrl; +} + +void LauncherHubWidget::setSelectedInstanceId(const QString& id) +{ + m_selectedInstanceId = id; + refreshCockpit(); +} + +QString LauncherHubWidget::activeInstanceId() const +{ + if (!m_selectedInstanceId.isEmpty()) + { + return m_selectedInstanceId; + } + + if (APPLICATION->settings()) + { + const QString selected = APPLICATION->settings()->get("SelectedInstance").toString(); + if (!selected.isEmpty()) + { + return selected; + } + } + + const QList<InstancePtr> instances = sortedInstances(); + if (!instances.isEmpty()) + { + return instances.first()->id(); + } + return {}; +} + +void LauncherHubWidget::refreshCockpit() +{ + if (!m_cockpitPage) + { + return; + } + + if (m_newsChecker && !m_newsChecker->isLoadingNews() && m_newsChecker->getNewsEntries().isEmpty() + && m_newsChecker->getLastLoadErrorMsg().isEmpty()) + { + m_newsChecker->reloadNews(); + } + + const QList<InstancePtr> instances = sortedInstances(); + int managedCount = 0; + int attentionCount = 0; + for (const auto& instance : instances) + { + if (instance->isManagedPack()) + { + managedCount++; + } + if (instance->hasUpdateAvailable() || instance->hasCrashed() || instance->hasVersionBroken()) + { + attentionCount++; + } + } + + if (m_instancesValueLabel) + { + m_instancesValueLabel->setText(QString::number(instances.size())); + } + if (m_instancesDetailLabel) + { + m_instancesDetailLabel->setText(instances.isEmpty() ? tr("No instances yet") + : tr("%1 managed pack(s) in rotation").arg(managedCount)); + } + + const int totalPlaytime = APPLICATION->instances() ? APPLICATION->instances()->getTotalPlayTime() : 0; + if (m_playtimeValueLabel) + { + m_playtimeValueLabel->setText( + totalPlaytime > 0 ? Time::prettifyDuration(totalPlaytime, + APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()) + : tr("0m")); + } + if (m_playtimeDetailLabel) + { + m_playtimeDetailLabel->setText(tr("Your full launcher history across every instance.")); + } + if (m_attentionValueLabel) + { + m_attentionValueLabel->setText(QString::number(attentionCount)); + } + if (m_attentionDetailLabel) + { + m_attentionDetailLabel->setText(attentionCount > 0 ? tr("Updates, crashes, or broken versions to review.") + : tr("Everything looks healthy right now.")); + } + + updateHero(); + rebuildRecentInstances(); + rebuildNewsFeed(); +} + +void LauncherHubWidget::updateHero() +{ + const QString instanceId = activeInstanceId(); + const InstancePtr instance = + APPLICATION->instances() ? APPLICATION->instances()->getInstanceById(instanceId) : nullptr; + + if (!instance) + { + m_cockpitBadgeLabel->setText(tr("Cockpit")); + m_cockpitTitleLabel->setText(tr("Launcher Hub is ready")); + m_cockpitSubtitleLabel->setText(tr("Open news, community pages, and help from one place. Once you create or " + "select an instance, it will appear here.")); + m_cockpitIconLabel->setPixmap(APPLICATION->logo().pixmap(40, 40)); + m_playButton->setEnabled(false); + m_editButton->setEnabled(false); + m_backupsButton->setEnabled(false); + m_folderButton->setEnabled(false); + return; + } + + m_cockpitBadgeLabel->setText(heroBadgeForInstance(instance)); + m_cockpitTitleLabel->setText(instance->name()); + + QString subtitle = instance->getStatusbarDescription(); + const QString lastLaunchText = relativeTimeLabel(instance->lastLaunch()); + if (!subtitle.isEmpty()) + { + subtitle += tr(" | Last launch: %1").arg(lastLaunchText); + } + else + { + subtitle = tr("Last launch: %1").arg(lastLaunchText); + } + m_cockpitSubtitleLabel->setText(subtitle); + m_cockpitIconLabel->setPixmap(APPLICATION->icons()->getIcon(instance->iconKey()).pixmap(40, 40)); + m_playButton->setEnabled(instance->canLaunch() && !instance->isRunning()); + m_editButton->setEnabled(instance->canEdit()); + m_backupsButton->setEnabled(true); + m_folderButton->setEnabled(true); +} + +void LauncherHubWidget::rebuildRecentInstances() +{ + clearLayout(m_recentInstancesLayout); + if (!m_recentInstancesLayout) + { + return; + } + + const QList<InstancePtr> instances = sortedInstances(); + if (instances.isEmpty()) + { + auto* label = + new QLabel(tr("No instances yet. Your recent worlds and packs will show up here."), m_cockpitPage); + label->setObjectName("hubPanelSubtitle"); + label->setWordWrap(true); + m_recentInstancesLayout->addWidget(label); + return; + } + + const QString currentId = activeInstanceId(); + const int limit = qMin(6, instances.size()); + for (int i = 0; i < limit; ++i) + { + const auto& instance = instances.at(i); + auto* row = new QWidget(m_cockpitPage); + auto* rowLayout = new QHBoxLayout(row); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(8); + + auto* button = + new QPushButton(QStringLiteral("%1\n%2").arg( + instance->name(), + tr("%1 | %2").arg(instance->typeName(), relativeTimeLabel(instance->lastLaunch()))), + row); + button->setObjectName("hubQuickButton"); + button->setProperty("active", instance->id() == currentId); + button->setIcon(APPLICATION->icons()->getIcon(instance->iconKey())); + button->setIconSize(QSize(28, 28)); + button->setMinimumHeight(56); + connect(button, + &QPushButton::clicked, + this, + [this, instance]() + { + m_selectedInstanceId = instance->id(); + emit selectInstanceRequested(instance->id()); + refreshCockpit(); + }); + + auto* launchButton = new QPushButton(tr("Play"), row); + launchButton->setObjectName("hubInlineAction"); + launchButton->setEnabled(instance->canLaunch() && !instance->isRunning()); + connect(launchButton, + &QPushButton::clicked, + this, + [this, instance]() + { + m_selectedInstanceId = instance->id(); + emit launchInstanceRequested(instance->id()); + refreshCockpit(); + }); + + rowLayout->addWidget(button, 1); + rowLayout->addWidget(launchButton); + m_recentInstancesLayout->addWidget(row); + } +} + +void LauncherHubWidget::rebuildNewsFeed() +{ + clearLayout(m_newsLayout); + if (!m_newsLayout || !m_newsChecker) + { + return; + } + + const QList<NewsEntryPtr> entries = m_newsChecker->getNewsEntries(); + if (entries.isEmpty()) + { + auto* label = new QLabel(m_newsChecker->isLoadingNews() + ? tr("Loading the latest posts...") + : tr("News is quiet right now. Use the button below to open the full feed."), + m_cockpitPage); + label->setObjectName("hubPanelSubtitle"); + label->setWordWrap(true); + m_newsLayout->addWidget(label); + } + else + { + const int limit = qMin(3, entries.size()); + for (int i = 0; i < limit; ++i) + { + const auto& entry = entries.at(i); + auto* button = new QPushButton(QStringLiteral("%1\n%2").arg(entry->title, stripHtmlExcerpt(entry->content)), + m_cockpitPage); + button->setObjectName("hubNewsButton"); + button->setMinimumHeight(66); + connect(button, + &QPushButton::clicked, + this, + [this, entry]() + { openUrl(QUrl(entry->link.isEmpty() ? BuildConfig.NEWS_OPEN_URL : entry->link)); }); + m_newsLayout->addWidget(button); + } + } + + auto* openFeedButton = new QPushButton(tr("Open full news feed"), m_cockpitPage); + openFeedButton->setObjectName("hubInlineAction"); + connect(openFeedButton, &QPushButton::clicked, this, [this]() { openUrl(QUrl(BuildConfig.NEWS_OPEN_URL)); }); + m_newsLayout->addWidget(openFeedButton, 0, Qt::AlignLeft); +} + +#endif // PROJT_DISABLE_LAUNCHER_HUB |
