#include "mainwindow.h" #include "tokens/AnimationTokens.h" #include #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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "components/themetransitionwidget.h" #include "components/moderntoast.h" #include #include #include #include #include #include #include #include #include #include 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("back-button"); auto icon = findChild("icon-button"); auto home = findChild("home-button"); auto fav = findChild("fav-button"); auto title = findChild("win-title-label"); auto macTitle = findChild("mac-title-label"); auto macNav = findChild("mac-titlebar-nav"); auto macNavSpacer = findChild("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(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(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(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( 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( 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( m_globalSearchCompleter->popup())) { popup->setHighlightText(text); popup->syncWidthToAnchor(m_globalSearchBox.data()); } m_globalSearchCompleter->setCompletionPrefix(text); if (auto *popup = qobject_cast( 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(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("min-button")) minBtn->show(); if (auto* maxBtn = findChild("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("min-button")) minBtn->show(); if (auto* maxBtn = findChild("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(ConfigKeys::UiAnimations, false); if (reduceAnimations) { #if !defined(Q_OS_MACOS) && !defined(Q_OS_MAC) if (auto* m = findChild("min-button")) m->hide(); if (auto* x = findChild("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("min-button")) m->hide(); if (auto* x = findChild("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(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(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(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(event); if (mouseEvent->button() == Qt::BackButton || mouseEvent->button() == Qt::XButton1) { if (auto *modal = QApplication::activeModalWidget()) { if (auto *dlg = qobject_cast(modal)) { dlg->reject(); } return true; } auto back = findChild("back-button"); if (back && back->isVisible()) { back->click(); return true; } } } else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) { auto *mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::BackButton || mouseEvent->button() == Qt::XButton1) { auto back = findChild("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(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(); } }