mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 00:55:09 -07:00
Repaint cockatrice.svg in Qt instead of showing the baked-in static art: fill the full-color logo's alpha silhouette with the same brand gradient the banner plate uses (light AccentSoft grading to dark AccentStrong along the SVG's userSpaceOnUse axis), then overlay the white highlight path. Renders an explicit QPixmap so the mark stays crisp at the 200px display size, and re-seeds it on theme/palette/appearance changes so it never goes stale.
494 lines
20 KiB
C++
494 lines
20 KiB
C++
#include "home_widget.h"
|
|
|
|
#include "../../../client/settings/cache_settings.h"
|
|
#include "../../../interface/widgets/tabs/tab_supervisor.h"
|
|
#include "../../pixel_map_generator.h"
|
|
#include "../../theme_manager.h"
|
|
#include "../../window_main.h"
|
|
#include "../cards/art_crop_attribution.h"
|
|
#include "background_sources.h"
|
|
#include "home_styled_button.h"
|
|
#include "home_tab_button_color.h"
|
|
|
|
#include <QGroupBox>
|
|
#include <QLabel>
|
|
#include <QLinearGradient>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QPushButton>
|
|
#include <QVBoxLayout>
|
|
#include <libcockatrice/card/database/card_database_manager.h>
|
|
#include <libcockatrice/network/client/remote/remote_client.h>
|
|
#include <libcockatrice/settings/appearance_settings.h>
|
|
#include <libcockatrice/settings/paths_settings.h>
|
|
|
|
HomeWidget::HomeWidget(QWidget *parent, TabSupervisor *_tabSupervisor)
|
|
: QWidget(parent), tabSupervisor(_tabSupervisor), background(themePixmap(QStringLiteral("backgrounds/home")))
|
|
{
|
|
layout = new QGridLayout(this);
|
|
|
|
backgroundSourceCard = new CardInfoPictureArtCropWidget(this);
|
|
|
|
gradientColors = determineButtonColor();
|
|
|
|
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().appearance(), &AppearanceSettings::homeTabBackgroundSourceChanged, this,
|
|
&HomeWidget::initializeBackgroundFromSource);
|
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundShuffleFrequencyChanged,
|
|
this, &HomeWidget::onBackgroundShuffleFrequencyChanged);
|
|
// Lambda is cleaner to read than overloading this
|
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabDisplayCardNameChanged, this,
|
|
[this] { repaint(); });
|
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabBackgroundDimChanged, this,
|
|
[this] { repaint(); });
|
|
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
|
&HomeWidget::initializeBackgroundFromSource);
|
|
connect(&SettingsCache::instance(), &SettingsCache::themeChanged, this,
|
|
&HomeWidget::updateButtonsToBackgroundColor);
|
|
// Scheme flips (light/dark/system with an OS switch) fire on themeManager,
|
|
// not on SettingsCache::themeChanged, so re-resolve the variant background.
|
|
connect(themeManager, &ThemeManager::themeChanged, this, &HomeWidget::initializeBackgroundFromSource);
|
|
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateButtonsToBackgroundColor);
|
|
connect(themeManager, &ThemeManager::paletteChanged, this, &HomeWidget::updateLogoOverlay);
|
|
connect(&SettingsCache::instance().appearance(), &AppearanceSettings::homeTabButtonColorChanged, this,
|
|
&HomeWidget::updateButtonsToBackgroundColor);
|
|
}
|
|
|
|
void HomeWidget::initializeBackgroundFromSource()
|
|
{
|
|
// The featured logo is theme/scheme-derived too; reload it alongside the
|
|
// background so a theme or appearance switch doesn't leave it stale.
|
|
updateLogoOverlay();
|
|
|
|
if (CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
|
|
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
|
|
&HomeWidget::initializeBackgroundFromSource);
|
|
return;
|
|
}
|
|
|
|
auto backgroundSourceType =
|
|
BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource());
|
|
|
|
switch (backgroundSourceType) {
|
|
case BackgroundSources::Theme:
|
|
cardChangeTimer->stop();
|
|
background = themePixmap(QStringLiteral("backgrounds/home"));
|
|
backgroundSourceDeck = DeckList();
|
|
backgroundSourceCard->setCard(ExactCard());
|
|
updateButtonsToBackgroundColor();
|
|
update();
|
|
break;
|
|
case BackgroundSources::RandomCardArt:
|
|
backgroundSourceDeck = DeckList();
|
|
updateRandomCard();
|
|
onBackgroundShuffleFrequencyChanged();
|
|
break;
|
|
case BackgroundSources::DeckFileArt:
|
|
loadBackgroundSourceDeck();
|
|
updateRandomCard();
|
|
onBackgroundShuffleFrequencyChanged();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void HomeWidget::loadBackgroundSourceDeck()
|
|
{
|
|
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(
|
|
SettingsCache::instance().paths().getDeckPath() + "background.cod", DeckFileFormat::Cockatrice, false);
|
|
backgroundSourceDeck = deckOpt.has_value() ? deckOpt.value().deckList : DeckList();
|
|
}
|
|
|
|
static QPair<QColor, QColor> paletteDerivedButtonColors()
|
|
{
|
|
return {themeManager->appColor(AppColor::AccentStrong), themeManager->appColor(AppColor::AccentSoft)};
|
|
}
|
|
|
|
QPair<QColor, QColor> HomeWidget::determineButtonColor() const
|
|
{
|
|
auto colorSource =
|
|
HomeTabButtonColor::intToSource(SettingsCache::instance().appearance().getHomeTabButtonColorSourceIndex());
|
|
|
|
switch (colorSource) {
|
|
case HomeTabButtonColor::FromThemeColors:
|
|
return paletteDerivedButtonColors();
|
|
case HomeTabButtonColor::FromBackground:
|
|
return extractDominantColors(background);
|
|
}
|
|
|
|
return paletteDerivedButtonColors();
|
|
}
|
|
|
|
void HomeWidget::setRandomCard(ExactCard &newCard)
|
|
{
|
|
static constexpr int ATTEMPTS = 10;
|
|
for (int i = 0; i < ATTEMPTS; ++i) {
|
|
ExactCard tmpCard = CardDatabaseManager::query()->getRandomCard();
|
|
if (tmpCard != backgroundSourceCard->getCard() && tmpCard.getCardPtr()->getProperty("layout") == "normal" &&
|
|
tmpCard.getPrinting().getSet() != nullptr) {
|
|
newCard = tmpCard;
|
|
return;
|
|
}
|
|
}
|
|
qWarning() << "failed to set random card image after" << ATTEMPTS << "attempts";
|
|
}
|
|
|
|
void HomeWidget::updateRandomCard()
|
|
{
|
|
auto backgroundSourceType =
|
|
BackgroundSources::fromId(SettingsCache::instance().appearance().getHomeTabBackgroundSource());
|
|
|
|
ExactCard newCard;
|
|
|
|
switch (backgroundSourceType) {
|
|
case BackgroundSources::Theme:
|
|
break;
|
|
case BackgroundSources::RandomCardArt:
|
|
setRandomCard(newCard);
|
|
break;
|
|
case BackgroundSources::DeckFileArt:
|
|
QList<CardRef> cardRefs = backgroundSourceDeck.getCardRefList();
|
|
ExactCard oldCard = backgroundSourceCard->getCard();
|
|
|
|
if (!cardRefs.empty()) {
|
|
if (cardRefs.size() == 1) {
|
|
newCard = CardDatabaseManager::query()->getCard(cardRefs.first());
|
|
} else {
|
|
// Keep picking until different
|
|
do {
|
|
int idx = QRandomGenerator::global()->bounded(cardRefs.size());
|
|
newCard = CardDatabaseManager::query()->getCard(cardRefs.at(idx));
|
|
} while (newCard == oldCard);
|
|
}
|
|
} else {
|
|
do {
|
|
newCard = CardDatabaseManager::query()->getRandomCard();
|
|
} while (newCard == oldCard);
|
|
}
|
|
break;
|
|
}
|
|
if (!newCard) {
|
|
return;
|
|
}
|
|
|
|
connect(newCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &HomeWidget::updateBackgroundProperties);
|
|
backgroundSourceCard->setCard(newCard);
|
|
background = backgroundSourceCard->getBackground();
|
|
}
|
|
|
|
void HomeWidget::onBackgroundShuffleFrequencyChanged()
|
|
{
|
|
cardChangeTimer->stop();
|
|
if (SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() > 0) {
|
|
cardChangeTimer->start(SettingsCache::instance().appearance().getHomeTabBackgroundShuffleFrequency() * 1000);
|
|
}
|
|
}
|
|
|
|
void HomeWidget::updateBackgroundProperties()
|
|
{
|
|
background = backgroundSourceCard->getBackground();
|
|
updateButtonsToBackgroundColor();
|
|
update(); // Triggers repaint
|
|
}
|
|
|
|
void HomeWidget::updateButtonsToBackgroundColor()
|
|
{
|
|
gradientColors = determineButtonColor();
|
|
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);
|
|
|
|
logoLabel = new QLabel;
|
|
logoLabel->setAlignment(Qt::AlignCenter);
|
|
boxLayout->addWidget(logoLabel);
|
|
updateLogoOverlay();
|
|
boxLayout->addSpacing(25);
|
|
|
|
connectButton = new HomeStyledButton("Connect/Play", gradientColors);
|
|
boxLayout->addWidget(connectButton);
|
|
|
|
auto visualDeckEditorButton = new HomeStyledButton(tr("Create New Deck"), gradientColors);
|
|
connect(visualDeckEditorButton, &QPushButton::clicked, tabSupervisor,
|
|
[this] { tabSupervisor->openDeckInNewTab(LoadedDeck()); });
|
|
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 archidektButton = new HomeStyledButton(tr("Browse Archidekt"), gradientColors);
|
|
connect(archidektButton, &QPushButton::clicked, tabSupervisor, &TabSupervisor::addArchidektTab);
|
|
boxLayout->addWidget(archidektButton);
|
|
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)
|
|
{
|
|
disconnect(connectButton, &QPushButton::clicked, nullptr, nullptr);
|
|
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)
|
|
{
|
|
// 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);
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
if (!background.isNull()) {
|
|
QSize widgetSize = size() * devicePixelRatio();
|
|
QPixmap toDraw = background.scaled(widgetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
|
|
|
// Draw scaled background centered
|
|
QSize bgSize = toDraw.size();
|
|
QPoint topLeft((widgetSize.width() - bgSize.width()) / (devicePixelRatio() * 2), // undo scaling for painter
|
|
(widgetSize.height() - bgSize.height()) / (devicePixelRatio() * 2));
|
|
|
|
painter.drawPixmap(topLeft, toDraw);
|
|
}
|
|
|
|
if (SettingsCache::instance().appearance().getHomeTabBackgroundDim()) {
|
|
// Draw translucent black overlay with rounded corners
|
|
QRectF overlayRect(5, 5, width() - 10, height() - 10);
|
|
QPainterPath roundedRectPath;
|
|
roundedRectPath.addRoundedRect(overlayRect, 20, 20);
|
|
|
|
QColor semiTransparentBlack(0, 0, 0, static_cast<int>(255 * 0.33));
|
|
painter.fillPath(roundedRectPath, semiTransparentBlack);
|
|
}
|
|
|
|
// Card name overlay (above the attribution, bottom-right)
|
|
QString cardName;
|
|
QString attribution;
|
|
ExactCard card = backgroundSourceCard->getCard();
|
|
if (card) {
|
|
cardName = card.getCardPtr()->getName();
|
|
if (card.getPrinting().getSet() != nullptr) {
|
|
cardName += " (" + card.getPrinting().getSet()->getCorrectedShortName() + ") " +
|
|
card.getPrinting().getProperty("num");
|
|
}
|
|
attribution = buildArtAttribution(card);
|
|
}
|
|
|
|
// Scryfall requires artist attribution wherever card art is shown cropped.
|
|
// Pin it to the bottom-right corner, using the same font as the card name pill,
|
|
// and align its right edge with the card name pill's right edge.
|
|
constexpr int margin = 15;
|
|
constexpr qreal attributionMargin = 4.0;
|
|
|
|
QFont attributionFont = painter.font();
|
|
attributionFont.setPointSize(14);
|
|
attributionFont.setBold(true);
|
|
painter.setFont(attributionFont);
|
|
|
|
// paintArtAttribution insets the pill 4px from the given rect's right edge,
|
|
// so nudge the rect's right edge to land exactly on the pill's right edge.
|
|
QRectF attributionArea = rect();
|
|
attributionArea.setRight(width() - margin + attributionMargin);
|
|
const QRectF attributionRect = paintArtAttribution(painter, attributionArea, attribution);
|
|
|
|
// Card name bubble above the attribution (when enabled).
|
|
if (!cardName.isEmpty() && SettingsCache::instance().appearance().getHomeTabDisplayCardName()) {
|
|
QFont font = painter.font();
|
|
font.setPointSize(14);
|
|
font.setBold(true);
|
|
painter.setFont(font);
|
|
|
|
QFontMetrics fm(font);
|
|
constexpr int padding = 10;
|
|
|
|
QRect textRect = fm.boundingRect(cardName);
|
|
|
|
int bubbleBottom = height() - margin;
|
|
if (!attributionRect.isEmpty()) {
|
|
bubbleBottom = attributionRect.top() - 6;
|
|
}
|
|
const QRect nameBubbleRect(width() - textRect.width() - padding * 2 - margin,
|
|
bubbleBottom - textRect.height() - padding * 2, textRect.width() + padding * 2,
|
|
textRect.height() + padding * 2);
|
|
|
|
// Background bubble
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(QColor(0, 0, 0, 160));
|
|
painter.drawRoundedRect(nameBubbleRect, 8, 8);
|
|
|
|
// Text
|
|
painter.setPen(Qt::white);
|
|
painter.drawText(nameBubbleRect.adjusted(padding, padding, -padding, -padding),
|
|
Qt::AlignRight | Qt::AlignVCenter, cardName);
|
|
}
|
|
|
|
QWidget::paintEvent(event);
|
|
}
|
|
|
|
void HomeWidget::updateLogoOverlay()
|
|
{
|
|
// Emulate cockatrice.svg in Qt rather than rendering the baked-in SVG.
|
|
// The SVG has no separate plate: the gradient fills the bird's silhouette
|
|
// paths (light #c9fd62/AccentSoft at the top-left, dark #139740/AccentStrong
|
|
// toward the bottom-right — the SVG's linearGradient4265-7-8 stops along
|
|
// its userSpaceOnUse axis), and the white highlight path
|
|
// (cockatrice-logo-white) sits on top. So we paint that gradient clipped to
|
|
// the full logo silhouette (the full-color logo's alpha), then overlay the
|
|
// white mark. Colours stay fully theme-driven and independent of the static
|
|
// greens baked into the SVG.
|
|
const QColor strong = themeManager->appColor(AppColor::AccentStrong);
|
|
const QColor soft = themeManager->appColor(AppColor::AccentSoft);
|
|
|
|
const QPixmap silhouette = themePixmap(QStringLiteral("cockatrice")).scaledToWidth(200, Qt::SmoothTransformation);
|
|
const QPixmap whiteMark =
|
|
themePixmap(QStringLiteral("cockatrice-logo-white")).scaledToWidth(200, Qt::SmoothTransformation);
|
|
if (silhouette.isNull() || whiteMark.isNull()) {
|
|
return;
|
|
}
|
|
|
|
QPixmap composite(silhouette.size());
|
|
composite.fill(Qt::transparent);
|
|
|
|
{
|
|
QPainter painter(&composite);
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
|
|
|
// Recreate cockatrice.svg's own gradient geometry (linearGradient
|
|
// 4265-7-8, userSpaceOnUse): light AccentSoft at S=(-8.097,-97.746),
|
|
// dark AccentStrong at E=(162.455,295.208), on the SVG's 300x300
|
|
// canvas. Scale those coordinates to this composite's size.
|
|
const qreal scale = composite.width() / 300.0;
|
|
QLinearGradient gradient(QPointF(-8.097, -97.746) * scale, QPointF(162.455, 295.208) * scale);
|
|
gradient.setColorAt(0.0, soft);
|
|
gradient.setColorAt(1.0, strong);
|
|
painter.fillRect(composite.rect(), gradient);
|
|
|
|
// Clip the gradient to the full logo silhouette exactly as the SVG's
|
|
// gradient paths are confined to the bird.
|
|
painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
|
painter.drawPixmap(0, 0, silhouette);
|
|
|
|
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
|
painter.drawPixmap(0, 0, whiteMark);
|
|
}
|
|
|
|
if (logoLabel) {
|
|
logoLabel->setPixmap(composite);
|
|
}
|
|
}
|