Files
qemby/src/qEmbyApp/mainwindow.cpp
T

1211 lines
42 KiB
C++

#include "mainwindow.h"
#include "tokens/AnimationTokens.h"
#include <qembycore.h>
#include "views/public/loginview.h"
#include "views/media/playerview.h"
#include "views/user/homeview.h"
#include "components/searchcompleterpopup.h"
#include "components/searchhistorypopup.h"
#include "managers/thememanager.h"
#include "managers/searchhistorymanager.h"
#include "managers/traymanager.h"
#include "config/configstore.h"
#include "config/config_keys.h"
#include "utils/contextmenuutils.h"
#include <services/manager/servermanager.h>
#include <QStackedWidget>
#include <QDebug>
#include <QApplication>
#include <QGuiApplication>
#include <QAbstractItemView>
#include <QFile>
#include <QTimer>
#include <QElapsedTimer>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QAction>
#include <QCompleter>
#include <QFrame>
#include <QStringListModel>
#include <QPropertyAnimation>
#include <QScreen>
#include <QGraphicsOpacityEffect>
#include <services/auth/authservice.h>
#include "components/themetransitionwidget.h"
#include "components/moderntoast.h"
#include <QEasingCurve>
#include <QFocusEvent>
#include <QLineF>
#include <QtMath>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QDialog>
#include <QWKWidgets/widgetwindowagent.h>
#include <widgetframe/windowbar.h>
#include <widgetframe/windowbutton.h>
static inline void emulateLeaveEvent(QWidget *widget) {
Q_ASSERT(widget);
if (!widget) return;
QTimer::singleShot(0, widget, [widget]() {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
const QScreen *screen = widget->screen();
#else
const QScreen *screen = widget->windowHandle()->screen();
#endif
const QPoint globalPos = QCursor::pos(screen);
if (!QRect(widget->mapToGlobal(QPoint{0, 0}), widget->size()).contains(globalPos)) {
QCoreApplication::postEvent(widget, new QEvent(QEvent::Leave));
if (widget->testAttribute(Qt::WA_Hover)) {
const QPoint localPos = widget->mapFromGlobal(globalPos);
const QPoint scenePos = widget->window()->mapFromGlobal(globalPos);
static constexpr const auto oldPos = QPoint{};
const Qt::KeyboardModifiers modifiers = QGuiApplication::keyboardModifiers();
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
const auto event = new QHoverEvent(QEvent::HoverLeave, scenePos, globalPos, oldPos, modifiers);
Q_UNUSED(localPos);
#elif (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
const auto event = new QHoverEvent(QEvent::HoverLeave, localPos, globalPos, oldPos, modifiers);
Q_UNUSED(scenePos);
#else
const auto event = new QHoverEvent(QEvent::HoverLeave, localPos, oldPos, modifiers);
Q_UNUSED(scenePos);
#endif
QCoreApplication::postEvent(widget, event);
}
}
});
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
m_core = new QEmbyCore(this);
m_trayManager = new TrayManager(this);
connect(m_trayManager, &TrayManager::showMainRequested, this, [this]() {
showNormal();
activateWindow();
if (m_wasPausedByTray) {
m_wasPausedByTray = false;
QTimer::singleShot(500, this, [this]() {
if (auto *player = m_homeView->activePlayerView()) {
player->resumePlayback();
}
});
}
});
connect(m_trayManager, &TrayManager::quitRequested, this,
&MainWindow::requestApplicationQuit);
m_backClickTimer.invalidate();
#if (defined(Q_OS_MACOS) || defined(Q_OS_MAC)) && \
(QT_VERSION >= QT_VERSION_CHECK(6, 9, 0))
setWindowFlag(Qt::ExpandedClientAreaHint, true);
setWindowFlag(Qt::NoTitleBarBackgroundHint, true);
setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea, false);
#endif
auto agent = new QWK::WidgetWindowAgent(this);
agent->setup(this);
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
agent->setWindowAttribute("no-system-buttons", false);
#endif
QPalette pal = palette();
pal.setColor(QPalette::Window, QColor(241, 245, 249));
setPalette(pal);
setAutoFillBackground(true);
auto titleLabel = new QLabel(qApp->applicationName());
titleLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
titleLabel->setObjectName(QStringLiteral("win-title-label"));
titleLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
titleLabel->setContentsMargins(0, 5, 0, 0);
auto iconButton = new QWK::WindowButton();
iconButton->setObjectName(QStringLiteral("icon-button"));
iconButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
iconButton->setIconNormal(qApp->windowIcon());
iconButton->setIconSize(QSize(16, 16));
auto themeButton = new QWK::WindowButton();
themeButton->setCheckable(true);
themeButton->setObjectName(QStringLiteral("theme-button"));
themeButton->setProperty("system-button", true);
themeButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
themeButton->setIconNormal(QIcon(":/svg/moon.svg"));
themeButton->setIconSize(QSize(15, 15));
themeButton->setChecked(true);
auto minButton = new QWK::WindowButton();
minButton->setObjectName(QStringLiteral("min-button"));
minButton->setProperty("system-button", true);
minButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
auto maxButton = new QWK::WindowButton();
maxButton->setCheckable(true);
maxButton->setObjectName(QStringLiteral("max-button"));
maxButton->setProperty("system-button", true);
maxButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
auto closeButton = new QWK::WindowButton();
closeButton->setObjectName(QStringLiteral("close-button"));
closeButton->setProperty("system-button", true);
closeButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
auto backButton = new QWK::WindowButton();
backButton->setObjectName(QStringLiteral("back-button"));
backButton->setProperty("system-button", true);
backButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
backButton->setShortcut(QKeySequence::Back);
backButton->hide();
auto homeButton = new QWK::WindowButton();
homeButton->setObjectName(QStringLiteral("home-button"));
homeButton->setProperty("system-button", true);
homeButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
homeButton->hide();
auto favButton = new QWK::WindowButton();
favButton->setObjectName(QStringLiteral("fav-button"));
favButton->setProperty("system-button", true);
favButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
favButton->hide();
connect(backButton, &QWK::WindowButton::clicked, this, [this]() {
if (m_viewStack->currentWidget() == m_homeView) {
if (m_homeView->canNavigateBack()) {
m_homeView->navigateBack();
m_backClickTimer.invalidate();
} else if (m_homeView->canGoHome()) {
m_homeView->goHome();
m_backClickTimer.invalidate();
} else {
if (m_backClickTimer.isValid() && m_backClickTimer.elapsed() < 2000) {
m_backClickTimer.invalidate();
navigateToLogin();
} else {
m_backClickTimer.start();
ModernToast::showMessage(tr("Press Back again to logout"));
}
}
}
});
connect(homeButton, &QWK::WindowButton::clicked, this, [this]() {
if (m_viewStack->currentWidget() == m_homeView) {
if (!m_homeView->canGoHome()) {
ModernToast::showMessage(tr("Refreshing Home..."), 1000);
}
m_homeView->goHome();
}
});
connect(favButton, &QWK::WindowButton::clicked, this, [this]() {
if (m_viewStack->currentWidget() == m_homeView) {
if (!m_homeView->canGoFav()) {
ModernToast::showMessage(tr("Refreshing Favorites..."), 1000);
}
m_homeView->goFav();
}
});
auto windowBar = new QWK::WindowBar();
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
windowBar->layout()->setContentsMargins(80, 0, 0, 0);
#endif
windowBar->setIconButton(iconButton);
windowBar->setPinButton(themeButton);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
windowBar->setMinButton(minButton);
windowBar->setMaxButton(maxButton);
windowBar->setCloseButton(closeButton);
#else
minButton->setParent(windowBar);
maxButton->setParent(windowBar);
closeButton->setParent(windowBar);
minButton->hide();
maxButton->hide();
closeButton->hide();
#endif
windowBar->setTitleLabel(titleLabel);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
windowBar->setBackButton(backButton);
windowBar->setHomeButton(homeButton);
windowBar->setFavButton(favButton);
#endif
titleLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
windowBar->setHostWidget(this);
auto *centerContainer = new QWidget(windowBar);
auto *centerLayout = new QHBoxLayout(centerContainer);
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
centerLayout->setContentsMargins(0, 1, 0, 1);
#else
centerLayout->setContentsMargins(0, 0, 0, 0);
#endif
centerLayout->setSpacing(0);
auto *globalSearchBox = new QLineEdit(centerContainer);
m_globalSearchBox = globalSearchBox;
globalSearchBox->setObjectName("titlebar-search");
globalSearchBox->setPlaceholderText(tr("Search..."));
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
globalSearchBox->setFixedSize(380, 30);
#else
globalSearchBox->setFixedSize(380, 32);
#endif
globalSearchBox->setClearButtonEnabled(true);
auto *searchAction = new QAction(QIcon(":/svg/light/search.svg"), tr("Search"), this);
globalSearchBox->addAction(searchAction, QLineEdit::LeadingPosition);
globalSearchBox->hide();
auto *macCenteredTitleLabel = new QLabel(centerContainer);
macCenteredTitleLabel->setObjectName("mac-title-label");
macCenteredTitleLabel->setAlignment(Qt::AlignCenter);
macCenteredTitleLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
macCenteredTitleLabel->hide();
connect(globalSearchBox, &QLineEdit::returnPressed, this, [this]() {
if (!m_globalSearchBox) {
qWarning() << "Global search submit ignored: search box was destroyed";
return;
}
submitGlobalSearch(m_globalSearchBox->text());
});
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
auto *macTitlebarNavSpacer = new QWidget(centerContainer);
macTitlebarNavSpacer->setObjectName("mac-titlebar-nav-spacer");
macTitlebarNavSpacer->setFixedWidth(150);
macTitlebarNavSpacer->hide();
auto *macTitlebarNav = new QWidget(centerContainer);
macTitlebarNav->setObjectName("mac-titlebar-nav");
auto *macTitlebarNavLayout = new QHBoxLayout(macTitlebarNav);
macTitlebarNavLayout->setContentsMargins(0, 0, 0, 0);
macTitlebarNavLayout->setSpacing(0);
macTitlebarNavLayout->addWidget(backButton);
macTitlebarNavLayout->addWidget(homeButton);
macTitlebarNavLayout->addWidget(favButton);
macTitlebarNav->setFixedWidth(150);
macTitlebarNav->hide();
centerLayout->addWidget(macTitlebarNavSpacer);
#endif
centerLayout->addStretch();
centerLayout->addWidget(globalSearchBox, 0, Qt::AlignVCenter);
centerLayout->addWidget(macCenteredTitleLabel, 0, Qt::AlignVCenter);
centerLayout->addStretch();
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
centerLayout->addWidget(macTitlebarNav);
#endif
centerContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
windowBar->setCenterWidget(centerContainer);
agent->setTitleBar(windowBar);
agent->setHitTestVisible(themeButton, true);
agent->setHitTestVisible(backButton, true);
agent->setHitTestVisible(homeButton, true);
agent->setHitTestVisible(favButton, true);
agent->setHitTestVisible(globalSearchBox, true);
agent->setSystemButton(QWK::WindowAgentBase::WindowIcon, iconButton);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
agent->setSystemButton(QWK::WindowAgentBase::Minimize, minButton);
agent->setSystemButton(QWK::WindowAgentBase::Maximize, maxButton);
agent->setSystemButton(QWK::WindowAgentBase::Close, closeButton);
#endif
#if defined(Q_OS_MAC)
agent->setSystemButtonAreaCallback([](const QSize &size) {
return QRect(QPoint(0, 0), QSize(80, size.height()));
});
#endif
auto *mainContainer = new QWidget(this);
mainContainer->setObjectName(QStringLiteral("main-window-content"));
auto *mainLayout = new QVBoxLayout(mainContainer);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
mainLayout->addWidget(windowBar, 0);
m_viewStack = new QStackedWidget(mainContainer);
m_viewStack->setObjectName("main-view-stack");
m_viewStack->setPalette(pal);
m_viewStack->setAutoFillBackground(true);
mainLayout->addWidget(m_viewStack, 1);
setCentralWidget(mainContainer);
m_loginView = new LoginView(m_core, this);
m_homeView = new HomeView(m_core, this);
setupGlobalSearchHistory();
m_viewStack->addWidget(m_loginView);
m_viewStack->addWidget(m_homeView);
connect(m_viewStack, &QStackedWidget::currentChanged, this, [this](int index) {
QWidget *currentView = m_viewStack->widget(index);
if (!currentView) return;
bool showSearch = currentView->property("showGlobalSearch").toBool();
if (m_globalSearchBox) {
m_globalSearchBox->setVisible(showSearch);
} else {
qWarning() << "Global search box missing during view change"
<< "index:" << index
<< "view:" << currentView;
}
if (!showSearch) {
hideGlobalSearchTransientUi();
}
auto back = findChild<QWK::WindowButton*>("back-button");
auto icon = findChild<QWK::WindowButton*>("icon-button");
auto home = findChild<QWK::WindowButton*>("home-button");
auto fav = findChild<QWK::WindowButton*>("fav-button");
auto title = findChild<QLabel*>("win-title-label");
auto macTitle = findChild<QLabel*>("mac-title-label");
auto macNav = findChild<QWidget*>("mac-titlebar-nav");
auto macNavSpacer = findChild<QWidget*>("mac-titlebar-nav-spacer");
if (currentView == m_homeView) {
if (back) {
back->setVisible(true);
back->setEnabled(true);
}
if (icon) icon->setVisible(false);
if (home)
{
home->setVisible(true);
home->setEnabled(true);
}
if (fav)
{
fav->setVisible(true);
fav->setEnabled(true);
}
} else {
bool showBack = currentView->property("showGlobalBack").toBool();
if (back) {
back->setVisible(showBack);
back->setEnabled(true);
}
if (icon) icon->setVisible(!showBack);
bool showHome = currentView->property("showGlobalHome").toBool();
if (home)
{
home->setVisible(showHome);
home->setEnabled(true);
}
bool showFav = currentView->property("showGlobalFav").toBool();
if (fav)
{
fav->setVisible(showFav);
fav->setEnabled(true);
}
}
QVariant customTitle = currentView->property("viewTitle");
if (customTitle.isValid()) {
if (title) {
title->setText(customTitle.toString());
}
if (macTitle) {
macTitle->setText(customTitle.toString());
}
}
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
const bool useCenteredLoginTitle = (currentView == m_loginView);
const bool showMacTitlebarNav = (currentView == m_homeView);
if (icon) {
icon->setVisible(false);
}
if (title) {
title->setVisible(false);
}
if (macTitle) {
macTitle->setVisible(useCenteredLoginTitle);
}
if (macNav) {
macNav->setVisible(showMacTitlebarNav);
}
if (macNavSpacer) {
macNavSpacer->setVisible(showMacTitlebarNav);
}
#else
if (macTitle) {
macTitle->hide();
}
#endif
});
connect(m_homeView, &HomeView::immersiveStateChanged, this, [windowBar, agent](bool isImmersive) {
windowBar->setVisible(!isImmersive);
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
if (!isImmersive) {
agent->setWindowAttribute("no-system-buttons", false);
}
#else
Q_UNUSED(agent);
#endif
});
connect(m_homeView, &HomeView::playerChromeVisibilityChanged, this,
[this, agent](bool visible) {
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
if (m_homeView && m_homeView->activePlayerView()) {
agent->setWindowAttribute("no-system-buttons", !visible);
}
#else
Q_UNUSED(visible);
Q_UNUSED(agent);
#endif
});
connect(m_homeView, &HomeView::canNavigateBackChanged, this, [this](bool canBack) {
});
connect(m_homeView, &HomeView::homeContentSwitched, this, [this] () {
});
connect(m_loginView, &LoginView::loginCompleted, this, &MainWindow::navigateToHome);
connect(m_homeView, &HomeView::logoutRequested, this, &MainWindow::navigateToLogin);
connect(windowBar, &QWK::WindowBar::pinRequested, this, [this, themeButton](bool checked) {
themeButton->setChecked(checked);
hideGlobalSearchTransientUi();
QString newMode = checked ? "dark" : "light";
themeButton->blockSignals(true);
if (checked) {
themeButton->setIconNormal(QIcon(":/svg/moon.svg"));
} else {
themeButton->setIconNormal(QIcon(":/svg/sun.svg"));
}
themeButton->blockSignals(false);
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
if (reduceAnimations) {
m_themeAnimating = true;
ConfigStore::instance()->set(ConfigKeys::ThemeMode, newMode);
m_themeAnimating = false;
return;
}
QPixmap snapshot = this->grab();
QPoint center = themeButton->mapTo(this, themeButton->rect().center());
auto transition = new ThemeTransitionWidget(snapshot, center, this);
transition->setGeometry(this->rect());
transition->raise();
transition->show();
int maxRadius = qMax(
qMax(QLineF(center, QPoint(0, 0)).length(), QLineF(center, QPoint(width(), 0)).length()),
qMax(QLineF(center, QPoint(0, height())).length(), QLineF(center, QPoint(width(), height())).length())
) + 10;
m_themeAnimating = true;
ConfigStore::instance()->set(ConfigKeys::ThemeMode, newMode);
m_themeAnimating = false;
auto anim = new QPropertyAnimation(transition, "radius", transition);
anim->setDuration(AnimationTokens::DurationMs::kSlow);
anim->setStartValue(0);
anim->setEndValue(maxRadius);
anim->setEasingCurve(AnimationTokens::Curve::enter());
connect(anim, &QPropertyAnimation::finished, transition, &QObject::deleteLater);
anim->start(QAbstractAnimation::DeleteWhenStopped);
});
connect(windowBar, &QWK::WindowBar::minimizeRequested, this, [this]() {
hideGlobalSearchTransientUi();
showMinimized();
});
connect(windowBar, &QWK::WindowBar::maximizeRequested, this, [this, maxButton](bool max) {
if (max) {
showMaximized();
} else {
showNormal();
QRect screenGeo = screen()->availableGeometry();
int w = 1280, h = 800;
setGeometry(
screenGeo.x() + (screenGeo.width() - w) / 2,
screenGeo.y() + (screenGeo.height() - h) / 2,
w, h);
}
emulateLeaveEvent(maxButton);
});
connect(windowBar, &QWK::WindowBar::closeRequested, this, &QWidget::close);
QString savedThemeMode = ConfigStore::instance()->get<QString>(ConfigKeys::ThemeMode, "system");
ThemeManager::instance()->applyThemeMode(savedThemeMode);
bool isDark = ThemeManager::instance()->isDarkMode();
themeButton->setChecked(isDark);
themeButton->setIconNormal(isDark ? QIcon(":/svg/moon.svg") : QIcon(":/svg/sun.svg"));
connect(ConfigStore::instance(), &ConfigStore::valueChanged, this,
[this, themeButton](const QString &key, const QVariant &newValue) {
if (key == ConfigKeys::FontSize) {
ThemeManager::instance()->setTheme(ThemeManager::instance()->currentTheme());
return;
}
if (key != ConfigKeys::ThemeMode) return;
QString mode = newValue.toString();
hideGlobalSearchTransientUi();
if (m_themeAnimating) {
ThemeManager::instance()->applyThemeMode(mode);
bool dark = ThemeManager::instance()->isDarkMode();
themeButton->blockSignals(true);
themeButton->setChecked(dark);
themeButton->setIconNormal(dark ? QIcon(":/svg/moon.svg") : QIcon(":/svg/sun.svg"));
themeButton->blockSignals(false);
return;
}
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
if (!reduceAnimations) {
QPixmap snapshot = this->grab();
QPoint center = this->rect().center();
ThemeManager::instance()->applyThemeMode(mode);
bool dark = ThemeManager::instance()->isDarkMode();
themeButton->blockSignals(true);
themeButton->setChecked(dark);
themeButton->setIconNormal(dark ? QIcon(":/svg/moon.svg") : QIcon(":/svg/sun.svg"));
themeButton->blockSignals(false);
auto transition = new ThemeTransitionWidget(snapshot, center, this);
transition->setGeometry(this->rect());
transition->raise();
transition->show();
int maxRadius = qMax(
qMax(QLineF(center, QPoint(0, 0)).length(), QLineF(center, QPoint(width(), 0)).length()),
qMax(QLineF(center, QPoint(0, height())).length(), QLineF(center, QPoint(width(), height())).length())
) + 10;
auto anim = new QPropertyAnimation(transition, "radius", transition);
anim->setDuration(AnimationTokens::DurationMs::kSlow);
anim->setStartValue(0);
anim->setEndValue(maxRadius);
anim->setEasingCurve(AnimationTokens::Curve::enter());
connect(anim, &QPropertyAnimation::finished, transition, &QObject::deleteLater);
anim->start(QAbstractAnimation::DeleteWhenStopped);
} else {
ThemeManager::instance()->applyThemeMode(mode);
bool dark = ThemeManager::instance()->isDarkMode();
themeButton->blockSignals(true);
themeButton->setChecked(dark);
themeButton->setIconNormal(dark ? QIcon(":/svg/moon.svg") : QIcon(":/svg/sun.svg"));
themeButton->blockSignals(false);
}
});
minButton->hide();
maxButton->hide();
m_viewStack->setCurrentWidget(m_loginView);
#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
iconButton->hide();
titleLabel->hide();
macCenteredTitleLabel->setText(m_loginView->property("viewTitle").toString());
macCenteredTitleLabel->show();
if (macTitlebarNav) {
macTitlebarNav->hide();
}
if (macTitlebarNavSpacer) {
macTitlebarNavSpacer->hide();
}
#endif
setMinimumSize(900, 600);
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
const QString startupWindowState = ConfigStore::instance()->get<QString>(
ConfigKeys::StartupWindowState, "normal");
if (startupWindowState == "maximized") {
setWindowState(windowState() | Qt::WindowMaximized);
} else {
resize(m_defaultWidth, m_defaultHeight);
}
qApp->installEventFilter(this);
connect(qApp, &QGuiApplication::applicationStateChanged, this,
[this](Qt::ApplicationState state) {
if (state != Qt::ApplicationActive) {
hideGlobalSearchTransientUi();
}
});
}
MainWindow::~MainWindow() {}
void MainWindow::setupGlobalSearchHistory()
{
if (!m_globalSearchBox) {
return;
}
m_globalSearchModel = new QStringListModel(this);
m_globalSearchCompleter = new QCompleter(m_globalSearchModel, this);
m_globalSearchCompleter->setCaseSensitivity(Qt::CaseInsensitive);
m_globalSearchCompleter->setFilterMode(Qt::MatchContains);
m_globalSearchCompleter->setCompletionMode(QCompleter::PopupCompletion);
m_globalSearchCompleter->setMaxVisibleItems(8);
m_globalSearchCompleter->setPopup(new SearchCompleterPopup());
if (auto *popup = qobject_cast<SearchCompleterPopup *>(
m_globalSearchCompleter->popup())) {
popup->setMaxVisibleRows(m_globalSearchCompleter->maxVisibleItems());
}
m_globalSearchBox->setCompleter(m_globalSearchCompleter);
m_globalSearchHistoryPopup = new SearchHistoryPopup(this);
connect(m_globalSearchHistoryPopup, &SearchHistoryPopup::termActivated, this,
[this](const QString &term) {
if (!m_globalSearchBox) {
qWarning() << "History term ignored: search box was destroyed";
return;
}
m_globalSearchBox->setText(term);
submitGlobalSearch(term);
});
connect(m_globalSearchHistoryPopup, &SearchHistoryPopup::clearHistoryRequested, this,
[this]() {
SearchHistoryManager::instance()->clearHistory(currentSearchServerId());
});
connect(m_globalSearchHistoryPopup, &SearchHistoryPopup::removeHistoryTermRequested,
this, [this](const QString &term) {
SearchHistoryManager::instance()->removeHistoryTerm(
currentSearchServerId(), term);
});
connect(m_globalSearchBox.data(), &QLineEdit::textEdited, this,
[this](const QString &text) {
updateGlobalSearchCompleter(text);
hideGlobalSearchTransientUi();
});
connect(SearchHistoryManager::instance(), &SearchHistoryManager::historyChanged,
this, [this](const QString &serverId) {
if (serverId == currentSearchServerId()) {
updateGlobalSearchCompleter(m_globalSearchBox
? m_globalSearchBox->text()
: QString());
}
});
connect(SearchHistoryManager::instance(), &SearchHistoryManager::enabledChanged,
this, [this](bool enabled) {
Q_UNUSED(enabled);
updateGlobalSearchCompleter(m_globalSearchBox
? m_globalSearchBox->text()
: QString());
hideGlobalSearchTransientUi();
});
connect(SearchHistoryManager::instance(),
&SearchHistoryManager::autocompleteEnabledChanged, this,
[this](bool ) {
updateGlobalSearchCompleter(m_globalSearchBox
? m_globalSearchBox->text()
: QString());
});
if (m_core && m_core->serverManager()) {
connect(m_core->serverManager(), &ServerManager::activeServerChanged, this,
[this](const ServerProfile &profile) {
Q_UNUSED(profile);
updateGlobalSearchCompleter(m_globalSearchBox
? m_globalSearchBox->text()
: QString());
hideGlobalSearchTransientUi();
});
}
updateGlobalSearchCompleter();
}
void MainWindow::hideGlobalSearchTransientUi()
{
if (m_globalSearchHistoryPopup) {
const bool immediateHide =
QGuiApplication::applicationState() != Qt::ApplicationActive ||
!isVisible() || isMinimized() || !isActiveWindow();
m_globalSearchHistoryPopup->dismiss(immediateHide);
}
if (m_globalSearchCompleter && m_globalSearchCompleter->popup()) {
m_globalSearchCompleter->popup()->hide();
}
}
void MainWindow::updateGlobalSearchCompleter(const QString &text)
{
if (!m_globalSearchModel || !m_globalSearchCompleter || !m_globalSearchBox) {
return;
}
const QStringList suggestions =
SearchHistoryManager::instance()->completionSuggestions(
currentSearchServerId(), text, 8);
m_globalSearchModel->setStringList(suggestions);
if (!SearchHistoryManager::instance()->isAutocompleteEnabled() ||
text.trimmed().isEmpty() ||
suggestions.isEmpty() || !m_globalSearchBox->hasFocus()) {
if (m_globalSearchCompleter->popup()) {
m_globalSearchCompleter->popup()->hide();
}
return;
}
if (auto *popup = qobject_cast<SearchCompleterPopup *>(
m_globalSearchCompleter->popup())) {
popup->setHighlightText(text);
popup->syncWidthToAnchor(m_globalSearchBox.data());
}
m_globalSearchCompleter->setCompletionPrefix(text);
if (auto *popup = qobject_cast<SearchCompleterPopup *>(
m_globalSearchCompleter->popup())) {
m_globalSearchCompleter->complete(
popup->popupRectForAnchor(m_globalSearchBox.data()));
return;
}
m_globalSearchCompleter->complete();
}
void MainWindow::showGlobalSearchHistoryPopup(const QString &filterText)
{
if (!m_globalSearchBox || !m_globalSearchHistoryPopup ||
!SearchHistoryManager::instance()->isEnabled()) {
return;
}
if (!isVisible() || isMinimized() || !m_globalSearchBox->isVisible() ||
!m_globalSearchBox->hasFocus() ||
QGuiApplication::applicationState() != Qt::ApplicationActive ||
!isActiveWindow()) {
hideGlobalSearchTransientUi();
return;
}
const QString serverId = currentSearchServerId();
const auto historyEntries =
SearchHistoryManager::instance()->historyEntries(serverId, filterText);
m_globalSearchHistoryPopup->setEntries(historyEntries);
if (!m_globalSearchHistoryPopup->hasContent()) {
hideGlobalSearchTransientUi();
return;
}
m_globalSearchHistoryPopup->dismiss(true);
if (m_globalSearchCompleter && m_globalSearchCompleter->popup()) {
m_globalSearchCompleter->popup()->hide();
}
m_globalSearchHistoryPopup->showBelow(m_globalSearchBox.data());
}
void MainWindow::submitGlobalSearch(const QString &query)
{
if (!m_homeView || m_viewStack->currentWidget() != m_homeView) {
return;
}
const QString trimmedQuery = query.trimmed();
if (trimmedQuery.isEmpty()) {
return;
}
hideGlobalSearchTransientUi();
m_homeView->triggerSearch(trimmedQuery);
if (m_globalSearchBox) {
m_globalSearchBox->clear();
}
hideGlobalSearchTransientUi();
}
QString MainWindow::currentSearchServerId() const
{
if (!m_core || !m_core->serverManager()) {
return {};
}
return m_core->serverManager()->activeProfile().id;
}
void MainWindow::navigateToHome()
{
if (m_viewTransitionInProgress) {
qWarning() << "navigateToHome ignored: view transition already in progress";
return;
}
if (m_viewStack && m_viewStack->currentWidget() == m_homeView) {
return;
}
m_viewTransitionInProgress = true;
m_homeView->beginAuthenticatedSession();
m_homeView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
if (reduceAnimations) {
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
setMinimumSize(900, 600);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
if (auto* minBtn = findChild<QWidget*>("min-button")) minBtn->show();
if (auto* maxBtn = findChild<QWidget*>("max-button")) maxBtn->show();
#endif
m_loginView->setGraphicsEffect(nullptr);
m_homeView->setGraphicsEffect(nullptr);
m_viewStack->setCurrentWidget(m_homeView);
m_viewTransitionInProgress = false;
return;
}
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
setMinimumSize(900, 600);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
if (auto* minBtn = findChild<QWidget*>("min-button")) minBtn->show();
if (auto* maxBtn = findChild<QWidget*>("max-button")) maxBtn->show();
#endif
m_loginView->setGraphicsEffect(nullptr);
auto *homeOpacity = new QGraphicsOpacityEffect(m_homeView);
homeOpacity->setOpacity(0.0);
m_homeView->setGraphicsEffect(homeOpacity);
m_viewStack->setCurrentWidget(m_homeView);
auto *fadeIn = new QPropertyAnimation(homeOpacity, "opacity", this);
fadeIn->setDuration(AnimationTokens::DurationMs::kNormal);
fadeIn->setStartValue(0.0);
fadeIn->setEndValue(1.0);
fadeIn->setEasingCurve(AnimationTokens::Curve::fade());
connect(fadeIn, &QPropertyAnimation::finished, this, [this]() {
m_homeView->setGraphicsEffect(nullptr);
m_viewTransitionInProgress = false;
});
fadeIn->start(QAbstractAnimation::DeleteWhenStopped);
}
void MainWindow::navigateToLogin() {
if (m_viewTransitionInProgress) {
qWarning() << "navigateToLogin ignored: view transition already in progress";
return;
}
if (m_viewStack && m_viewStack->currentWidget() == m_loginView) {
return;
}
m_viewTransitionInProgress = true;
m_homeView->clearAuthenticatedSession();
m_homeView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
bool reduceAnimations = ConfigStore::instance()->get<bool>(ConfigKeys::UiAnimations, false);
if (reduceAnimations) {
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
if (auto* m = findChild<QWidget*>("min-button")) m->hide();
if (auto* x = findChild<QWidget*>("max-button")) x->hide();
#endif
setMinimumSize(900, 600);
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
m_homeView->setGraphicsEffect(nullptr);
m_loginView->setGraphicsEffect(nullptr);
m_viewStack->setCurrentWidget(m_loginView);
m_core->authService()->logout();
m_viewTransitionInProgress = false;
return;
}
setMinimumSize(900, 600);
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
#if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC)
if (auto* m = findChild<QWidget*>("min-button")) m->hide();
if (auto* x = findChild<QWidget*>("max-button")) x->hide();
#endif
m_homeView->setGraphicsEffect(nullptr);
auto *loginOpacity = new QGraphicsOpacityEffect(m_loginView);
loginOpacity->setOpacity(0.0);
m_loginView->setGraphicsEffect(loginOpacity);
m_viewStack->setCurrentWidget(m_loginView);
auto *fadeIn = new QPropertyAnimation(loginOpacity, "opacity", this);
fadeIn->setDuration(AnimationTokens::DurationMs::kNormal);
fadeIn->setStartValue(0.0);
fadeIn->setEndValue(1.0);
fadeIn->setEasingCurve(AnimationTokens::Curve::fade());
connect(fadeIn, &QPropertyAnimation::finished, this, [this]() {
m_loginView->setGraphicsEffect(nullptr);
m_core->authService()->logout();
m_viewTransitionInProgress = false;
});
fadeIn->start(QAbstractAnimation::DeleteWhenStopped);
}
void MainWindow::requestApplicationQuit()
{
if (m_quitPending) {
return;
}
m_quitPending = true;
m_realQuit = true;
const bool stoppedPlayback = stopActivePlaybackForQuit();
if (stoppedPlayback) {
QTimer::singleShot(500, this, &MainWindow::finishApplicationQuit);
return;
}
finishApplicationQuit();
}
bool MainWindow::stopActivePlaybackForQuit()
{
if (!m_homeView) {
return false;
}
auto *player = m_homeView->activePlayerView();
if (!player) {
return false;
}
player->stopAndReport();
return true;
}
void MainWindow::finishApplicationQuit()
{
if (m_core && m_core->authService()) {
m_core->authService()->logout();
}
qApp->quit();
}
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
if (watched == qApp && event->type() == QEvent::ApplicationStateChange) {
if (QGuiApplication::applicationState() != Qt::ApplicationActive) {
hideGlobalSearchTransientUi();
}
}
if (watched == this) {
if (event->type() == QEvent::WindowDeactivate ||
event->type() == QEvent::Move ||
event->type() == QEvent::Resize ||
event->type() == QEvent::Hide ||
event->type() == QEvent::Close ||
event->type() == QEvent::WindowStateChange) {
hideGlobalSearchTransientUi();
}
}
if (watched == m_globalSearchHistoryPopup && m_globalSearchHistoryPopup) {
if (event->type() == QEvent::WindowDeactivate ||
event->type() == QEvent::Hide ||
event->type() == QEvent::Close) {
hideGlobalSearchTransientUi();
}
}
if (m_globalSearchBox && watched == m_globalSearchBox.data()) {
if (event->type() == QEvent::FocusIn) {
auto *focusEvent = static_cast<QFocusEvent *>(event);
if (focusEvent->reason() != Qt::MouseFocusReason &&
m_globalSearchBox->text().trimmed().isEmpty()) {
QTimer::singleShot(0, this, [this]() {
showGlobalSearchHistoryPopup();
});
}
} else if (event->type() == QEvent::MouseButtonPress) {
if (m_globalSearchBox->text().trimmed().isEmpty()) {
QTimer::singleShot(0, this, [this]() {
showGlobalSearchHistoryPopup();
});
}
} else if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Down) {
showGlobalSearchHistoryPopup(m_globalSearchBox->text());
return true;
}
} else if (event->type() == QEvent::Hide) {
hideGlobalSearchTransientUi();
}
}
if ((event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseButtonDblClick) &&
m_globalSearchHistoryPopup && m_globalSearchHistoryPopup->isVisible() &&
m_globalSearchBox) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
const QPoint globalPos = mouseEvent->globalPosition().toPoint();
const QRect popupRect(m_globalSearchHistoryPopup->mapToGlobal(QPoint(0, 0)),
m_globalSearchHistoryPopup->size());
const QRect searchRect(m_globalSearchBox->mapToGlobal(QPoint(0, 0)),
m_globalSearchBox->size());
if (!popupRect.contains(globalPos) && !searchRect.contains(globalPos)) {
hideGlobalSearchTransientUi();
}
}
if (event->type() == QEvent::MouseButtonRelease) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::BackButton || mouseEvent->button() == Qt::XButton1) {
if (auto *modal = QApplication::activeModalWidget()) {
if (auto *dlg = qobject_cast<QDialog *>(modal)) {
dlg->reject();
}
return true;
}
auto back = findChild<QWK::WindowButton*>("back-button");
if (back && back->isVisible()) {
back->click();
return true;
}
}
} else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) {
auto *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::BackButton || mouseEvent->button() == Qt::XButton1) {
auto back = findChild<QWK::WindowButton*>("back-button");
if (back && back->isVisible()) {
return true;
}
}
}
if (ContextMenuUtils::showStyledTextContextMenu(watched, event)) {
return true;
}
if (ContextMenuUtils::showStyledLabelContextMenu(watched, event)) {
return true;
}
return QMainWindow::eventFilter(watched, event);
}
void MainWindow::closeEvent(QCloseEvent *event)
{
hideGlobalSearchTransientUi();
bool useTray = ConfigStore::instance()->get<bool>(ConfigKeys::CloseToTray, false);
if (!m_realQuit && useTray && QSystemTrayIcon::isSystemTrayAvailable()) {
if (auto *player = m_homeView->activePlayerView()) {
if (player->isMediaPlaying()) {
player->pausePlayback();
m_wasPausedByTray = true;
}
}
hide();
event->ignore();
} else {
if (!m_quitPending) {
requestApplicationQuit();
event->ignore();
return;
}
event->accept();
}
}