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*/)
{
}