diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 01ae93962..dfe40b03d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -50,6 +50,7 @@ set(cockatrice_SOURCES src/client/tabs/tab_deck_editor.cpp src/client/tabs/tab_deck_storage.cpp src/client/tabs/tab_game.cpp + src/client/tabs/tab_home.cpp src/client/tabs/tab_logs.cpp src/client/tabs/tab_message.cpp src/client/tabs/tab_replays.cpp @@ -84,6 +85,7 @@ set(cockatrice_SOURCES src/client/ui/widgets/cards/card_group_display_widgets/overlapped_card_group_display_widget.cpp src/client/ui/widgets/cards/card_info_display_widget.cpp src/client/ui/widgets/cards/card_info_frame_widget.cpp + src/client/ui/widgets/cards/card_info_picture_art_crop_widget.cpp src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp src/client/ui/widgets/cards/card_info_picture_widget.cpp src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -100,6 +102,9 @@ set(cockatrice_SOURCES src/client/ui/widgets/deck_editor/deck_editor_deck_dock_widget.cpp src/client/ui/widgets/deck_editor/deck_editor_filter_dock_widget.cpp src/client/ui/widgets/deck_editor/deck_editor_printing_selector_dock_widget.cpp + src/client/ui/widgets/general/background_sources.cpp + src/client/ui/widgets/general/home_styled_button.cpp + src/client/ui/widgets/general/home_widget.cpp src/client/ui/widgets/general/display/banner_widget.cpp src/client/ui/widgets/general/display/bar_widget.cpp src/client/ui/widgets/general/display/dynamic_font_size_label.cpp diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 67cde480e..88d1e62dc 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -47,6 +47,8 @@ resources/icons/mana/U.svg resources/icons/mana/W.svg + resources/backgrounds/home.png + resources/config/general.svg resources/config/appearance.svg resources/config/interface.svg diff --git a/cockatrice/resources/backgrounds/home.png b/cockatrice/resources/backgrounds/home.png new file mode 100644 index 000000000..68f48e2c2 Binary files /dev/null and b/cockatrice/resources/backgrounds/home.png differ diff --git a/cockatrice/src/client/tabs/tab_home.cpp b/cockatrice/src/client/tabs/tab_home.cpp new file mode 100644 index 000000000..a44945315 --- /dev/null +++ b/cockatrice/src/client/tabs/tab_home.cpp @@ -0,0 +1,14 @@ +#include "tab_home.h" + +#include +#include + +TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) +{ + homeWidget = new HomeWidget(this, tabSupervisor); + setCentralWidget(homeWidget); +} + +void TabHome::retranslateUi() +{ +} diff --git a/cockatrice/src/client/tabs/tab_home.h b/cockatrice/src/client/tabs/tab_home.h new file mode 100644 index 000000000..3a5317143 --- /dev/null +++ b/cockatrice/src/client/tabs/tab_home.h @@ -0,0 +1,29 @@ +#ifndef TAB_HOME_H +#define TAB_HOME_H + +#include "../../server/abstract_client.h" +#include "../ui/widgets/general/home_widget.h" +#include "tab.h" + +#include +#include + +class AbstractClient; + +class TabHome : public Tab +{ + Q_OBJECT +private: + AbstractClient *client; + HomeWidget *homeWidget; + +public: + TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client); + void retranslateUi() override; + QString getTabText() const override + { + return tr("Home"); + } +}; + +#endif // TAB_HOME_H diff --git a/cockatrice/src/client/tabs/tab_supervisor.cpp b/cockatrice/src/client/tabs/tab_supervisor.cpp index 9e834071c..3fb696954 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.cpp +++ b/cockatrice/src/client/tabs/tab_supervisor.cpp @@ -21,6 +21,7 @@ #include "tab_deck_editor.h" #include "tab_deck_storage.h" #include "tab_game.h" +#include "tab_home.h" #include "tab_logs.h" #include "tab_message.h" #include "tab_replays.h" @@ -312,7 +313,9 @@ static void checkAndTrigger(QAction *checkableAction, bool checked) */ void TabSupervisor::initStartupTabs() { + auto homeTab = addHomeTab(); addDeckEditorTab(nullptr); + setCurrentWidget(homeTab); if (SettingsCache::instance().getTabVisualDeckStorageOpen()) { openTabVisualDeckStorage(); @@ -501,6 +504,8 @@ void TabSupervisor::actTabVisualDeckStorage(bool checked) if (checked && !tabVisualDeckStorage) { openTabVisualDeckStorage(); setCurrentWidget(tabVisualDeckStorage); + } else if (checked && tabVisualDeckStorage) { + setCurrentWidget(tabVisualDeckStorage); } else if (!checked && tabVisualDeckStorage) { tabVisualDeckStorage->closeRequest(); } @@ -735,6 +740,17 @@ void TabSupervisor::roomLeft(TabRoom *tab) removeTab(indexOf(tab)); } +void TabSupervisor::switchToFirstAvailableNetworkTab() +{ + if (!roomTabs.isEmpty()) { + setCurrentWidget(roomTabs.first()); + } else if (tabServer) { + setCurrentWidget(tabServer); + } else { + openTabServer(); + } +} + void TabSupervisor::openReplay(GameReplay *replay) { auto *replayTab = new TabGame(this, replay); @@ -796,6 +812,14 @@ void TabSupervisor::talkLeft(TabMessage *tab) removeTab(indexOf(tab)); } +TabHome *TabSupervisor::addHomeTab() +{ + auto *tab = new TabHome(this, client); + myAddTab(tab); + setCurrentWidget(tab); + return tab; +} + /** * Creates a new deck editor tab and loads the deck into it. * Creates either a classic or visual deck editor tab depending on settings diff --git a/cockatrice/src/client/tabs/tab_supervisor.h b/cockatrice/src/client/tabs/tab_supervisor.h index 1c367043e..14c79bdca 100644 --- a/cockatrice/src/client/tabs/tab_supervisor.h +++ b/cockatrice/src/client/tabs/tab_supervisor.h @@ -26,6 +26,7 @@ class AbstractClient; class Tab; class TabServer; class TabRoom; +class TabHome; class TabGame; class TabDeckStorage; class TabReplays; @@ -167,15 +168,16 @@ public slots: TabEdhRecMain *addEdhrecMainTab(); TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false); void openReplay(GameReplay *replay); + void switchToFirstAvailableNetworkTab(); void maximizeMainWindow(); + void actTabVisualDeckStorage(bool checked); + void actTabReplays(bool checked); private slots: void refreshShortcuts(); - void actTabVisualDeckStorage(bool checked); void actTabServer(bool checked); void actTabAccount(bool checked); void actTabDeckStorage(bool checked); - void actTabReplays(bool checked); void actTabAdmin(bool checked); void actTabLog(bool checked); @@ -199,6 +201,7 @@ private slots: void processUserLeft(const QString &userName); void processUserJoined(const ServerInfo_User &userInfo); void talkLeft(TabMessage *tab); + TabHome *addHomeTab(); void deckEditorClosed(AbstractTabDeckEditor *tab); void tabUserEvent(bool globalEvent); void updateTabText(Tab *tab, const QString &newTabText); diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_art_crop_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_art_crop_widget.cpp new file mode 100644 index 000000000..5f5962a1c --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_art_crop_widget.cpp @@ -0,0 +1,41 @@ +#include "card_info_picture_art_crop_widget.h" + +#include "../../picture_loader/picture_loader.h" + +CardInfoPictureArtCropWidget::CardInfoPictureArtCropWidget(QWidget *parent) + : CardInfoPictureWidget(parent, false, false) +{ + hide(); +} + +QPixmap CardInfoPictureArtCropWidget::getProcessedBackground(const QSize &targetSize) +{ + // Load the full-resolution card image, not a pre-scaled one + QPixmap fullResPixmap; + if (getCard()) { + PictureLoader::getPixmap(fullResPixmap, getCard(), QSize(745, 1040)); // or a high default size + } else { + PictureLoader::getCardBackPixmap(fullResPixmap, QSize(745, 1040)); + } + + // Fail-safe if loading failed + if (fullResPixmap.isNull()) { + return QPixmap(targetSize); + } + + const QSize sz = fullResPixmap.size(); + + int marginX = sz.width() * 0.07; + int topMargin = sz.height() * 0.11; + int bottomMargin = sz.height() * 0.45; + + QRect foilRect(marginX, topMargin, sz.width() - 2 * marginX, sz.height() - topMargin - bottomMargin); + + foilRect = foilRect.intersected(fullResPixmap.rect()); // always clamp to source bounds + + // Crop first, then scale for best quality + QPixmap cropped = fullResPixmap.copy(foilRect); + QPixmap scaled = cropped.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + + return scaled; +} diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_art_crop_widget.h b/cockatrice/src/client/ui/widgets/cards/card_info_picture_art_crop_widget.h new file mode 100644 index 000000000..8b25af5a8 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_art_crop_widget.h @@ -0,0 +1,17 @@ +#ifndef CARD_INFO_PICTURE_ART_CROP_WIDGET_H +#define CARD_INFO_PICTURE_ART_CROP_WIDGET_H + +#include "card_info_picture_widget.h" + +class CardInfoPictureArtCropWidget : public CardInfoPictureWidget +{ + Q_OBJECT + +public: + explicit CardInfoPictureArtCropWidget(QWidget *parent = nullptr); + + // Returns a processed (cropped & scaled) version of the pixmap + QPixmap getProcessedBackground(const QSize &targetSize); +}; + +#endif // CARD_INFO_PICTURE_ART_CROP_WIDGET_H diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp index 1d8aca5e0..c84f6a7d5 100644 --- a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp +++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.cpp @@ -65,8 +65,10 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT CardInfoPictureWidget::~CardInfoPictureWidget() { - enlargedPixmapWidget->hide(); - enlargedPixmapWidget->deleteLater(); + if (enlargedPixmapWidget) { + enlargedPixmapWidget->hide(); + enlargedPixmapWidget->deleteLater(); + } } /** diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h index 38d5266f9..7de144a80 100644 --- a/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h +++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_widget.h @@ -74,7 +74,7 @@ private: bool hoverToZoomEnabled; bool raiseOnEnter; int hoverActivateThresholdInMs = 500; - CardInfoPictureEnlargedWidget *enlargedPixmapWidget; + CardInfoPictureEnlargedWidget *enlargedPixmapWidget = nullptr; int enlargedPixmapOffset = 10; QTimer *hoverTimer; QPropertyAnimation *animation; diff --git a/cockatrice/src/client/ui/widgets/general/background_sources.cpp b/cockatrice/src/client/ui/widgets/general/background_sources.cpp new file mode 100644 index 000000000..0d3efa330 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/background_sources.cpp @@ -0,0 +1,3 @@ +#include "background_sources.h" + +// Required so moc generates Q_OBJECT macros \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/general/background_sources.h b/cockatrice/src/client/ui/widgets/general/background_sources.h new file mode 100644 index 000000000..2b4cb64cc --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/background_sources.h @@ -0,0 +1,62 @@ +#ifndef COCKATRICE_BACKGROUND_SOURCES_H +#define COCKATRICE_BACKGROUND_SOURCES_H + +#include +#include +#include + +class BackgroundSources +{ + Q_GADGET +public: + enum Type + { + Theme, + RandomCardArt, + DeckFileArt + }; + Q_ENUM(Type) + + struct Entry + { + Type type; + const char *id; // stable ID for settings + const char *trKey; // key for translation + }; + + static QList all() + { + return {{Theme, "theme", QT_TR_NOOP("Theme")}, + {RandomCardArt, "random_card_art", QT_TR_NOOP("Art crop of random card")}, + {DeckFileArt, "deck_file_art", QT_TR_NOOP("Art crop of background.cod deck file")}}; + } + + static QString toId(Type type) + { + for (const auto &e : all()) { + if (e.type == type) + return e.id; + } + return {}; + } + + static Type fromId(const QString &id) + { + for (const auto &e : all()) { + if (id == e.id) + return e.type; + } + return Theme; // default + } + + static QString toDisplay(Type type) + { + for (const auto &e : all()) { + if (e.type == type) + return QObject::tr(e.trKey); + } + return {}; + } +}; + +#endif // COCKATRICE_BACKGROUND_SOURCES_H diff --git a/cockatrice/src/client/ui/widgets/general/home_styled_button.cpp b/cockatrice/src/client/ui/widgets/general/home_styled_button.cpp new file mode 100644 index 000000000..b1723caa6 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/home_styled_button.cpp @@ -0,0 +1,87 @@ +#include "home_styled_button.h" + +#include +#include +#include + +HomeStyledButton::HomeStyledButton(const QString &text, QPair _gradientColors, QWidget *parent) + : QPushButton(text, parent), gradientColors(_gradientColors) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumHeight(50); + setStyleSheet(generateButtonStylesheet(gradientColors)); +} + +void HomeStyledButton::updateStylesheet(const QPair &colors) +{ + gradientColors = colors; + setStyleSheet(generateButtonStylesheet(gradientColors)); +} + +QString HomeStyledButton::generateButtonStylesheet(const QPair &colors) +{ + QColor base1 = colors.first; + QColor base2 = colors.second; + + QColor hover1 = base1.lighter(120); // 20% lighter + QColor hover2 = base2.lighter(120); + + QColor pressed1 = base1.darker(130); // 30% darker + QColor pressed2 = base2.darker(130); + + return QString(R"( + QPushButton { + font-size: 34px; + padding: 30px; + color: white; + border: 2px solid %1; + border-radius: 20px; + background: qlineargradient(x1:0, y1:1, x2:0, y2:0, + stop:0 %2, stop:1 %3); + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:1, x2:0, y2:0, + stop:0 %4, stop:1 %5); + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, + stop:0 %6, stop:1 %7); + } + )") + .arg(base1.name()) // border color + .arg(base1.name()) // normal gradient start + .arg(base2.name()) // normal gradient end + .arg(hover1.name()) // hover start + .arg(hover2.name()) // hover end + .arg(pressed1.name()) // pressed start + .arg(pressed2.name()); // pressed end +} + +void HomeStyledButton::paintEvent(QPaintEvent *event) +{ + QString originalText = text(); + setText(""); // Prevent QPushButton from drawing the text + + QPushButton::paintEvent(event); // Draw background, borders, etc. + + setText(originalText); // Restore text for internal logic + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::TextAntialiasing); + + QFont font = this->font(); + font.setBold(true); + painter.setFont(font); + + QFontMetrics fm(font); + QSize textSize = fm.size(Qt::TextSingleLine, originalText); + QPointF center((width() - textSize.width()) / 2.0, (height() + textSize.height() / 2.0) / 2.0); + + QPainterPath path; + path.addText(center, font, originalText); + + painter.setPen(QPen(Qt::black, 2.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + painter.setBrush(Qt::white); + painter.drawPath(path); +} diff --git a/cockatrice/src/client/ui/widgets/general/home_styled_button.h b/cockatrice/src/client/ui/widgets/general/home_styled_button.h new file mode 100644 index 000000000..2ff30fc81 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/home_styled_button.h @@ -0,0 +1,23 @@ +// +// Created by ascor on 6/15/25. +// + +#ifndef HOME_STYLED_BUTTON_H +#define HOME_STYLED_BUTTON_H +#include + +class HomeStyledButton : public QPushButton +{ + Q_OBJECT +public: + HomeStyledButton(const QString &text, QPair gradientColors, QWidget *parent = nullptr); + void updateStylesheet(const QPair &colors); + QString generateButtonStylesheet(const QPair &colors); +public slots: + void paintEvent(QPaintEvent *event) override; + +private: + QPair gradientColors; +}; + +#endif // HOME_STYLED_BUTTON_H diff --git a/cockatrice/src/client/ui/widgets/general/home_widget.cpp b/cockatrice/src/client/ui/widgets/general/home_widget.cpp new file mode 100644 index 000000000..12a97e85f --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/home_widget.cpp @@ -0,0 +1,298 @@ +#include "home_widget.h" + +#include "../../../../game/cards/card_database_manager.h" +#include "../../../../server/remote/remote_client.h" +#include "../../../../settings/cache_settings.h" +#include "../../../tabs/tab_supervisor.h" +#include "../../window_main.h" +#include "background_sources.h" +#include "home_styled_button.h" + +#include +#include +#include +#include + +HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor) + : QWidget(parent), tabSupervisor(_tabSupervisor), background("theme:backgrounds/home"), overlay("theme:cockatrice") +{ + setAttribute(Qt::WA_OpaquePaintEvent); + layout = new QGridLayout(this); + + backgroundSourceCard = new CardInfoPictureArtCropWidget(this); + backgroundSourceDeck = new DeckLoader(); + + backgroundSourceDeck->loadFromFile(SettingsCache::instance().getDeckPath() + "background.cod", + DeckLoader::CockatriceFormat, false); + + gradientColors = extractDominantColors(background); + + layout->addWidget(createButtons(), 1, 1, Qt::AlignVCenter | Qt::AlignHCenter); + + layout->setRowStretch(0, 1); + layout->setRowStretch(2, 1); + layout->setColumnStretch(0, 1); + layout->setColumnStretch(2, 1); + + setLayout(layout); + + cardChangeTimer = new QTimer(this); + connect(cardChangeTimer, &QTimer::timeout, this, &HomeWidget::updateRandomCard); + + initializeBackgroundFromSource(); + + updateConnectButton(tabSupervisor->getClient()->getStatus()); + + connect(tabSupervisor->getClient(), &RemoteClient::statusChanged, this, &HomeWidget::updateConnectButton); + connect(&SettingsCache::instance(), &SettingsCache::homeTabBackgroundSourceChanged, this, + &HomeWidget::initializeBackgroundFromSource); +} + +void HomeWidget::initializeBackgroundFromSource() +{ + if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) { + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + &HomeWidget::initializeBackgroundFromSource); + return; + } + + auto backgroundSourceType = BackgroundSources::fromId(SettingsCache::instance().getHomeTabBackgroundSource()); + + cardChangeTimer->stop(); + + switch (backgroundSourceType) { + case BackgroundSources::Theme: + background = QPixmap("theme:backgrounds/home"); + updateButtonsToBackgroundColor(); + update(); + break; + case BackgroundSources::RandomCardArt: + cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000); + break; + case BackgroundSources::DeckFileArt: + backgroundSourceDeck->loadFromFile(SettingsCache::instance().getDeckPath() + "background.cod", + DeckLoader::CockatriceFormat, false); + cardChangeTimer->start(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency() * 1000); + break; + } +} + +void HomeWidget::updateRandomCard() +{ + auto backgroundSourceType = BackgroundSources::fromId(SettingsCache::instance().getHomeTabBackgroundSource()); + + ExactCard newCard; + + switch (backgroundSourceType) { + case BackgroundSources::Theme: + break; + case BackgroundSources::RandomCardArt: + do { + newCard = CardDatabaseManager::getInstance()->getRandomCard(); + } while (newCard == backgroundSourceCard->getCard()); + break; + case BackgroundSources::DeckFileArt: + QList cardRefs = backgroundSourceDeck->getCardRefList(); + ExactCard oldCard = backgroundSourceCard->getCard(); + + if (!cardRefs.empty()) { + if (cardRefs.size() == 1) { + newCard = CardDatabaseManager::getInstance()->getCard(cardRefs.first()); + } else { + // Keep picking until different + do { + int idx = QRandomGenerator::global()->bounded(cardRefs.size()); + newCard = CardDatabaseManager::getInstance()->getCard(cardRefs.at(idx)); + } while (newCard == oldCard); + } + } else { + do { + newCard = CardDatabaseManager::getInstance()->getRandomCard(); + } while (newCard == oldCard); + } + break; + } + if (!newCard) + return; + + connect(newCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &HomeWidget::updateBackgroundProperties); + backgroundSourceCard->setCard(newCard); + background = backgroundSourceCard->getProcessedBackground(size()); +} + +void HomeWidget::updateBackgroundProperties() +{ + background = backgroundSourceCard->getProcessedBackground(size()); + updateButtonsToBackgroundColor(); + update(); // Triggers repaint +} + +void HomeWidget::updateButtonsToBackgroundColor() +{ + gradientColors = extractDominantColors(background); + for (HomeStyledButton *button : findChildren()) { + button->updateStylesheet(gradientColors); + button->update(); + } +} + +QGroupBox *HomeWidget::createButtons() +{ + QGroupBox *box = new QGroupBox(this); + box->setStyleSheet(R"( + QGroupBox { + font-size: 20px; + color: white; /* Title text color */ + background: transparent; + } + + QGroupBox::title { + color: white; + subcontrol-origin: margin; + subcontrol-position: top center; /* or top left / right */ + } +)"); + box->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + QVBoxLayout *boxLayout = new QVBoxLayout; + boxLayout->setAlignment(Qt::AlignHCenter); + + QLabel *logoLabel = new QLabel; + logoLabel->setPixmap(overlay.scaledToWidth(200, Qt::SmoothTransformation)); + logoLabel->setAlignment(Qt::AlignCenter); + boxLayout->addWidget(logoLabel); + boxLayout->addSpacing(25); + + connectButton = new HomeStyledButton("Connect/Play", gradientColors); + boxLayout->addWidget(connectButton, 1); + + auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors); + connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor, + [this] { tabSupervisor->addVisualDeckEditorTab(nullptr); }); + boxLayout->addWidget(visualDeckEditorButton); + auto visualDeckStorageButton = new HomeStyledButton(tr("Browse Decks"), gradientColors); + connect(visualDeckStorageButton, &QPushButton::clicked, tabSupervisor, + [this] { tabSupervisor->actTabVisualDeckStorage(true); }); + boxLayout->addWidget(visualDeckStorageButton); + auto visualDatabaseDisplayButton = new HomeStyledButton(tr("Browse Card Database"), gradientColors); + connect(visualDatabaseDisplayButton, &QPushButton::clicked, tabSupervisor, + &TabSupervisor::addVisualDatabaseDisplayTab); + boxLayout->addWidget(visualDatabaseDisplayButton); + auto edhrecButton = new HomeStyledButton(tr("Browse EDHRec"), gradientColors); + connect(edhrecButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addEdhrecMainTab); + boxLayout->addWidget(edhrecButton); + auto replaybutton = new HomeStyledButton(tr("View Replays"), gradientColors); + connect(replaybutton, &QPushButton::clicked, tabSupervisor, [this] { tabSupervisor->actTabReplays(true); }); + boxLayout->addWidget(replaybutton); + if (qobject_cast(tabSupervisor->parentWidget())) { + auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors); + connect(exitButton, &QPushButton::clicked, qobject_cast(tabSupervisor->parentWidget()), + &MainWindow::actExit); + boxLayout->addWidget(exitButton); + } + + box->setLayout(boxLayout); + return box; +} + +void HomeWidget::updateConnectButton(const ClientStatus status) +{ + connectButton->disconnect(); + switch (status) { + case StatusConnecting: + connectButton->setText(tr("Connecting...")); + connectButton->setEnabled(false); + break; + case StatusDisconnected: + connectButton->setText(tr("Connect")); + connectButton->setEnabled(true); + connect(connectButton, &QPushButton::clicked, qobject_cast(tabSupervisor->parentWidget()), + &MainWindow::actConnect); + break; + case StatusLoggedIn: + connectButton->setText(tr("Play")); + connectButton->setEnabled(true); + connect(connectButton, &QPushButton::clicked, tabSupervisor, + &TabSupervisor::switchToFirstAvailableNetworkTab); + break; + default: + break; + } +} + +QPair HomeWidget::extractDominantColors(const QPixmap &pixmap) +{ + if (SettingsCache::instance().getThemeName() == "Default" && + SettingsCache::instance().getHomeTabBackgroundSource() == BackgroundSources::toId(BackgroundSources::Theme)) { + return QPair(QColor::fromRgb(20, 140, 60), QColor::fromRgb(120, 200, 80)); + } + + // Step 1: Downscale image for performance + QImage image = pixmap.toImage() + .scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation) + .convertToFormat(QImage::Format_RGB32); + + QMap colorCount; + + // Step 2: Count quantized colors + for (int y = 0; y < image.height(); ++y) { + const QRgb *scanLine = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < image.width(); ++x) { + QColor color = QColor::fromRgb(scanLine[x]); + + int r = color.red() & 0xF0; + int g = color.green() & 0xF0; + int b = color.blue() & 0xF0; + + QRgb quantized = qRgb(r, g, b); + colorCount[quantized]++; + } + } + + // Step 3: Sort by frequency + QVector> sortedColors; + for (auto it = colorCount.constBegin(); it != colorCount.constEnd(); ++it) { + sortedColors.append(qMakePair(it.key(), it.value())); + } + + std::sort(sortedColors.begin(), sortedColors.end(), + [](const QPair &a, const QPair &b) { return a.second > b.second; }); + + // Step 4: Pick top two distinct colors + QColor first = QColor(sortedColors.value(0).first); + QColor second = first; + + for (int i = 1; i < sortedColors.size(); ++i) { + QColor candidate = QColor(sortedColors[i].first); + if (candidate != first) { + second = candidate; + break; + } + } + + return QPair(first, second); +} + +void HomeWidget::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + + background = background.scaled(size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + + // Draw already-scaled background centered + QSize widgetSize = size(); + QSize bgSize = background.size(); + QPoint topLeft((widgetSize.width() - bgSize.width()) / 2, (widgetSize.height() - bgSize.height()) / 2); + + painter.drawPixmap(topLeft, background); + + // Draw translucent black overlay with rounded corners + QRectF overlayRect(5, 5, width() - 10, height() - 10); // 5px inset + QPainterPath roundedRectPath; + roundedRectPath.addRoundedRect(overlayRect, 20, 20); // 20px corner radius + + QColor semiTransparentBlack(0, 0, 0, static_cast(255 * 0.33)); // 33% opacity + painter.setRenderHint(QPainter::Antialiasing); + painter.fillPath(roundedRectPath, semiTransparentBlack); + + QWidget::paintEvent(event); +} diff --git a/cockatrice/src/client/ui/widgets/general/home_widget.h b/cockatrice/src/client/ui/widgets/general/home_widget.h new file mode 100644 index 000000000..3a465bcdc --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/home_widget.h @@ -0,0 +1,42 @@ +#ifndef HOME_WIDGET_H +#define HOME_WIDGET_H +#include "../../../../server/abstract_client.h" +#include "../../../tabs/tab_supervisor.h" +#include "../cards/card_info_picture_art_crop_widget.h" +#include "home_styled_button.h" + +#include +#include +#include + +class HomeWidget : public QWidget +{ + + Q_OBJECT + +public: + HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor); + void updateRandomCard(); + QPair extractDominantColors(const QPixmap &pixmap); + +public slots: + void paintEvent(QPaintEvent *event) override; + void initializeBackgroundFromSource(); + void updateBackgroundProperties(); + void updateButtonsToBackgroundColor(); + QGroupBox *createButtons(); + void updateConnectButton(const ClientStatus status); + +private: + QGridLayout *layout; + QTimer *cardChangeTimer; + TabSupervisor *tabSupervisor; + QPixmap background; + CardInfoPictureArtCropWidget *backgroundSourceCard = nullptr; + DeckLoader *backgroundSourceDeck; + QPixmap overlay; + QPair gradientColors; + HomeStyledButton *connectButton; +}; + +#endif // HOME_WIDGET_H diff --git a/cockatrice/src/client/ui/window_main.h b/cockatrice/src/client/ui/window_main.h index 76901c9d4..a1ffd48cc 100644 --- a/cockatrice/src/client/ui/window_main.h +++ b/cockatrice/src/client/ui/window_main.h @@ -59,6 +59,8 @@ public slots: void actCheckCardUpdatesBackground(); void actCheckServerUpdates(); void actCheckClientUpdates(); + void actConnect(); + void actExit(); private slots: void updateTabMenu(const QList &newMenuList); void statusChanged(ClientStatus _status); @@ -77,14 +79,12 @@ private slots: void localGameEnded(); void pixmapCacheSizeChanged(int newSizeInMBs); void notifyUserAboutUpdate(); - void actConnect(); void actDisconnect(); void actSinglePlayer(); void actWatchReplay(); void actFullScreen(bool checked); void actRegister(); void actSettings(); - void actExit(); void actForgotPasswordRequest(); void actAbout(); void actTips(); diff --git a/cockatrice/src/dialogs/dlg_settings.cpp b/cockatrice/src/dialogs/dlg_settings.cpp index 0de11aa48..c9982efae 100644 --- a/cockatrice/src/dialogs/dlg_settings.cpp +++ b/cockatrice/src/dialogs/dlg_settings.cpp @@ -7,6 +7,7 @@ #include "../client/tabs/tab_supervisor.h" #include "../client/ui/picture_loader/picture_loader.h" #include "../client/ui/theme_manager.h" +#include "../client/ui/widgets/general/background_sources.h" #include "../deck/custom_line_edit.h" #include "../game/cards/card_database.h" #include "../game/cards/card_database_manager.h" @@ -424,10 +425,36 @@ AppearanceSettingsPage::AppearanceSettingsPage() connect(&themeBox, qOverload(&QComboBox::currentIndexChanged), this, &AppearanceSettingsPage::themeBoxChanged); connect(&openThemeButton, &QPushButton::clicked, this, &AppearanceSettingsPage::openThemeLocation); + for (const auto &entry : BackgroundSources::all()) { + homeTabBackgroundSourceBox.addItem(QObject::tr(entry.trKey), QVariant::fromValue(entry.type)); + } + + QString homeTabBackgroundSource = SettingsCache::instance().getHomeTabBackgroundSource(); + int homeTabBackgroundSourceId = + homeTabBackgroundSourceBox.findData(BackgroundSources::fromId(homeTabBackgroundSource)); + if (homeTabBackgroundSourceId != -1) { + homeTabBackgroundSourceBox.setCurrentIndex(homeTabBackgroundSourceId); + } + + connect(&homeTabBackgroundSourceBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this]() { + auto type = homeTabBackgroundSourceBox.currentData().value(); + SettingsCache::instance().setHomeTabBackgroundSource(BackgroundSources::toId(type)); + }); + + homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600); + homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds")); + homeTabBackgroundShuffleFrequencySpinBox.setValue(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency()); + connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload(&QSpinBox::valueChanged), + &SettingsCache::instance(), &SettingsCache::setHomeTabBackgroundShuffleFrequency); + auto *themeGrid = new QGridLayout; themeGrid->addWidget(&themeLabel, 0, 0); themeGrid->addWidget(&themeBox, 0, 1); themeGrid->addWidget(&openThemeButton, 1, 1); + themeGrid->addWidget(&homeTabBackgroundSourceLabel, 2, 0); + themeGrid->addWidget(&homeTabBackgroundSourceBox, 2, 1); + themeGrid->addWidget(&homeTabBackgroundShuffleFrequencyLabel, 3, 0); + themeGrid->addWidget(&homeTabBackgroundShuffleFrequencySpinBox, 3, 1); themeGroupBox = new QGroupBox; themeGroupBox->setLayout(themeGrid); @@ -656,6 +683,8 @@ void AppearanceSettingsPage::retranslateUi() themeGroupBox->setTitle(tr("Theme settings")); themeLabel.setText(tr("Current theme:")); openThemeButton.setText(tr("Open themes folder")); + homeTabBackgroundSourceLabel.setText(tr("Home tab background source:")); + homeTabBackgroundShuffleFrequencyLabel.setText(tr("Home tab background shuffle frequency:")); menuGroupBox->setTitle(tr("Menu settings")); showShortcutsCheckBox.setText(tr("Show keyboard shortcuts in right-click menus")); diff --git a/cockatrice/src/dialogs/dlg_settings.h b/cockatrice/src/dialogs/dlg_settings.h index 19677c790..d21bf69a9 100644 --- a/cockatrice/src/dialogs/dlg_settings.h +++ b/cockatrice/src/dialogs/dlg_settings.h @@ -107,6 +107,10 @@ private: QLabel themeLabel; QComboBox themeBox; QPushButton openThemeButton; + QLabel homeTabBackgroundSourceLabel; + QComboBox homeTabBackgroundSourceBox; + QLabel homeTabBackgroundShuffleFrequencyLabel; + QSpinBox homeTabBackgroundShuffleFrequencySpinBox; QLabel minPlayersForMultiColumnLayoutLabel; QLabel maxFontSizeForCardsLabel; QCheckBox showShortcutsCheckBox; diff --git a/cockatrice/src/game/cards/card_database.cpp b/cockatrice/src/game/cards/card_database.cpp index e32519de1..35d30549e 100644 --- a/cockatrice/src/game/cards/card_database.cpp +++ b/cockatrice/src/game/cards/card_database.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include CardDatabase::CardDatabase(QObject *parent) : QObject(parent), loadStatus(NotLoaded) @@ -213,6 +214,19 @@ ExactCard CardDatabase::guessCard(const CardRef &cardRef) const return ExactCard(temp, findPrintingWithId(temp, cardRef.providerId)); } +ExactCard CardDatabase::getRandomCard() +{ + if (cards.isEmpty()) + return {}; + + const auto keys = cards.keys(); + int randomIndex = QRandomGenerator::global()->bounded(keys.size()); + const QString &randomKey = keys.at(randomIndex); + CardInfoPtr randomCard = getCardInfo(randomKey); + + return ExactCard{randomCard, getPreferredPrinting(randomCard)}; +} + CardSetPtr CardDatabase::getSet(const QString &setName) { if (sets.contains(setName)) { diff --git a/cockatrice/src/game/cards/card_database.h b/cockatrice/src/game/cards/card_database.h index 0654c7494..1f453d95f 100644 --- a/cockatrice/src/game/cards/card_database.h +++ b/cockatrice/src/game/cards/card_database.h @@ -85,6 +85,7 @@ public: ExactCard getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const; [[nodiscard]] ExactCard guessCard(const CardRef &cardRef) const; + [[nodiscard]] ExactCard getRandomCard(); /* * Get a card by its simple name. The name will be simplified in this @@ -98,6 +99,7 @@ public: return cards; } SetList getSetList() const; + CardInfoPtr getCardFromMap(const CardNameMap &cardMap, const QString &cardName) const; LoadStatus loadFromFile(const QString &fileName); bool saveCustomTokensToFile(); QStringList getAllMainCardTypes() const; diff --git a/cockatrice/src/settings/cache_settings.cpp b/cockatrice/src/settings/cache_settings.cpp index 6bfe04e2f..942881958 100644 --- a/cockatrice/src/settings/cache_settings.cpp +++ b/cockatrice/src/settings/cache_settings.cpp @@ -217,6 +217,9 @@ SettingsCache::SettingsCache() themeName = settings->value("theme/name").toString(); + homeTabBackgroundSource = settings->value("home/background", "themed").toString(); + homeTabBackgroundShuffleFrequency = settings->value("home/background/shuffleTimer", 0).toInt(); + tabVisualDeckStorageOpen = settings->value("tabs/visualDeckStorage", true).toBool(); tabServerOpen = settings->value("tabs/server", true).toBool(); tabAccountOpen = settings->value("tabs/account", true).toBool(); @@ -550,6 +553,20 @@ void SettingsCache::setThemeName(const QString &_themeName) emit themeChanged(); } +void SettingsCache::setHomeTabBackgroundSource(const QString &_backgroundSource) +{ + homeTabBackgroundSource = _backgroundSource; + settings->setValue("home/background", homeTabBackgroundSource); + emit homeTabBackgroundSourceChanged(); +} + +void SettingsCache::setHomeTabBackgroundShuffleFrequency(int _frequency) +{ + homeTabBackgroundShuffleFrequency = _frequency; + settings->setValue("home/background/shuffleTimer", homeTabBackgroundShuffleFrequency); + emit homeTabBackgroundShuffleFrequencyChanged(); +} + void SettingsCache::setTabVisualDeckStorageOpen(bool value) { tabVisualDeckStorageOpen = value; diff --git a/cockatrice/src/settings/cache_settings.h b/cockatrice/src/settings/cache_settings.h index ec8b506c5..c73149d3a 100644 --- a/cockatrice/src/settings/cache_settings.h +++ b/cockatrice/src/settings/cache_settings.h @@ -135,6 +135,8 @@ signals: void picsPathChanged(); void cardDatabasePathChanged(); void themeChanged(); + void homeTabBackgroundSourceChanged(); + void homeTabBackgroundShuffleFrequencyChanged(); void picDownloadChanged(); void showStatusBarChanged(bool state); void displayCardNamesChanged(); @@ -195,7 +197,7 @@ private: QByteArray setsDialogGeometry; QString lang; QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath, - customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName; + customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName, homeTabBackgroundSource; bool tabVisualDeckStorageOpen, tabServerOpen, tabAccountOpen, tabDeckStorageOpen, tabReplaysOpen, tabAdminOpen, tabLogOpen; bool checkUpdatesOnStartup; @@ -208,6 +210,7 @@ private: bool notifyAboutNewVersion; bool showTipsOnStartup; QList seenTips; + int homeTabBackgroundShuffleFrequency; bool mbDownloadSpoilers; int updateReleaseChannel; int maxFontSize; @@ -385,6 +388,14 @@ public: { return themeName; } + QString getHomeTabBackgroundSource() const + { + return homeTabBackgroundSource; + } + int getHomeTabBackgroundShuffleFrequency() const + { + return homeTabBackgroundShuffleFrequency; + } bool getTabVisualDeckStorageOpen() const { return tabVisualDeckStorageOpen; @@ -947,6 +958,8 @@ public slots: void setSpoilerDatabasePath(const QString &_spoilerDatabasePath); void setTokenDatabasePath(const QString &_tokenDatabasePath); void setThemeName(const QString &_themeName); + void setHomeTabBackgroundSource(const QString &_backgroundSource); + void setHomeTabBackgroundShuffleFrequency(int _frequency); void setTabVisualDeckStorageOpen(bool value); void setTabServerOpen(bool value); void setTabAccountOpen(bool value); diff --git a/dbconverter/src/mocks.cpp b/dbconverter/src/mocks.cpp index ade0922e4..213b42f02 100644 --- a/dbconverter/src/mocks.cpp +++ b/dbconverter/src/mocks.cpp @@ -136,6 +136,12 @@ void SettingsCache::setTokenDatabasePath(const QString & /* _tokenDatabasePath * void SettingsCache::setThemeName(const QString & /* _themeName */) { } +void SettingsCache::setHomeTabBackgroundSource(const QString & /* _backgroundSource */) +{ +} +void SettingsCache::setHomeTabBackgroundShuffleFrequency(int /* frequency */) +{ +} void SettingsCache::setTabVisualDeckStorageOpen(bool /*value*/) { } diff --git a/tests/carddatabase/mocks.cpp b/tests/carddatabase/mocks.cpp index 4c3a81322..08d82d765 100644 --- a/tests/carddatabase/mocks.cpp +++ b/tests/carddatabase/mocks.cpp @@ -140,6 +140,12 @@ void SettingsCache::setTokenDatabasePath(const QString & /* _tokenDatabasePath * void SettingsCache::setThemeName(const QString & /* _themeName */) { } +void SettingsCache::setHomeTabBackgroundSource(const QString & /* _backgroundSource */) +{ +} +void SettingsCache::setHomeTabBackgroundShuffleFrequency(int /* frequency */) +{ +} void SettingsCache::setTabVisualDeckStorageOpen(bool /*value*/) { }