Home tab to replace generic deck editor on startup (#6114)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Debian, DEB, 13) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Debian, DEB, skip, 11) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Debian, DEB, skip, 12) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Fedora, RPM, 42) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Fedora, RPM, skip, 41) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Ubuntu, DEB, 24.04) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (Ubuntu, DEB, skip, 22.04) (push) Blocked by required conditions
Build Desktop / ${{matrix.distro}} ${{matrix.version}} (yes, Arch, skip) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (1, macos-13, Intel, 13, Release, 14.3.1) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (1, macos-14, Apple, 14, Release, 15.4) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (1, macos-15, Apple, 15, Release, 16.2) (push) Blocked by required conditions
Build Desktop / macOS ${{matrix.target}}${{ matrix.soc == 'Intel' && ' Intel' || '' }}${{ matrix.type == 'Debug' && ' Debug' || '' }} (macos-15, Apple, 15, Debug, 16.2) (push) Blocked by required conditions
Build Desktop / Windows ${{matrix.target}} (msvc2019_64, 5.15.*, 7) (push) Blocked by required conditions
Build Desktop / Windows ${{matrix.target}} (msvc2019_64, qtimageformats qtmultimedia qtwebsockets, 6.6.*, 10) (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run

This commit is contained in:
BruebachL 2025-09-11 21:36:34 +02:00 committed by GitHub
parent 22c8268f02
commit 93c15c8151
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 751 additions and 8 deletions

View File

@ -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

View File

@ -47,6 +47,8 @@
<file>resources/icons/mana/U.svg</file>
<file>resources/icons/mana/W.svg</file>
<file>resources/backgrounds/home.png</file>
<file>resources/config/general.svg</file>
<file>resources/config/appearance.svg</file>
<file>resources/config/interface.svg</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View File

@ -0,0 +1,14 @@
#include "tab_home.h"
#include <QGroupBox>
#include <QPushButton>
TabHome::TabHome(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
{
homeWidget = new HomeWidget(this, tabSupervisor);
setCentralWidget(homeWidget);
}
void TabHome::retranslateUi()
{
}

View File

@ -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 <QHBoxLayout>
#include <qgroupbox.h>
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

View File

@ -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

View File

@ -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);

View File

@ -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;
}

View File

@ -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

View File

@ -65,8 +65,10 @@ CardInfoPictureWidget::CardInfoPictureWidget(QWidget *parent, const bool _hoverT
CardInfoPictureWidget::~CardInfoPictureWidget()
{
enlargedPixmapWidget->hide();
enlargedPixmapWidget->deleteLater();
if (enlargedPixmapWidget) {
enlargedPixmapWidget->hide();
enlargedPixmapWidget->deleteLater();
}
}
/**

View File

@ -74,7 +74,7 @@ private:
bool hoverToZoomEnabled;
bool raiseOnEnter;
int hoverActivateThresholdInMs = 500;
CardInfoPictureEnlargedWidget *enlargedPixmapWidget;
CardInfoPictureEnlargedWidget *enlargedPixmapWidget = nullptr;
int enlargedPixmapOffset = 10;
QTimer *hoverTimer;
QPropertyAnimation *animation;

View File

@ -0,0 +1,3 @@
#include "background_sources.h"
// Required so moc generates Q_OBJECT macros

View File

@ -0,0 +1,62 @@
#ifndef COCKATRICE_BACKGROUND_SOURCES_H
#define COCKATRICE_BACKGROUND_SOURCES_H
#include <QList>
#include <QObject>
#include <QString>
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<Entry> 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

View File

@ -0,0 +1,87 @@
#include "home_styled_button.h"
#include <QPainter>
#include <QPainterPath>
#include <qgraphicseffect.h>
HomeStyledButton::HomeStyledButton(const QString &text, QPair<QColor, QColor> _gradientColors, QWidget *parent)
: QPushButton(text, parent), gradientColors(_gradientColors)
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setMinimumHeight(50);
setStyleSheet(generateButtonStylesheet(gradientColors));
}
void HomeStyledButton::updateStylesheet(const QPair<QColor, QColor> &colors)
{
gradientColors = colors;
setStyleSheet(generateButtonStylesheet(gradientColors));
}
QString HomeStyledButton::generateButtonStylesheet(const QPair<QColor, QColor> &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);
}

View File

@ -0,0 +1,23 @@
//
// Created by ascor on 6/15/25.
//
#ifndef HOME_STYLED_BUTTON_H
#define HOME_STYLED_BUTTON_H
#include <QPushButton>
class HomeStyledButton : public QPushButton
{
Q_OBJECT
public:
HomeStyledButton(const QString &text, QPair<QColor, QColor> gradientColors, QWidget *parent = nullptr);
void updateStylesheet(const QPair<QColor, QColor> &colors);
QString generateButtonStylesheet(const QPair<QColor, QColor> &colors);
public slots:
void paintEvent(QPaintEvent *event) override;
private:
QPair<QColor, QColor> gradientColors;
};
#endif // HOME_STYLED_BUTTON_H

View File

@ -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 <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QVBoxLayout>
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<CardRef> 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<HomeStyledButton *>()) {
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<MainWindow *>(tabSupervisor->parentWidget())) {
auto exitButton = new HomeStyledButton(tr("Quit"), gradientColors);
connect(exitButton, &QPushButton::clicked, qobject_cast<MainWindow *>(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<MainWindow *>(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<QColor, QColor> HomeWidget::extractDominantColors(const QPixmap &pixmap)
{
if (SettingsCache::instance().getThemeName() == "Default" &&
SettingsCache::instance().getHomeTabBackgroundSource() == BackgroundSources::toId(BackgroundSources::Theme)) {
return QPair<QColor, QColor>(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<QRgb, int> colorCount;
// Step 2: Count quantized colors
for (int y = 0; y < image.height(); ++y) {
const QRgb *scanLine = reinterpret_cast<const QRgb *>(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<QPair<QRgb, int>> 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<QRgb, int> &a, const QPair<QRgb, int> &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<QColor, QColor>(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<int>(255 * 0.33)); // 33% opacity
painter.setRenderHint(QPainter::Antialiasing);
painter.fillPath(roundedRectPath, semiTransparentBlack);
QWidget::paintEvent(event);
}

View File

@ -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 <QGridLayout>
#include <QGroupBox>
#include <QWidget>
class HomeWidget : public QWidget
{
Q_OBJECT
public:
HomeWidget(QWidget *parent, TabSupervisor *tabSupervisor);
void updateRandomCard();
QPair<QColor, QColor> 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<QColor, QColor> gradientColors;
HomeStyledButton *connectButton;
};
#endif // HOME_WIDGET_H

View File

@ -59,6 +59,8 @@ public slots:
void actCheckCardUpdatesBackground();
void actCheckServerUpdates();
void actCheckClientUpdates();
void actConnect();
void actExit();
private slots:
void updateTabMenu(const QList<QMenu *> &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();

View File

@ -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<int>(&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<int>::of(&QComboBox::currentIndexChanged), this, [this]() {
auto type = homeTabBackgroundSourceBox.currentData().value<BackgroundSources::Type>();
SettingsCache::instance().setHomeTabBackgroundSource(BackgroundSources::toId(type));
});
homeTabBackgroundShuffleFrequencySpinBox.setRange(0, 3600);
homeTabBackgroundShuffleFrequencySpinBox.setSuffix(tr(" seconds"));
homeTabBackgroundShuffleFrequencySpinBox.setValue(SettingsCache::instance().getHomeTabBackgroundShuffleFrequency());
connect(&homeTabBackgroundShuffleFrequencySpinBox, qOverload<int>(&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"));

View File

@ -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;

View File

@ -14,6 +14,7 @@
#include <QMessageBox>
#include <QRegularExpression>
#include <algorithm>
#include <qrandom.h>
#include <utility>
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)) {

View File

@ -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;

View File

@ -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;

View File

@ -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<int> 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);

View File

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

View File

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