mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 19:13:25 -08:00
Compare commits
2 commits
74a454552a
...
ed4eb1cb31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed4eb1cb31 | ||
|
|
9eafd90a91 |
131 changed files with 6699 additions and 71 deletions
|
|
@ -43,7 +43,9 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_load_remote_deck.cpp
|
||||
src/interface/widgets/dialogs/dlg_local_game_options.cpp
|
||||
src/interface/widgets/dialogs/dlg_manage_sets.cpp
|
||||
src/interface/widgets/dialogs/dlg_my_reports.cpp
|
||||
src/interface/widgets/dialogs/dlg_register.cpp
|
||||
src/interface/widgets/dialogs/dlg_report_user.cpp
|
||||
src/interface/widgets/dialogs/dlg_select_set_for_cards.cpp
|
||||
src/interface/widgets/dialogs/dlg_settings.cpp
|
||||
src/interface/widgets/dialogs/dlg_startup_card_check.cpp
|
||||
|
|
@ -240,6 +242,10 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/printing_selector/printing_selector_card_selection_widget.cpp
|
||||
src/interface/widgets/printing_selector/printing_selector_card_sorting_widget.cpp
|
||||
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||
src/interface/widgets/playmat/playmat_collection_dialog.cpp
|
||||
src/interface/widgets/playmat/playmat_collection_dialog.h
|
||||
src/interface/widgets/playmat/playmat_preview_widget.cpp
|
||||
src/interface/widgets/playmat/playmat_settings_dialog.cpp
|
||||
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||
src/interface/widgets/replay/replay_manager.cpp
|
||||
|
|
@ -278,6 +284,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/settings_page/user_interface_settings_page.cpp
|
||||
src/interface/widgets/utility/custom_line_edit.cpp
|
||||
src/interface/widgets/utility/get_text_with_max.cpp
|
||||
src/interface/widgets/utility/report_utils.cpp
|
||||
src/interface/widgets/utility/report_utils.h
|
||||
src/interface/widgets/utility/sequence_edit.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.cpp
|
||||
src/interface/widgets/utility/visibility_change_listener.h
|
||||
|
|
@ -371,6 +379,8 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/tabs/tab_home.cpp
|
||||
src/interface/widgets/tabs/tab_logs.cpp
|
||||
src/interface/widgets/tabs/tab_message.cpp
|
||||
src/interface/widgets/tabs/tab_moderation.cpp
|
||||
src/interface/widgets/tabs/tab_report.cpp
|
||||
src/interface/widgets/tabs/tab_replays.cpp
|
||||
src/interface/widgets/tabs/tab_room.cpp
|
||||
src/interface/widgets/tabs/tab_server.cpp
|
||||
|
|
|
|||
|
|
@ -296,6 +296,15 @@ void ConnectionController::onLoginError(int r,
|
|||
return;
|
||||
}
|
||||
|
||||
case Response::RespPasswordChangeRequired: {
|
||||
QMessageBox::information(
|
||||
dialogParent, tr("Password Change Required"),
|
||||
tr("An administrator has reset your password. Please contact your server administrator to obtain "
|
||||
"your temporary password, then log in and change it via Account -> Change Password."));
|
||||
remoteClient->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
|
||||
case Response::RespServerFull: {
|
||||
QMessageBox::critical(dialogParent, tr("Server Full"),
|
||||
tr("The server has reached its maximum user capacity, please check back later."));
|
||||
|
|
|
|||
|
|
@ -786,6 +786,10 @@ private:
|
|||
ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabLogs",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Logs"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabReport",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Report Queue"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
{"Tabs/aTabModeration",
|
||||
ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Moderation"), parseSequenceString(""), ShortcutGroup::Tabs)},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ void GameEventHandler::eventGameStateChanged(const Event_GameStateChanged &event
|
|||
emit playerJoined(prop);
|
||||
}
|
||||
player->processPlayerInfo(playerInfo);
|
||||
// Extract playmat from player properties for opponent display
|
||||
if (prop.has_playmat_params()) {
|
||||
player->setPlaymatFromProperties(prop);
|
||||
}
|
||||
if (player->getPlayerInfo()->getLocal()) {
|
||||
emit localPlayerDeckSelected(player, playerId, playerInfo);
|
||||
} else {
|
||||
|
|
@ -351,6 +355,11 @@ void GameEventHandler::eventPlayerPropertiesChanged(const Event_PlayerProperties
|
|||
const ServerInfo_PlayerProperties &prop = event.player_properties();
|
||||
emit playerPropertiesChanged(prop, eventPlayerId);
|
||||
|
||||
// Update playmat from player properties
|
||||
if (prop.has_playmat_params()) {
|
||||
player->setPlaymatFromProperties(prop);
|
||||
}
|
||||
|
||||
const auto contextType = static_cast<GameEventContext::ContextType>(getPbExtension(context));
|
||||
switch (contextType) {
|
||||
case GameEventContext::READY_START: {
|
||||
|
|
|
|||
|
|
@ -250,6 +250,22 @@ void PlayerLogic::setDeck(const DeckList &_deck)
|
|||
emit deckChanged();
|
||||
}
|
||||
|
||||
void PlayerLogic::setPlaymatFromProperties(const ServerInfo_PlayerProperties &props)
|
||||
{
|
||||
if (props.has_playmat_params() && !props.playmat_params().card_name().empty()) {
|
||||
const auto &pp = props.playmat_params();
|
||||
remotePlaymatCard = {QString::fromStdString(pp.card_name()), QString::fromStdString(pp.card_provider_id())};
|
||||
remotePlaymatParams = {qBound(0.0, pp.margin_pct_l(), 0.95), qBound(0.0, pp.margin_pct_r(), 0.95),
|
||||
qBound(0.0, pp.vertical_offset(), 1.0), qBound(0.1, pp.zoom(), 4.0)};
|
||||
hasRemotePlaymat = true;
|
||||
} else {
|
||||
remotePlaymatCard = CardRef{};
|
||||
remotePlaymatParams = PlaymatParams{};
|
||||
hasRemotePlaymat = false;
|
||||
}
|
||||
emit playmatChanged();
|
||||
}
|
||||
|
||||
CounterState *PlayerLogic::addCounter(const ServerInfo_Counter &counter)
|
||||
{
|
||||
return addCounter(counter.id(), QString::fromStdString(counter.name()),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "../zones/table_zone_logic.h"
|
||||
#include "player_event_handler.h"
|
||||
#include "player_info.h"
|
||||
#include "player_manager.h"
|
||||
|
||||
#include <QInputDialog>
|
||||
#include <QLoggingCategory>
|
||||
|
|
@ -72,6 +73,8 @@ signals:
|
|||
const QList<const ServerInfo_Card *> &cardList,
|
||||
bool withWritePermission);
|
||||
void deckChanged();
|
||||
/** @brief Emitted when the remote playmat (card/params) is updated from player properties. */
|
||||
void playmatChanged();
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void requestCardMenuUpdate(const CardItem *card);
|
||||
void counterAdded(CounterState *state);
|
||||
|
|
@ -226,6 +229,20 @@ public:
|
|||
|
||||
void setZoneId(int _zoneId);
|
||||
|
||||
void setPlaymatFromProperties(const ServerInfo_PlayerProperties &props);
|
||||
const CardRef &getRemotePlaymatCard() const
|
||||
{
|
||||
return remotePlaymatCard;
|
||||
}
|
||||
const PlaymatParams &getRemotePlaymatParams() const
|
||||
{
|
||||
return remotePlaymatParams;
|
||||
}
|
||||
bool getHasRemotePlaymat() const
|
||||
{
|
||||
return hasRemotePlaymat;
|
||||
}
|
||||
|
||||
private:
|
||||
AbstractGame *game;
|
||||
PlayerInfo *playerInfo;
|
||||
|
|
@ -243,6 +260,11 @@ private:
|
|||
|
||||
bool dialogSemaphore;
|
||||
QList<CardItem *> cardsToDelete;
|
||||
|
||||
// Playmat from player properties (for opponent display)
|
||||
CardRef remotePlaymatCard;
|
||||
PlaymatParams remotePlaymatParams;
|
||||
bool hasRemotePlaymat = false;
|
||||
};
|
||||
|
||||
class AnnotationDialog : public QInputDialog
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@
|
|||
#include <QMessageBox>
|
||||
#include <libcockatrice/card/database/card_database.h>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/settings/visual_deck_storage_settings.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
|
|
@ -100,6 +103,9 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
|
|||
connect(&SettingsCache::instance().visualDeckStorage(), &VisualDeckStorageSettings::visualDeckStorageInGameChanged,
|
||||
this, &DeckViewContainer::setVisualDeckStorageExists);
|
||||
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatSettingsChanged, this,
|
||||
&DeckViewContainer::onPlaymatSettingsChanged);
|
||||
|
||||
switchToDeckSelectView();
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +283,8 @@ void DeckViewContainer::loadDeckFromFile(const QString &filePath)
|
|||
|
||||
void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
||||
{
|
||||
currentDeck = deck;
|
||||
|
||||
QString deckString = deck.writeToString_Native();
|
||||
|
||||
if (deckString.length() > MAX_FILE_LENGTH) {
|
||||
|
|
@ -289,6 +297,52 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
|
|||
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
|
||||
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
void DeckViewContainer::resolveAndSendPlaymat()
|
||||
{
|
||||
if (currentDeck.getCardRefList().isEmpty() && currentDeck.getPlaymat().card.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &settings = SettingsCache::instance().userInterface();
|
||||
const auto fallbackBehavior = static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior());
|
||||
|
||||
QList<PlaymatInfo> fallbackList = settings.getPlaymatFallbackList();
|
||||
|
||||
// In random mode with 2+ entries, remove the last-resolved mat to avoid repeats.
|
||||
if (fallbackBehavior == PlaymatFallbackModeRandom && fallbackList.size() > 1) {
|
||||
fallbackList.removeAll(lastResolvedPlaymat);
|
||||
}
|
||||
|
||||
const PlaymatInfo resolved =
|
||||
resolvePlaymatForDeck(currentDeck, fallbackList, static_cast<PlaymatMode>(settings.getPlaymatMode()),
|
||||
fallbackBehavior, playmatRotationIndex);
|
||||
|
||||
lastResolvedPlaymat = resolved;
|
||||
|
||||
Command_SetPlaymat playmatCmd;
|
||||
auto *pp = playmatCmd.mutable_playmat_params();
|
||||
pp->set_card_name(resolved.card.name.toStdString());
|
||||
pp->set_card_provider_id(resolved.card.providerId.toStdString());
|
||||
pp->set_margin_pct_l(resolved.params.marginPctL);
|
||||
pp->set_margin_pct_r(resolved.params.marginPctR);
|
||||
pp->set_vertical_offset(resolved.params.verticalOffset);
|
||||
pp->set_zoom(resolved.params.zoom);
|
||||
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(playmatPend, playerId);
|
||||
}
|
||||
|
||||
void DeckViewContainer::onPlaymatSettingsChanged()
|
||||
{
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
void DeckViewContainer::advancePlaymatRotation()
|
||||
{
|
||||
playmatRotationIndex++;
|
||||
}
|
||||
|
||||
void DeckViewContainer::loadRemoteDeck()
|
||||
|
|
@ -379,6 +433,10 @@ void DeckViewContainer::sideboardPlanChanged()
|
|||
*/
|
||||
void DeckViewContainer::sendReadyStartCommand(bool ready)
|
||||
{
|
||||
if (ready) {
|
||||
resolveAndSendPlaymat();
|
||||
}
|
||||
|
||||
Command_ReadyStart cmd;
|
||||
cmd.set_ready(ready);
|
||||
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
|
||||
|
|
@ -416,6 +474,7 @@ void DeckViewContainer::setSideboardLocked(bool locked)
|
|||
|
||||
void DeckViewContainer::setDeck(const DeckList &deck)
|
||||
{
|
||||
currentDeck = deck;
|
||||
deckView->setDeck(deck);
|
||||
switchToDeckLoadedView();
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ private:
|
|||
VisualDeckStorageWidget *visualDeckStorageWidget;
|
||||
TabGame *parentGame;
|
||||
int playerId;
|
||||
int playmatRotationIndex = 0; ///< Per-match cursor for round-robin playmat mode.
|
||||
DeckList currentDeck; ///< Cached deck for live settings re-resolution.
|
||||
PlaymatInfo lastResolvedPlaymat; ///< Tracks last sent playmat to avoid repeats in random mode.
|
||||
|
||||
void tryCreateVisualDeckStorageWidget();
|
||||
void sendReadyStartCommand(bool ready);
|
||||
|
|
@ -75,6 +78,7 @@ private slots:
|
|||
void sideboardLockButtonClicked();
|
||||
void updateSideboardLockButtonText();
|
||||
void refreshShortcuts();
|
||||
void onPlaymatSettingsChanged();
|
||||
signals:
|
||||
void newCardAdded(AbstractCardItem *card);
|
||||
void notIdle();
|
||||
|
|
@ -87,6 +91,8 @@ public:
|
|||
void setSideboardLocked(bool locked);
|
||||
void setDeck(const DeckList &deck);
|
||||
void setVisualDeckStorageExists(bool exists);
|
||||
void advancePlaymatRotation();
|
||||
void resolveAndSendPlaymat();
|
||||
|
||||
public slots:
|
||||
void loadDeckFromFile(const QString &filePath);
|
||||
|
|
|
|||
|
|
@ -252,17 +252,27 @@ void GameScene::adjustPlayerRotation(int rotationAdjustment)
|
|||
*/
|
||||
void GameScene::rearrange()
|
||||
{
|
||||
int firstPlayerIndex = 0;
|
||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||
if (rearranging) {
|
||||
needsReArrange = true;
|
||||
return;
|
||||
}
|
||||
rearranging = true;
|
||||
do {
|
||||
needsReArrange = false;
|
||||
|
||||
int columns = determineColumnCount(playersPlaying.size());
|
||||
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
||||
int firstPlayerIndex = 0;
|
||||
auto playersPlaying = collectActivePlayers(firstPlayerIndex);
|
||||
playersPlaying = rotatePlayers(playersPlaying, firstPlayerIndex);
|
||||
|
||||
phasesToolbar->setHeight(sceneSize.height());
|
||||
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
||||
int columns = determineColumnCount(playersPlaying.size());
|
||||
QSizeF sceneSize = computeSceneSizeAndPlayerLayout(playersPlaying, columns);
|
||||
|
||||
processViewSizeChange(viewSize);
|
||||
phasesToolbar->setHeight(sceneSize.height());
|
||||
setSceneRect(0, 0, sceneSize.width(), sceneSize.height());
|
||||
|
||||
processViewSizeChange(viewSize);
|
||||
} while (needsReArrange);
|
||||
rearranging = false;
|
||||
}
|
||||
|
||||
// ---------- View Size ----------
|
||||
|
|
@ -459,8 +469,14 @@ void GameScene::resizeColumnsAndPlayers(const QList<qreal> &minWidthByColumn, qr
|
|||
qreal extraWidthPerColumn = (newWidth - minWidth) / playersByColumn.size();
|
||||
qreal newx = phasesToolbar->getWidth();
|
||||
|
||||
for (int col = 0; col < playersByColumn.size(); ++col) {
|
||||
for (PlayerGraphicsItem *player : playersByColumn[col]) {
|
||||
// Snapshot the columns: resizing a player's table can synchronously trigger
|
||||
// GameScene::rearrange (table width -> sizeChanged -> updateBoundingRect ->
|
||||
// sizeChanged -> rearrange), and rearrange rebuilds playersByColumn. Iterating
|
||||
// the live container across that re-entrant call would use invalidated iterators.
|
||||
const QList<QList<PlayerGraphicsItem *>> columns = playersByColumn;
|
||||
|
||||
for (int col = 0; col < columns.size(); ++col) {
|
||||
for (PlayerGraphicsItem *player : columns[col]) {
|
||||
player->processSceneSizeChange(minWidthByColumn[col] + extraWidthPerColumn);
|
||||
player->setPos(newx, player->y());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ private:
|
|||
QBasicTimer *animationTimer; ///< Timer for scene animations
|
||||
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
|
||||
int playerRotation; ///< Rotation offset for player layout
|
||||
bool rearranging = false; ///< Guard against re-entrant rearrange
|
||||
bool needsReArrange = false; ///< Pending rearrange requested during a pass
|
||||
|
||||
/**
|
||||
* @brief Updates which card is currently hovered based on scene coordinates.
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ void GameView::startRubberBand(const QPointF &_selectionOrigin)
|
|||
}
|
||||
|
||||
selectionOrigin = _selectionOrigin;
|
||||
previousBandRect = QRect();
|
||||
rubberBand->setGeometry(QRect(mapFromScene(selectionOrigin), QSize(0, 0)));
|
||||
rubberBand->show();
|
||||
}
|
||||
|
|
@ -128,7 +129,17 @@ void GameView::resizeRubberBand(const QPointF &cursorPoint, int selectedCount)
|
|||
|
||||
QPoint cursor = cursorPoint.toPoint();
|
||||
QRect rect = QRect(mapFromScene(selectionOrigin), cursor).normalized();
|
||||
|
||||
rubberBand->setGeometry(rect);
|
||||
if (viewport()) {
|
||||
// Repaint the union of the previous and current band rects: the vacated
|
||||
// strip of a child widget is not reliably invalidated on all platforms
|
||||
// (notably macOS), leaving stale pixels under the selection.
|
||||
QRect dirty = previousBandRect.isNull() ? rect : previousBandRect.united(rect);
|
||||
dirty.adjust(-1, -1, 1, 1);
|
||||
viewport()->update(dirty);
|
||||
previousBandRect = rect;
|
||||
}
|
||||
|
||||
if (!SettingsCache::instance().userInterface().getShowDragSelectionCount()) {
|
||||
dragCountLabel->hide();
|
||||
|
|
@ -171,7 +182,13 @@ void GameView::stopRubberBand()
|
|||
return;
|
||||
}
|
||||
|
||||
// Same rationale as resizeRubberBand: repaint the last known band area
|
||||
// since hiding a child widget doesn't reliably invalidate its region.
|
||||
rubberBand->hide();
|
||||
if (viewport() && !previousBandRect.isNull()) {
|
||||
viewport()->update(previousBandRect.adjusted(-1, -1, 1, 1));
|
||||
previousBandRect = QRect();
|
||||
}
|
||||
dragCountLabel->hide();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ private:
|
|||
QWidget *tallyContainer;
|
||||
QGridLayout *tallyLayout;
|
||||
QPointF selectionOrigin;
|
||||
QRect previousBandRect; ///< Last rubber-band rect for targeted repaint
|
||||
QList<TallyRow> cachedTallyRows; ///< Cached entries to avoid redundant rebuilds
|
||||
|
||||
QSize rebuildTallyLabels(const QList<TallyRow> &entries);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "player_graphics_item.h"
|
||||
|
||||
#include "../../game/player/player_actions.h"
|
||||
#include "../../interface/card_picture_loader/card_picture_loader.h"
|
||||
#include "../../interface/widgets/cards/art_crop_attribution.h"
|
||||
#include "../../interface/widgets/playmat/playmat_utils.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../board/abstract_card_item.h"
|
||||
#include "../board/counter_general.h"
|
||||
|
|
@ -13,6 +16,9 @@
|
|||
#include "player_dialogs.h"
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
#include <libcockatrice/deck_list/playmat_resolver.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
||||
|
|
@ -28,6 +34,10 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
connect(player, &PlayerLogic::counterAdded, this, &PlayerGraphicsItem::onCounterAdded);
|
||||
connect(player, &PlayerLogic::counterRemoved, this, &PlayerGraphicsItem::onCounterRemoved);
|
||||
connect(player, &PlayerLogic::deckChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||
connect(player, &PlayerLogic::playmatChanged, this, &PlayerGraphicsItem::updatePlaymat);
|
||||
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::playmatVisibilityChanged, this,
|
||||
[this](int) { updatePlaymat(); });
|
||||
|
||||
playerMenu = new PlayerMenu(this);
|
||||
|
||||
|
|
@ -67,6 +77,9 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
|
|||
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
|
||||
connect(this, &PlayerGraphicsItem::playmatChanged, tableZoneGraphicsItem, &TableZone::onPlaymatChanged);
|
||||
connect(this, &PlayerGraphicsItem::playmatChanged, stackZoneGraphicsItem, &StackZone::onPlaymatChanged);
|
||||
|
||||
updateBoundingRect();
|
||||
|
||||
rearrangeZones();
|
||||
|
|
@ -112,7 +125,6 @@ void PlayerGraphicsItem::initializeZones()
|
|||
rfgZoneGraphicsItem->setPos(base + QPointF(0, 2 * h + h2 + 10));
|
||||
|
||||
tableZoneGraphicsItem = new TableZone(player->getTableZone(), mirrored, this);
|
||||
connect(tableZoneGraphicsItem, &TableZone::sizeChanged, this, &PlayerGraphicsItem::updateBoundingRect);
|
||||
connect(this, &PlayerGraphicsItem::mirroredChanged, tableZoneGraphicsItem, &TableZone::setMirrored);
|
||||
|
||||
stackZoneGraphicsItem =
|
||||
|
|
@ -155,10 +167,61 @@ qreal PlayerGraphicsItem::getMinimumWidth() const
|
|||
return result;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::paint(QPainter * /*painter*/,
|
||||
const QStyleOptionGraphicsItem * /*option*/,
|
||||
QWidget * /*widget*/)
|
||||
void PlayerGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
|
||||
{
|
||||
if (!hasPlaymat || playmatPixmap.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the combined bounding rect of stack + table zones
|
||||
QPointF stackPos = stackZoneGraphicsItem->pos();
|
||||
QPointF tablePos = tableZoneGraphicsItem->pos();
|
||||
QSizeF stackSize = stackZoneGraphicsItem->boundingRect().size();
|
||||
QSizeF tableSize = tableZoneGraphicsItem->boundingRect().size();
|
||||
|
||||
// Combined area: from stack left edge to table right edge
|
||||
double combinedLeft = qMin(stackPos.x(), tablePos.x());
|
||||
double combinedTop = qMin(stackPos.y(), tablePos.y());
|
||||
double combinedRight = qMax(stackPos.x() + stackSize.width(), tablePos.x() + tableSize.width());
|
||||
double combinedBottom = qMax(stackPos.y() + stackSize.height(), tablePos.y() + tableSize.height());
|
||||
|
||||
QRectF combinedArea(combinedLeft, combinedTop, combinedRight - combinedLeft, combinedBottom - combinedTop);
|
||||
|
||||
const QRectF srcRect = computeArtSourceRect(playmatPixmap.size(), playmatParams);
|
||||
const QRectF dstRect = coverFitRect(combinedArea, srcRect.size());
|
||||
|
||||
painter->save();
|
||||
painter->setClipRect(combinedArea);
|
||||
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
|
||||
// Render from a down-scaled copy of the art so the full-resolution source
|
||||
// pixmap is never re-sampled at a tiny device size (also much cheaper than
|
||||
// scaling it on every frame).
|
||||
const QPixmap scaledPixmap = scaledPlaymatFor(srcRect, painter->worldTransform().mapRect(dstRect).size());
|
||||
painter->drawPixmap(dstRect, scaledPixmap, QRectF(scaledPixmap.rect()));
|
||||
|
||||
painter->restore();
|
||||
|
||||
if (!playmatAttribution.isEmpty()) {
|
||||
paintArtAttribution(*painter, combinedArea, playmatAttribution, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap PlayerGraphicsItem::scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize)
|
||||
{
|
||||
// Bucket the render size so the source pixmap is re-scaled at most once per
|
||||
// zoom step instead of once per frame.
|
||||
constexpr int bucketSize = 32;
|
||||
const QSize target = QSize(qMax(1, qRound(deviceDstSize.width() / bucketSize) * bucketSize),
|
||||
qMax(1, qRound(deviceDstSize.height() / bucketSize) * bucketSize))
|
||||
.boundedTo(srcRect.toAlignedRect().size());
|
||||
|
||||
if (scaledPlaymatKey != target) {
|
||||
const QPixmap crop = playmatPixmap.copy(srcRect.toAlignedRect());
|
||||
scaledPlaymatPixmap = crop.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
scaledPlaymatKey = target;
|
||||
}
|
||||
return scaledPlaymatPixmap;
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::processSceneSizeChange(int newPlayerWidth)
|
||||
|
|
@ -303,3 +366,100 @@ void PlayerGraphicsItem::updateBoundingRect()
|
|||
|
||||
emit sizeChanged();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::updatePlaymat()
|
||||
{
|
||||
int visibility = SettingsCache::instance().userInterface().getPlaymatVisibility();
|
||||
|
||||
// "Don't use playmats" — never show
|
||||
if (visibility == PlaymatVisibilityNone) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
// "Show own playmat only" — hide playmats for remote players
|
||||
if (visibility == PlaymatVisibilityOwnOnly && !player->getPlayerInfo()->getLocal()) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
CardRef playmatCard;
|
||||
PlaymatParams params;
|
||||
|
||||
if (player->getHasRemotePlaymat()) {
|
||||
// Prefer the server-confirmed playmat (updated by Command_SetPlaymat).
|
||||
playmatCard = player->getRemotePlaymatCard();
|
||||
params = player->getRemotePlaymatParams();
|
||||
} else if (player->getPlayerInfo()->getLocal()) {
|
||||
// Local player without a server broadcast yet: apply the full
|
||||
// settings-based resolution chain (mode, fallback list, behavior).
|
||||
const auto &settings = SettingsCache::instance().userInterface();
|
||||
const PlaymatInfo resolved = resolvePlaymatForDeck(
|
||||
player->getDeck(), settings.getPlaymatFallbackList(), static_cast<PlaymatMode>(settings.getPlaymatMode()),
|
||||
static_cast<PlaymatFallbackMode>(settings.getPlaymatFallbackBehavior()), 0);
|
||||
playmatCard = resolved.card;
|
||||
params = resolved.params;
|
||||
} else {
|
||||
// Opponent without a server broadcast: use the deck-embedded playmat.
|
||||
const DeckList &deck = player->getDeck();
|
||||
const PlaymatInfo &deckPlaymat = deck.getPlaymat();
|
||||
if (!deckPlaymat.card.isEmpty()) {
|
||||
playmatCard = deckPlaymat.card;
|
||||
params = deckPlaymat.params;
|
||||
}
|
||||
}
|
||||
|
||||
if (playmatCard.isEmpty()) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
playmatParams = params;
|
||||
scaledPlaymatKey = QSize(); // the art crop depends on the params, drop any cached scale
|
||||
|
||||
ExactCard card = CardDatabaseManager::query()->getCard(playmatCard);
|
||||
if (!card) {
|
||||
clearPlaymat();
|
||||
return;
|
||||
}
|
||||
|
||||
playmatAttribution = buildArtAttribution(card);
|
||||
|
||||
QPixmap fullRes;
|
||||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (fullRes.isNull()) {
|
||||
disconnect(playmatPixmapConnection);
|
||||
CardInfo *cardInfo = card.getCardPtr().data();
|
||||
if (cardInfo) {
|
||||
playmatPixmapConnection =
|
||||
connect(cardInfo, &CardInfo::pixmapUpdated, this, &PlayerGraphicsItem::onPlaymatPixmapReady);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPlaymat) {
|
||||
hasPlaymat = true;
|
||||
emit playmatChanged(true);
|
||||
}
|
||||
playmatPixmap = fullRes;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::clearPlaymat()
|
||||
{
|
||||
disconnect(playmatPixmapConnection);
|
||||
playmatAttribution.clear();
|
||||
if (hasPlaymat) {
|
||||
hasPlaymat = false;
|
||||
playmatPixmap = QPixmap();
|
||||
scaledPlaymatKey = QSize();
|
||||
emit playmatChanged(false);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerGraphicsItem::onPlaymatPixmapReady()
|
||||
{
|
||||
updatePlaymat();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "../game_scene.h"
|
||||
|
||||
#include <QGraphicsObject>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class HandZone;
|
||||
class PileZone;
|
||||
|
|
@ -126,6 +127,7 @@ signals:
|
|||
void playerCountChanged();
|
||||
void mirroredChanged(bool isMirrored);
|
||||
void cardInfoRequested(const CardRef &cardRef);
|
||||
void playmatChanged(bool hasPlaymat);
|
||||
|
||||
private:
|
||||
PlayerLogic *player;
|
||||
|
|
@ -146,9 +148,23 @@ private:
|
|||
bool mirrored;
|
||||
bool handVisible = false;
|
||||
|
||||
QPixmap playmatPixmap;
|
||||
QPixmap scaledPlaymatPixmap; // down-scaled copy of playmatPixmap for the current render size
|
||||
QSize scaledPlaymatKey; // size bucket scaledPlaymatPixmap was rendered for
|
||||
PlaymatParams playmatParams;
|
||||
QString playmatAttribution;
|
||||
bool hasPlaymat = false;
|
||||
QMetaObject::Connection playmatPixmapConnection;
|
||||
|
||||
private slots:
|
||||
void updateBoundingRect();
|
||||
void rearrangeZones();
|
||||
void clearPlaymat();
|
||||
void updatePlaymat();
|
||||
void onPlaymatPixmapReady();
|
||||
|
||||
private:
|
||||
QPixmap scaledPlaymatFor(const QRectF &srcRect, const QSizeF &deviceDstSize);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYER_GRAPHICS_ITEM_H
|
||||
|
|
|
|||
|
|
@ -31,8 +31,22 @@ QRectF StackZone::boundingRect() const
|
|||
|
||||
void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish stack zone from table zone (slightly darker)
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 80));
|
||||
} else {
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Stack, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
}
|
||||
}
|
||||
|
||||
void StackZone::onPlaymatChanged(bool active)
|
||||
{
|
||||
playmatActive = active;
|
||||
// See TableZone::onPlaymatChanged for the rationale. Translucent overlay
|
||||
// over a dynamic playmat should not be held in the device cache.
|
||||
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||
update();
|
||||
}
|
||||
|
||||
void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ class StackZone : public SelectZone
|
|||
Q_OBJECT
|
||||
private:
|
||||
qreal zoneHeight;
|
||||
bool playmatActive = false;
|
||||
private slots:
|
||||
void updateBg();
|
||||
|
||||
public slots:
|
||||
void onPlaymatChanged(bool active);
|
||||
|
||||
public:
|
||||
StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent);
|
||||
/** @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. */
|
||||
|
|
|
|||
|
|
@ -92,14 +92,19 @@ bool TableZone::isInverted() const
|
|||
|
||||
void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
||||
{
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
if (playmatActive) {
|
||||
// Subtle overlay to distinguish table zone from stack zone
|
||||
painter->fillRect(boundingRect(), QColor(0, 0, 0, 60));
|
||||
} else {
|
||||
QBrush brush = themeManager->getExtraBgBrush(ThemeManager::Table, getLogic()->getPlayer()->getZoneId());
|
||||
painter->fillRect(boundingRect(), brush);
|
||||
}
|
||||
|
||||
if (active) {
|
||||
paintZoneOutline(painter);
|
||||
} else {
|
||||
// inactive player gets a darker table zone with a semi transparent black mask
|
||||
// this means if the user provides a custom background it will fade
|
||||
// this means if the user provides a custom background or playmat it will fade
|
||||
painter->fillRect(boundingRect(), FADE_MASK);
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +118,17 @@ void TableZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
|
|||
paintLandDivider(painter);
|
||||
}
|
||||
|
||||
void TableZone::onPlaymatChanged(bool active)
|
||||
{
|
||||
playmatActive = active;
|
||||
// While a playmat is shown the zone paints a translucent overlay over the
|
||||
// dynamic playmat behind it. Keep it out of the device cache so the cached
|
||||
// pixels are never stale relative to the playmat (and to avoid compositing
|
||||
// artifacts of cached translucent content on some platforms).
|
||||
setCacheMode(active ? QGraphicsItem::NoCache : QGraphicsItem::DeviceCoordinateCache);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
Render a soft outline around the edge of the TableZone.
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ private:
|
|||
*/
|
||||
bool active = false;
|
||||
bool mirrored = false;
|
||||
bool playmatActive = false;
|
||||
|
||||
[[nodiscard]] bool isInverted() const;
|
||||
|
||||
|
|
@ -95,6 +96,9 @@ private slots:
|
|||
*/
|
||||
void updateBg();
|
||||
|
||||
public slots:
|
||||
void onPlaymatChanged(bool active);
|
||||
|
||||
public slots:
|
||||
/**
|
||||
Reorganizes CardItems in the TableZone
|
||||
|
|
@ -184,8 +188,17 @@ public:
|
|||
}
|
||||
void setWidth(qreal _width)
|
||||
{
|
||||
// The width is stored as an int; truncate to match the previous implicit conversion.
|
||||
const int newWidth = static_cast<int>(_width);
|
||||
if (width == newWidth) {
|
||||
return;
|
||||
}
|
||||
prepareGeometryChange();
|
||||
width = _width;
|
||||
width = newWidth;
|
||||
// The parent player item's boundingRect (which clips the playmat painting) is
|
||||
// derived from this zone's size. Without this signal the playmat is cut off at
|
||||
// the stale boundingRect edge whenever the scene is resized wider.
|
||||
emit sizeChanged();
|
||||
}
|
||||
[[nodiscard]] qreal getWidth() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ struct ContextJoinGame
|
|||
{
|
||||
ContextJoinRoom roomContext;
|
||||
int gameId;
|
||||
bool asSpectator = false;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CONTEXT_JOIN_GAME_H
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ bool IntentJoinServerGame::tryJoinGame(TabRoom *room)
|
|||
return false;
|
||||
}
|
||||
|
||||
if (room->getGameSelector()->joinGameById(context->gameId)) {
|
||||
if (room->getGameSelector()->joinGameById(context->gameId, context->asSpectator)) {
|
||||
emitFinished();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../playmat/playmat_settings_dialog.h"
|
||||
#include "../settings_page/user_interface_settings_page.h"
|
||||
#include "../tabs/api/commander_spellbook/commander_bracket_widget.h"
|
||||
#include "deck_list_style_proxy.h"
|
||||
|
|
@ -11,10 +12,12 @@
|
|||
#include <QDockWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTextEdit>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
#include <libcockatrice/utility/macros.h>
|
||||
#include <libcockatrice/utility/string_limits.h>
|
||||
|
||||
|
|
@ -228,10 +231,18 @@ void DeckEditorDeckDockWidget::createDeckDock()
|
|||
upperLayout->addWidget(bannerCardLabel, 4, 0);
|
||||
upperLayout->addWidget(bannerCardComboBox, 4, 1);
|
||||
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
|
||||
playmatLabel = new QLabel();
|
||||
playmatLabel->setObjectName("playmatLabel");
|
||||
playmatLabel->setText(tr("Playmat"));
|
||||
playmatSettingsButton = new QPushButton(tr("Edit Playmat..."));
|
||||
connect(playmatSettingsButton, &QPushButton::clicked, this, &DeckEditorDeckDockWidget::openPlaymatSettings);
|
||||
upperLayout->addWidget(playmatLabel, 5, 0);
|
||||
upperLayout->addWidget(playmatSettingsButton, 5, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
|
||||
upperLayout->addWidget(deckTagsDisplayWidget, 6, 1);
|
||||
|
||||
upperLayout->addWidget(activeGroupCriteriaLabel, 7, 0);
|
||||
upperLayout->addWidget(activeGroupCriteriaComboBox, 7, 1);
|
||||
|
||||
hashLabel1 = new QLabel();
|
||||
hashLabel1->setObjectName("hashLabel1");
|
||||
|
|
@ -440,6 +451,35 @@ void DeckEditorDeckDockWidget::writeBannerCard(int index)
|
|||
deckStateManager->setBannerCard(bannerCard);
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::openPlaymatSettings()
|
||||
{
|
||||
PlaymatInfo current = deckStateManager->getMetadata().playmat;
|
||||
|
||||
PlaymatSettingsDialog dialog(current.card, current.params, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
CardRef newCard = dialog.card();
|
||||
PlaymatParams newParams = dialog.params();
|
||||
|
||||
if (newCard.isEmpty()) {
|
||||
deckStateManager->setPlaymat(PlaymatInfo{});
|
||||
} else {
|
||||
deckStateManager->setPlaymat({newCard, newParams});
|
||||
}
|
||||
|
||||
updatePlaymatLabel();
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::updatePlaymatLabel()
|
||||
{
|
||||
CardRef playmat = deckStateManager->getMetadata().playmat.card;
|
||||
if (playmat.isEmpty()) {
|
||||
playmatSettingsButton->setText(tr("Edit Playmat..."));
|
||||
} else {
|
||||
playmatSettingsButton->setText(tr("Edit Playmat (%1)").arg(playmat.name));
|
||||
}
|
||||
}
|
||||
|
||||
void DeckEditorDeckDockWidget::applyActiveGroupCriteria()
|
||||
{
|
||||
getModel()->setActiveGroupCriteria(
|
||||
|
|
@ -497,6 +537,7 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
|
|||
syncBannerCardComboBoxSelectionWithDeck();
|
||||
updateBannerCardComboBox();
|
||||
bannerCardComboBox->blockSignals(false);
|
||||
updatePlaymatLabel();
|
||||
updateHash();
|
||||
|
||||
formatComboBox->blockSignals(true);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <QTextEdit>
|
||||
#include <QTreeView>
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class CommanderBracketWidget;
|
||||
class DeckListModel;
|
||||
|
|
@ -33,6 +34,8 @@ public:
|
|||
DeckListStyleProxy *proxy;
|
||||
QTreeView *deckView;
|
||||
QComboBox *bannerCardComboBox;
|
||||
QLabel *playmatLabel;
|
||||
QPushButton *playmatSettingsButton;
|
||||
void createDeckDock();
|
||||
ExactCard getCurrentCard();
|
||||
void retranslateUi();
|
||||
|
|
@ -102,6 +105,8 @@ private slots:
|
|||
void writeName();
|
||||
void writeComments();
|
||||
void writeBannerCard(int);
|
||||
void openPlaymatSettings();
|
||||
void updatePlaymatLabel();
|
||||
void applyActiveGroupCriteria();
|
||||
void setSelectedIndex(const QModelIndex &newCardIndex, bool preserveWidgetFocus);
|
||||
void updateHash();
|
||||
|
|
|
|||
|
|
@ -142,6 +142,19 @@ void DeckStateManager::setBannerCard(const CardRef &bannerCard)
|
|||
doMetadataModified();
|
||||
}
|
||||
|
||||
void DeckStateManager::setPlaymat(const PlaymatInfo &playmat)
|
||||
{
|
||||
PlaymatInfo previous = deckList->getPlaymat();
|
||||
if (previous == playmat) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestHistorySave(tr("Set playmat to %1").arg(playmat.card.name));
|
||||
deckList->setPlaymat(playmat);
|
||||
|
||||
doMetadataModified();
|
||||
}
|
||||
|
||||
void DeckStateManager::setTags(const QStringList &tags)
|
||||
{
|
||||
QStringList previous = deckList->getTags();
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public:
|
|||
void setName(const QString &name);
|
||||
void setComments(const QString &comments);
|
||||
void setBannerCard(const CardRef &bannerCard);
|
||||
void setPlaymat(const PlaymatInfo &playmat);
|
||||
void setTags(const QStringList &tags);
|
||||
void setFormat(const QString &format);
|
||||
///@}
|
||||
|
|
|
|||
297
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp
Normal file
297
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.cpp
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
#include "dlg_my_reports.h"
|
||||
|
||||
#include "../utility/report_utils.h"
|
||||
#include "abstract_client.h"
|
||||
|
||||
#include <QFontDatabase>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_my_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_my_list.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int COL_ID = 0;
|
||||
constexpr int COL_TIME = 1;
|
||||
constexpr int COL_REPORTED = 2;
|
||||
constexpr int COL_CATEGORY = 3;
|
||||
constexpr int COL_GAMEID = 4;
|
||||
constexpr int COL_STATUS = 5;
|
||||
constexpr int COL_ASSIGNED = 6;
|
||||
constexpr int COL_COUNT = 7;
|
||||
} // namespace
|
||||
|
||||
DlgMyReports::DlgMyReports(AbstractClient *_client, QWidget *parent)
|
||||
: QDialog(parent), client(_client), selectedReportId(-1)
|
||||
{
|
||||
setWindowTitle(tr("My Reports"));
|
||||
setMinimumSize(800, 500);
|
||||
|
||||
table = new QTableWidget(0, COL_COUNT);
|
||||
table->setHorizontalHeaderLabels(
|
||||
{tr("#"), tr("Time"), tr("Reported User"), tr("Category"), tr("Game ID"), tr("Status"), tr("Assigned To")});
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSortingEnabled(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_TIME, QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
connect(table, &QTableWidget::itemSelectionChanged, this, &DlgMyReports::onSelectionChanged);
|
||||
|
||||
auto *detailsGroup = new QGroupBox(tr("Report Details"));
|
||||
descriptionEdit = new QTextEdit;
|
||||
descriptionEdit->setReadOnly(true);
|
||||
descriptionEdit->setFixedHeight(80);
|
||||
|
||||
auto *chatGroup = new QGroupBox(tr("Chat Log Context"));
|
||||
chatLogEdit = new QTextEdit;
|
||||
chatLogEdit->setReadOnly(true);
|
||||
QFont monoFont("monospace");
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
|
||||
if (systemPointSize > 0) {
|
||||
monoFont.setPointSize(systemPointSize);
|
||||
}
|
||||
chatLogEdit->setFont(monoFont);
|
||||
auto *chatLayout = new QVBoxLayout(chatGroup);
|
||||
chatLayout->setContentsMargins(4, 4, 4, 4);
|
||||
chatLayout->addWidget(chatLogEdit);
|
||||
|
||||
auto *commentsLabel = new QLabel(tr("Comments:"));
|
||||
commentsEdit = new QTextEdit;
|
||||
commentsEdit->setReadOnly(true);
|
||||
commentsEdit->setFixedHeight(120);
|
||||
|
||||
auto *addCommentLabel = new QLabel(tr("Add a comment:"));
|
||||
commentInput = new QLineEdit;
|
||||
commentInput->setPlaceholderText(tr("Type your comment here..."));
|
||||
commentButton = new QPushButton(tr("Send"));
|
||||
commentButton->setEnabled(false);
|
||||
connect(commentButton, &QPushButton::clicked, this, &DlgMyReports::addComment);
|
||||
connect(commentInput, &QLineEdit::returnPressed, this, &DlgMyReports::addComment);
|
||||
|
||||
auto *detailsLayout = new QVBoxLayout(detailsGroup);
|
||||
detailsLayout->setContentsMargins(4, 4, 4, 4);
|
||||
detailsLayout->addWidget(descriptionEdit);
|
||||
detailsLayout->addWidget(chatGroup);
|
||||
detailsLayout->addWidget(commentsLabel);
|
||||
detailsLayout->addWidget(commentsEdit);
|
||||
detailsLayout->addWidget(addCommentLabel);
|
||||
auto *commentRow = new QHBoxLayout;
|
||||
commentRow->addWidget(commentInput);
|
||||
commentRow->addWidget(commentButton);
|
||||
detailsLayout->addLayout(commentRow);
|
||||
|
||||
closeButton = new QPushButton(tr("Close"));
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
|
||||
refreshButton = new QPushButton(tr("Refresh"));
|
||||
connect(refreshButton, &QPushButton::clicked, this, &DlgMyReports::refreshList);
|
||||
|
||||
statusLabel = new QLabel;
|
||||
|
||||
auto *bottomBar = new QHBoxLayout;
|
||||
bottomBar->addWidget(statusLabel);
|
||||
bottomBar->addStretch();
|
||||
bottomBar->addWidget(refreshButton);
|
||||
bottomBar->addWidget(closeButton);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(table, 1);
|
||||
layout->addWidget(detailsGroup);
|
||||
layout->addLayout(bottomBar);
|
||||
|
||||
setActionsEnabled(false);
|
||||
refreshList();
|
||||
}
|
||||
|
||||
void DlgMyReports::refreshList()
|
||||
{
|
||||
selectedReportIdBeforeRefresh = selectedReportId;
|
||||
commentDraftBeforeRefresh = commentInput->text();
|
||||
statusLabel->setText(tr("Loading..."));
|
||||
refreshButton->setEnabled(false);
|
||||
table->setRowCount(0);
|
||||
currentReports.clear();
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
commentInput->clear();
|
||||
setActionsEnabled(false);
|
||||
selectedReportId = -1;
|
||||
|
||||
Command_ReportMyList cmd;
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportListResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DlgMyReports::reportListResponse(const Response &response)
|
||||
{
|
||||
refreshButton->setEnabled(true);
|
||||
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("Failed to load reports."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportMyList &resp = response.GetExtension(Response_ReportMyList::ext);
|
||||
currentReports.clear();
|
||||
for (int i = 0; i < resp.reports_size(); ++i) {
|
||||
currentReports.append(resp.reports(i));
|
||||
}
|
||||
|
||||
table->setSortingEnabled(false);
|
||||
table->setRowCount(currentReports.size());
|
||||
|
||||
for (int row = 0; row < currentReports.size(); ++row) {
|
||||
const ServerInfo_Report &r = currentReports[row];
|
||||
|
||||
report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID,
|
||||
COL_STATUS, COL_ASSIGNED);
|
||||
}
|
||||
|
||||
table->setSortingEnabled(true);
|
||||
table->resizeColumnsToContents();
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
|
||||
if (selectedReportIdBeforeRefresh >= 0) {
|
||||
for (int row = 0; row < table->rowCount(); ++row) {
|
||||
if (table->item(row, COL_ID) &&
|
||||
table->item(row, COL_ID)->data(Qt::UserRole).toInt() == selectedReportIdBeforeRefresh) {
|
||||
table->setCurrentCell(row, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (commentInput->text().isEmpty()) {
|
||||
commentInput->setText(commentDraftBeforeRefresh);
|
||||
}
|
||||
|
||||
statusLabel->setText(tr("%1 report(s)").arg(currentReports.size()));
|
||||
}
|
||||
|
||||
void DlgMyReports::onSelectionChanged()
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_ID)) {
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
commentInput->clear();
|
||||
commentButton->setEnabled(false);
|
||||
selectedReportId = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const int reportId = table->item(row, COL_ID)->data(Qt::UserRole).toInt();
|
||||
selectedReportId = reportId;
|
||||
|
||||
for (const ServerInfo_Report &r : currentReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
descriptionEdit->setPlainText(QString::fromStdString(r.description()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chatLogEdit->setPlainText(tr("Loading..."));
|
||||
commentsEdit->setPlainText(tr("Loading..."));
|
||||
|
||||
Command_ReportDetails cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::reportDetailsResponse);
|
||||
client->sendCommand(pend);
|
||||
|
||||
QString status = table->item(row, COL_STATUS)->text();
|
||||
bool canComment = (status == "open" || status == "assigned");
|
||||
commentButton->setEnabled(canComment);
|
||||
commentInput->setEnabled(canComment);
|
||||
if (!canComment) {
|
||||
commentInput->setPlaceholderText(tr("This report is closed."));
|
||||
} else {
|
||||
commentInput->setPlaceholderText(tr("Type your comment here..."));
|
||||
}
|
||||
}
|
||||
|
||||
void DlgMyReports::reportDetailsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
if (selectedReportId == -1) {
|
||||
return;
|
||||
}
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->setPlainText(tr("Failed to load report details."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportDetails &resp = response.GetExtension(Response_ReportDetails::ext);
|
||||
const ServerInfo_Report &r = resp.report();
|
||||
|
||||
if (selectedReportId != r.report_id()) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadReportDetails(r);
|
||||
}
|
||||
|
||||
void DlgMyReports::loadReportDetails(const ServerInfo_Report &report)
|
||||
{
|
||||
report_utils::renderReportDetails(chatLogEdit, commentsEdit, report, tr("No comments yet."), tr("[Moderator]"),
|
||||
tr("[You]"));
|
||||
}
|
||||
|
||||
void DlgMyReports::addComment()
|
||||
{
|
||||
if (selectedReportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = commentInput->text().trimmed();
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentButton->setEnabled(false);
|
||||
|
||||
Command_ReportAddComment cmd;
|
||||
cmd.set_report_id(selectedReportId);
|
||||
cmd.set_comment(text.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgMyReports::addCommentResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DlgMyReports::addCommentResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
commentInput->clear();
|
||||
refreshList();
|
||||
} else {
|
||||
commentButton->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DlgMyReports::setActionsEnabled(bool enabled)
|
||||
{
|
||||
commentButton->setEnabled(enabled);
|
||||
commentInput->setEnabled(enabled);
|
||||
}
|
||||
52
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h
Normal file
52
cockatrice/src/interface/widgets/dialogs/dlg_my_reports.h
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#ifndef COCKATRICE_DLG_MY_REPORTS_H
|
||||
#define COCKATRICE_DLG_MY_REPORTS_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
|
||||
class DlgMyReports : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgMyReports(AbstractClient *_client, QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void refreshList();
|
||||
void reportListResponse(const Response &response);
|
||||
void onSelectionChanged();
|
||||
void reportDetailsResponse(const Response &response);
|
||||
void addComment();
|
||||
void addCommentResponse(const Response &response);
|
||||
|
||||
private:
|
||||
void loadReportDetails(const ServerInfo_Report &report);
|
||||
void setActionsEnabled(bool enabled);
|
||||
|
||||
AbstractClient *client;
|
||||
|
||||
QTableWidget *table;
|
||||
QTextEdit *descriptionEdit;
|
||||
QTextEdit *chatLogEdit;
|
||||
QTextEdit *commentsEdit;
|
||||
QLineEdit *commentInput;
|
||||
QPushButton *commentButton;
|
||||
QPushButton *refreshButton;
|
||||
QPushButton *closeButton;
|
||||
QLabel *statusLabel;
|
||||
|
||||
QList<ServerInfo_Report> currentReports;
|
||||
int selectedReportId;
|
||||
int selectedReportIdBeforeRefresh = -1;
|
||||
QString commentDraftBeforeRefresh;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DLG_MY_REPORTS_H
|
||||
191
cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp
Normal file
191
cockatrice/src/interface/widgets/dialogs/dlg_report_user.cpp
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
#include "dlg_report_user.h"
|
||||
|
||||
#include "abstract_client.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFontDatabase>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
DlgReportUser::DlgReportUser(AbstractClient *_client,
|
||||
const QString &_reportedUser,
|
||||
int _gameId,
|
||||
const QString &_autoChatLog,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), client(_client), reportedUser(_reportedUser), gameId(_gameId)
|
||||
{
|
||||
setWindowTitle(tr("Report User"));
|
||||
setMinimumWidth(500);
|
||||
|
||||
auto *infoLabel =
|
||||
new QLabel(tr("Reports are reviewed by moderators. False reports may result in account penalties."));
|
||||
infoLabel->setWordWrap(true);
|
||||
infoLabel->setStyleSheet("color: palette(placeholderText); padding: 5px;");
|
||||
|
||||
auto *reportGroup = new QGroupBox(tr("Report Details"));
|
||||
auto *reportGrid = new QGridLayout(reportGroup);
|
||||
|
||||
reportGrid->addWidget(new QLabel(tr("Reported User:")), 0, 0);
|
||||
reportedUserLabel = new QLabel(reportedUser);
|
||||
reportedUserLabel->setStyleSheet("font-weight: bold;");
|
||||
reportGrid->addWidget(reportedUserLabel, 0, 1);
|
||||
|
||||
reportGrid->addWidget(new QLabel(tr("Game ID:")), 1, 0);
|
||||
if (gameId >= 0) {
|
||||
gameIdLabel = new QLabel(QString::number(gameId));
|
||||
gameIdLabel->setStyleSheet("font-weight: bold;");
|
||||
reportGrid->addWidget(gameIdLabel, 1, 1);
|
||||
} else {
|
||||
gameIdEdit = new QLineEdit;
|
||||
gameIdEdit->setPlaceholderText(tr("(Optional) Enter game ID if available"));
|
||||
gameIdEdit->setToolTip(tr("If the report is related to a specific game, enter its ID."));
|
||||
reportGrid->addWidget(gameIdEdit, 1, 1);
|
||||
gameIdLabel = nullptr;
|
||||
}
|
||||
|
||||
auto *categoryGroup = new QGroupBox(tr("Category"));
|
||||
auto *categoryGrid = new QGridLayout(categoryGroup);
|
||||
|
||||
categoryBox = new QComboBox;
|
||||
categoryBox->addItem(tr("Cheating / Unsporting behavior"), "cheating");
|
||||
categoryBox->setItemData(categoryBox->count() - 1,
|
||||
tr("Using external tools, card marked manipulation, or exploiting game bugs"),
|
||||
Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Harassment / Abuse"), "harassment");
|
||||
categoryBox->setItemData(categoryBox->count() - 1, tr("Threatening, bullying, or persistent unwanted contact"),
|
||||
Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Hate speech"), "hate_speech");
|
||||
categoryBox->setItemData(categoryBox->count() - 1,
|
||||
tr("Discriminatory language targeting race, gender, religion, etc."), Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Spam"), "spam");
|
||||
categoryBox->setItemData(categoryBox->count() - 1, tr("Repeated unwanted messages or advertisements"),
|
||||
Qt::ToolTipRole);
|
||||
categoryBox->addItem(tr("Other"), "other");
|
||||
categoryBox->setItemData(categoryBox->count() - 1, tr("Any behavior not covered by the above categories"),
|
||||
Qt::ToolTipRole);
|
||||
|
||||
categoryGrid->addWidget(new QLabel(tr("Category:")), 0, 0);
|
||||
categoryGrid->addWidget(categoryBox, 0, 1);
|
||||
|
||||
auto *descGroup = new QGroupBox(tr("Description"));
|
||||
auto *descLayout = new QVBoxLayout(descGroup);
|
||||
|
||||
descriptionEdit = new QTextEdit;
|
||||
descriptionEdit->setPlaceholderText(
|
||||
tr("Please describe what happened. Include dates, game details, or any evidence if available."));
|
||||
descriptionEdit->setFixedHeight(120);
|
||||
descLayout->addWidget(descriptionEdit);
|
||||
|
||||
auto *chatGroup = new QGroupBox(tr("Chat Log Context"));
|
||||
auto *chatLayout = new QVBoxLayout(chatGroup);
|
||||
chatLogEdit = new QTextEdit;
|
||||
chatLogEdit->setReadOnly(true);
|
||||
QFont monoFont("monospace");
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
|
||||
if (systemPointSize > 0) {
|
||||
monoFont.setPointSize(systemPointSize);
|
||||
}
|
||||
chatLogEdit->setFont(monoFont);
|
||||
if (!_autoChatLog.isEmpty()) {
|
||||
chatLogEdit->setPlainText(_autoChatLog);
|
||||
} else {
|
||||
chatLogEdit->setPlaceholderText(tr("No chat context available (not triggered from chat)."));
|
||||
}
|
||||
chatLogEdit->setFixedHeight(100);
|
||||
chatLayout->addWidget(chatLogEdit);
|
||||
|
||||
auto *chatNote = new QLabel(
|
||||
tr("This chat log is captured from your local chat window and may not reflect the full conversation."));
|
||||
chatNote->setWordWrap(true);
|
||||
chatNote->setStyleSheet("color: palette(placeholderText);");
|
||||
chatLayout->addWidget(chatNote);
|
||||
|
||||
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submit Report"));
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgReportUser::actSubmit);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(infoLabel);
|
||||
layout->addWidget(reportGroup);
|
||||
layout->addWidget(categoryGroup);
|
||||
layout->addWidget(descGroup);
|
||||
layout->addWidget(chatGroup);
|
||||
layout->addWidget(buttonBox);
|
||||
}
|
||||
|
||||
void DlgReportUser::actSubmit()
|
||||
{
|
||||
const QString description = descriptionEdit->toPlainText().trimmed();
|
||||
if (description.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Missing description"), tr("Please describe what happened before submitting."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply =
|
||||
QMessageBox::question(this, tr("Confirm Report"),
|
||||
tr("Submit report against %1 for %2?").arg(reportedUser, categoryBox->currentText()),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
buttonBox->setEnabled(false);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submitting..."));
|
||||
|
||||
Command_Report cmd;
|
||||
cmd.set_reported_user(reportedUser.toStdString());
|
||||
cmd.set_category(categoryBox->currentData().toString().toStdString());
|
||||
cmd.set_description(description.toStdString());
|
||||
|
||||
if (gameId >= 0) {
|
||||
cmd.set_game_id(gameId);
|
||||
} else if (gameIdEdit && !gameIdEdit->text().trimmed().isEmpty()) {
|
||||
bool ok;
|
||||
int manualGameId = gameIdEdit->text().trimmed().toInt(&ok);
|
||||
if (ok && manualGameId > 0) {
|
||||
cmd.set_game_id(manualGameId);
|
||||
}
|
||||
}
|
||||
|
||||
const QString chatLog = chatLogEdit->toPlainText().trimmed();
|
||||
if (!chatLog.isEmpty()) {
|
||||
cmd.set_chat_log(chatLog.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &DlgReportUser::reportResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void DlgReportUser::reportResponse(const Response &response)
|
||||
{
|
||||
buttonBox->setEnabled(true);
|
||||
buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Submit Report"));
|
||||
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
QMessageBox::information(this, tr("Report Submitted"),
|
||||
tr("Your report has been submitted and will be reviewed by a moderator. Thank you."));
|
||||
accept();
|
||||
} else if (response.response_code() == Response::RespTooManyRequests) {
|
||||
QMessageBox::warning(this, tr("Submission Failed"),
|
||||
tr("You have reached the daily report limit. Please try again later."));
|
||||
} else if (response.response_code() == Response::RespNameNotFound) {
|
||||
QMessageBox::warning(
|
||||
this, tr("Submission Failed"),
|
||||
tr("The reported user could not be found. Guests (unregistered users) cannot be reported."));
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Submission Failed"), tr("Failed to submit report. Please try again."));
|
||||
}
|
||||
}
|
||||
42
cockatrice/src/interface/widgets/dialogs/dlg_report_user.h
Normal file
42
cockatrice/src/interface/widgets/dialogs/dlg_report_user.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#ifndef COCKATRICE_DLG_REPORT_USER_H
|
||||
#define COCKATRICE_DLG_REPORT_USER_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QComboBox;
|
||||
class QDialogButtonBox;
|
||||
class QLineEdit;
|
||||
class QTextEdit;
|
||||
class QLabel;
|
||||
|
||||
class DlgReportUser : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
DlgReportUser(AbstractClient *_client,
|
||||
const QString &_reportedUser,
|
||||
int _gameId = -1,
|
||||
const QString &_autoChatLog = QString(),
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void actSubmit();
|
||||
void reportResponse(const Response &response);
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
QString reportedUser;
|
||||
int gameId;
|
||||
|
||||
QLabel *reportedUserLabel;
|
||||
QLabel *gameIdLabel = nullptr;
|
||||
QLineEdit *gameIdEdit = nullptr;
|
||||
QComboBox *categoryBox;
|
||||
QTextEdit *descriptionEdit;
|
||||
QTextEdit *chatLogEdit;
|
||||
QDialogButtonBox *buttonBox;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_DLG_REPORT_USER_H
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
#include "playmat_collection_dialog.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "playmat_settings_dialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
PlaymatCollectionDialog::PlaymatCollectionDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
setMinimumWidth(420);
|
||||
setupUi();
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::accept()
|
||||
{
|
||||
auto &interfaceSettings = SettingsCache::instance().userInterface();
|
||||
interfaceSettings.setPlaymatFallbackList(playmats);
|
||||
interfaceSettings.setPlaymatFallbackBehavior(modeCombo->currentData().toInt());
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
int PlaymatCollectionDialog::currentRow() const
|
||||
{
|
||||
return playmatList->currentRow();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::setupUi()
|
||||
{
|
||||
auto &interfaceSettings = SettingsCache::instance().userInterface();
|
||||
playmats = interfaceSettings.getPlaymatFallbackList();
|
||||
|
||||
playmatList = new QListWidget;
|
||||
for (const PlaymatInfo &entry : playmats) {
|
||||
playmatList->addItem(entry.card.name);
|
||||
}
|
||||
connect(playmatList, &QListWidget::itemSelectionChanged, this, &PlaymatCollectionDialog::selectionChanged);
|
||||
connect(playmatList, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { editPlaymat(); });
|
||||
|
||||
addButton = new QPushButton;
|
||||
editButton = new QPushButton;
|
||||
removeButton = new QPushButton;
|
||||
moveUpButton = new QPushButton;
|
||||
moveDownButton = new QPushButton;
|
||||
|
||||
connect(addButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::addPlaymat);
|
||||
connect(editButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::editPlaymat);
|
||||
connect(removeButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::removePlaymat);
|
||||
connect(moveUpButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatUp);
|
||||
connect(moveDownButton, &QPushButton::clicked, this, &PlaymatCollectionDialog::movePlaymatDown);
|
||||
|
||||
auto *listButtons = new QVBoxLayout;
|
||||
listButtons->addWidget(addButton);
|
||||
listButtons->addWidget(editButton);
|
||||
listButtons->addWidget(removeButton);
|
||||
listButtons->addWidget(moveUpButton);
|
||||
listButtons->addWidget(moveDownButton);
|
||||
listButtons->addStretch();
|
||||
|
||||
auto *listRow = new QHBoxLayout;
|
||||
listRow->addWidget(playmatList, 1);
|
||||
listRow->addLayout(listButtons);
|
||||
|
||||
modeCombo = new QComboBox;
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeFixed);
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeRoundRobin);
|
||||
modeCombo->addItem(QString(), PlaymatFallbackModeRandom);
|
||||
const int modeIndex = modeCombo->findData(interfaceSettings.getPlaymatFallbackBehavior());
|
||||
if (modeIndex >= 0) {
|
||||
modeCombo->setCurrentIndex(modeIndex);
|
||||
}
|
||||
|
||||
auto *modeRow = new QHBoxLayout;
|
||||
modeLabel = new QLabel;
|
||||
modeLabel->setBuddy(modeCombo);
|
||||
modeRow->addWidget(modeLabel);
|
||||
modeRow->addWidget(modeCombo, 1);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &PlaymatCollectionDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addLayout(listRow);
|
||||
root->addLayout(modeRow);
|
||||
root->addWidget(buttonBox);
|
||||
setLayout(root);
|
||||
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::selectionChanged()
|
||||
{
|
||||
const bool hasSelection = playmatList->currentRow() >= 0;
|
||||
editButton->setEnabled(hasSelection);
|
||||
removeButton->setEnabled(hasSelection);
|
||||
moveUpButton->setEnabled(hasSelection && playmatList->currentRow() > 0);
|
||||
moveDownButton->setEnabled(hasSelection && playmatList->currentRow() < playmatList->count() - 1);
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::addPlaymat()
|
||||
{
|
||||
PlaymatSettingsDialog dialog(CardRef{}, PlaymatParams{}, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
const CardRef card = dialog.card();
|
||||
if (!card.isEmpty()) {
|
||||
PlaymatInfo res = {card, dialog.params()};
|
||||
playmats.append(res);
|
||||
playmatList->addItem(res.card.name);
|
||||
playmatList->setCurrentRow(playmatList->count() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::editPlaymat()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PlaymatInfo ¤t = playmats.at(row);
|
||||
PlaymatSettingsDialog dialog(current.card, current.params, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
const CardRef card = dialog.card();
|
||||
if (card.isEmpty()) {
|
||||
return; // Removal is handled by the Remove button
|
||||
}
|
||||
playmats[row] = {card, dialog.params()};
|
||||
playmatList->item(row)->setText(card.name);
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::removePlaymat()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0) {
|
||||
return;
|
||||
}
|
||||
playmats.removeAt(row);
|
||||
delete playmatList->takeItem(row);
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::movePlaymatUp()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row <= 0) {
|
||||
return;
|
||||
}
|
||||
playmats.swapItemsAt(row, row - 1);
|
||||
playmatList->insertItem(row - 1, playmatList->takeItem(row));
|
||||
playmatList->setCurrentRow(row - 1);
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::movePlaymatDown()
|
||||
{
|
||||
const int row = currentRow();
|
||||
if (row < 0 || row >= playmats.size() - 1) {
|
||||
return;
|
||||
}
|
||||
playmats.swapItemsAt(row, row + 1);
|
||||
playmatList->insertItem(row + 1, playmatList->takeItem(row));
|
||||
playmatList->setCurrentRow(row + 1);
|
||||
selectionChanged();
|
||||
}
|
||||
|
||||
void PlaymatCollectionDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Default Playmats"));
|
||||
addButton->setText(tr("Add..."));
|
||||
editButton->setText(tr("Edit..."));
|
||||
removeButton->setText(tr("Remove"));
|
||||
moveUpButton->setText(tr("Move Up"));
|
||||
moveDownButton->setText(tr("Move Down"));
|
||||
modeLabel->setText(tr("List mode:"));
|
||||
modeCombo->setItemText(0, tr("Fixed (always the first entry)"));
|
||||
modeCombo->setItemText(1, tr("Round-robin (cycle through entries)"));
|
||||
modeCombo->setItemText(2, tr("Random (pick one per game)"));
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
#define COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
* @brief Dialog for editing the user-level playmat collection.
|
||||
*
|
||||
* The collection is the fallback used when a deck has no playmat of its own.
|
||||
* It supports multiple entries and a pick mode (always first / round-robin /
|
||||
* random). The dialog edits a working copy and writes it to the settings only
|
||||
* when accepted.
|
||||
*/
|
||||
class PlaymatCollectionDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatCollectionDialog(QWidget *parent = nullptr);
|
||||
|
||||
void accept() override;
|
||||
|
||||
private slots:
|
||||
void addPlaymat();
|
||||
void editPlaymat();
|
||||
void removePlaymat();
|
||||
void movePlaymatUp();
|
||||
void movePlaymatDown();
|
||||
void selectionChanged();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void retranslateUi();
|
||||
int currentRow() const;
|
||||
|
||||
QList<PlaymatInfo> playmats; ///< Working copy edited by the dialog.
|
||||
QListWidget *playmatList;
|
||||
QComboBox *modeCombo;
|
||||
QLabel *modeLabel;
|
||||
QPushButton *addButton;
|
||||
QPushButton *editButton;
|
||||
QPushButton *removeButton;
|
||||
QPushButton *moveUpButton;
|
||||
QPushButton *moveDownButton;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_COLLECTION_DIALOG_H
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "playmat_utils.h"
|
||||
|
||||
#include <QLinearGradient>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
|
||||
PlaymatPreviewWidget::PlaymatPreviewWidget(QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
setMinimumSize(400, 120);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setPixmap(const QPixmap &pixmap)
|
||||
{
|
||||
sourcePixmap = pixmap;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setParams(const PlaymatParams &p)
|
||||
{
|
||||
params = p;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::setAttribution(const QString &attribution)
|
||||
{
|
||||
attributionText = attribution;
|
||||
update();
|
||||
}
|
||||
|
||||
void PlaymatPreviewWidget::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
|
||||
|
||||
const QRect rect = this->rect();
|
||||
const QColor accentColor(100, 116, 139);
|
||||
|
||||
// Background
|
||||
const QRectF cardRect = QRectF(rect).adjusted(3, 2, -3, -2);
|
||||
QLinearGradient bg(cardRect.topLeft(), cardRect.topRight());
|
||||
bg.setColorAt(0, accentColor.darker(320));
|
||||
bg.setColorAt(1, QColor(18, 22, 30));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(bg);
|
||||
painter.drawRoundedRect(cardRect, 6, 6);
|
||||
painter.setBrush(accentColor);
|
||||
painter.drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2);
|
||||
|
||||
if (sourcePixmap.isNull()) {
|
||||
painter.setPen(QColor(150, 150, 150));
|
||||
painter.drawText(rect, Qt::AlignCenter, tr("No card selected"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the playmat art using the same logic as PlayerGraphicsItem
|
||||
// The preview area represents the combined stack+table play area
|
||||
// Stack is ~20% width on the left, table is ~80% on the right
|
||||
const QRectF playArea = cardRect.adjusted(6, 4, -4, -4);
|
||||
|
||||
const QRectF srcRect = computeArtSourceRect(sourcePixmap.size(), params);
|
||||
const QRectF dstRect = coverFitRect(playArea, srcRect.size());
|
||||
|
||||
painter.setClipRect(playArea.toRect());
|
||||
painter.drawPixmap(dstRect, sourcePixmap, srcRect);
|
||||
painter.setClipping(false);
|
||||
|
||||
// Draw zone divider: stack is roughly the left portion
|
||||
const double stackWidthRatio = 0.18; // Stack is about 18% of total play area
|
||||
const double stackDividerX = playArea.left() + playArea.width() * stackWidthRatio;
|
||||
|
||||
// Subtle semi-transparent overlays to distinguish zones
|
||||
// Stack zone overlay (slightly darker)
|
||||
QRectF stackOverlay(playArea.left(), playArea.top(), playArea.width() * stackWidthRatio, playArea.height());
|
||||
painter.fillRect(stackOverlay, QColor(0, 0, 0, 40));
|
||||
|
||||
// Table zone overlay (very subtle)
|
||||
QRectF tableOverlay(stackDividerX, playArea.top(), playArea.width() * (1.0 - stackWidthRatio), playArea.height());
|
||||
painter.fillRect(tableOverlay, QColor(0, 0, 0, 20));
|
||||
|
||||
// Zone divider line
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 50), 1));
|
||||
painter.drawLine(QPointF(stackDividerX, playArea.top()), QPointF(stackDividerX, playArea.bottom()));
|
||||
|
||||
// Land divider line (about 60% down the table area)
|
||||
const double landDividerY = playArea.top() + playArea.height() * 0.65;
|
||||
painter.setPen(QPen(QColor(255, 255, 255, 30), 1));
|
||||
painter.drawLine(QPointF(stackDividerX, landDividerY), QPointF(playArea.right(), landDividerY));
|
||||
|
||||
// Border around entire play area
|
||||
painter.setPen(QPen(QColor(70, 80, 95, 120), 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(playArea.adjusted(0, 0, -1, -1), 3, 3);
|
||||
|
||||
paintArtAttribution(painter, playArea, attributionText, Qt::AlignRight | Qt::AlignBottom, 0.8);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
#define COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
/**
|
||||
* @brief Preview widget that shows how a playmat card art will appear
|
||||
* across the combined table + stack play area.
|
||||
*
|
||||
* Renders a miniature mockup with the card art applied using the
|
||||
* given PlaymatParams, including faint zone divider lines.
|
||||
*/
|
||||
class PlaymatPreviewWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatPreviewWidget(QWidget *parent = nullptr);
|
||||
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
void setParams(const PlaymatParams ¶ms);
|
||||
void setAttribution(const QString &attribution);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
QPixmap sourcePixmap;
|
||||
PlaymatParams params;
|
||||
QString attributionText;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_PREVIEW_WIDGET_H
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
#include "playmat_settings_dialog.h"
|
||||
|
||||
#include "../../card_picture_loader/card_picture_loader.h"
|
||||
#include "../cards/art_crop_attribution.h"
|
||||
#include "../utility/completer_utils.h"
|
||||
#include "card_database_display_model.h"
|
||||
#include "card_database_model.h"
|
||||
#include "playmat_preview_widget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/card/database/card_database_manager.h>
|
||||
|
||||
PlaymatSettingsDialog::PlaymatSettingsDialog(const CardRef &initialCard,
|
||||
const PlaymatParams &initialParams,
|
||||
QWidget *parent)
|
||||
: QDialog(parent), currentCard(initialCard), currentParams(initialParams)
|
||||
{
|
||||
setMinimumWidth(500);
|
||||
setupUi();
|
||||
|
||||
// Seed UI from initial values
|
||||
if (!initialCard.name.isEmpty()) {
|
||||
searchBar->setText(initialCard.name);
|
||||
onCardNameChanged(initialCard.name);
|
||||
|
||||
// onCardNameChanged leaves the printing combo on the first printing in
|
||||
// the database, which would silently change the deck's stored playmat
|
||||
// card on accept. Restore the stored printing when it resolves locally.
|
||||
const int storedPrintingIndex = providerComboBox->findData(initialCard.providerId);
|
||||
if (storedPrintingIndex != -1) {
|
||||
providerComboBox->setCurrentIndex(storedPrintingIndex);
|
||||
} else {
|
||||
// Stored printing not in the local database: keep it rather than
|
||||
// silently substituting the first printing.
|
||||
currentCard.providerId = initialCard.providerId;
|
||||
reloadPreview();
|
||||
}
|
||||
}
|
||||
marginLSpin->setValue(initialParams.marginPctL);
|
||||
marginRSpin->setValue(initialParams.marginPctR);
|
||||
verticalOffsetSpin->setValue(initialParams.verticalOffset);
|
||||
zoomSpin->setValue(initialParams.zoom);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
CardRef PlaymatSettingsDialog::card() const
|
||||
{
|
||||
return currentCard;
|
||||
}
|
||||
|
||||
PlaymatParams PlaymatSettingsDialog::params() const
|
||||
{
|
||||
return currentParams;
|
||||
}
|
||||
|
||||
QDoubleSpinBox *PlaymatSettingsDialog::makeSpinBox(double min, double max, double value, double step)
|
||||
{
|
||||
auto *spin = new QDoubleSpinBox;
|
||||
spin->setRange(min, max);
|
||||
spin->setSingleStep(step);
|
||||
spin->setDecimals(3);
|
||||
spin->setValue(value);
|
||||
return spin;
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::initializeSearchBar()
|
||||
{
|
||||
searchBar = new QLineEdit;
|
||||
|
||||
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
|
||||
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
|
||||
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);
|
||||
|
||||
const CardCompleterSetup cardSetup = createCardCompleter(cardDatabaseDisplayModel, this, 15);
|
||||
searchModel = cardSetup.searchModel;
|
||||
proxyModel = cardSetup.proxyModel;
|
||||
completer = cardSetup.completer;
|
||||
searchBar->setCompleter(completer);
|
||||
|
||||
connectCardCompleterSearch(searchBar, cardSetup);
|
||||
|
||||
connect(completer, static_cast<void (QCompleter::*)(const QString &)>(&QCompleter::activated), this,
|
||||
[this](const QString &completion) {
|
||||
if (searchBar->text() != completion) {
|
||||
searchBar->setText(completion);
|
||||
searchBar->setCursorPosition(searchBar->text().length());
|
||||
}
|
||||
onCardNameChanged(completion);
|
||||
});
|
||||
|
||||
connect(searchBar, &QLineEdit::returnPressed, this, [this]() { onCardNameChanged(searchBar->text()); });
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::setupUi()
|
||||
{
|
||||
initializeSearchBar();
|
||||
|
||||
providerComboBox = new QComboBox;
|
||||
connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() {
|
||||
currentCard.providerId = providerComboBox->currentData().toString();
|
||||
reloadPreview();
|
||||
onParamChanged();
|
||||
});
|
||||
|
||||
marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01);
|
||||
marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01);
|
||||
verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01);
|
||||
zoomSpin = makeSpinBox(0.1, 4.0, currentParams.zoom, 0.05);
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
cardNameLabel = new QLabel;
|
||||
printingLabel = new QLabel;
|
||||
leftMarginLabel = new QLabel;
|
||||
rightMarginLabel = new QLabel;
|
||||
verticalOffsetLabel = new QLabel;
|
||||
zoomLabel = new QLabel;
|
||||
form->addRow(cardNameLabel, searchBar);
|
||||
form->addRow(printingLabel, providerComboBox);
|
||||
form->addRow(leftMarginLabel, marginLSpin);
|
||||
form->addRow(rightMarginLabel, marginRSpin);
|
||||
form->addRow(verticalOffsetLabel, verticalOffsetSpin);
|
||||
form->addRow(zoomLabel, zoomSpin);
|
||||
|
||||
controlsGroup = new QGroupBox;
|
||||
controlsGroup->setLayout(form);
|
||||
|
||||
preview = new PlaymatPreviewWidget;
|
||||
|
||||
auto *previewLayout = new QVBoxLayout;
|
||||
previewLayout->addWidget(preview);
|
||||
previewGroup = new QGroupBox;
|
||||
previewGroup->setLayout(previewLayout);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
removeButton = new QPushButton;
|
||||
buttons->addButton(removeButton, QDialogButtonBox::ResetRole);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(removeButton, &QPushButton::clicked, this, [this]() {
|
||||
currentCard = CardRef{}; // empty signals removal
|
||||
accept();
|
||||
});
|
||||
|
||||
auto *root = new QVBoxLayout;
|
||||
root->addWidget(controlsGroup);
|
||||
root->addWidget(previewGroup);
|
||||
root->addWidget(buttons);
|
||||
setLayout(root);
|
||||
|
||||
connect(marginLSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(marginRSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(verticalOffsetSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &PlaymatSettingsDialog::onParamChanged);
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::populateProviderCombo(const QString &cardName)
|
||||
{
|
||||
providerComboBox->clear();
|
||||
|
||||
auto card = CardDatabaseManager::query()->getCard({cardName});
|
||||
|
||||
const auto &sets = card.getInfo().getSets();
|
||||
|
||||
for (const auto &printings : sets) {
|
||||
for (const auto &p : printings) {
|
||||
QString setName = p.getSet()->getLongName();
|
||||
QString collector = p.getProperty("num");
|
||||
QString uuid = p.getUuid();
|
||||
|
||||
QString label = setName;
|
||||
if (!collector.isEmpty()) {
|
||||
label += " #" + collector;
|
||||
}
|
||||
|
||||
providerComboBox->addItem(label, uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::onCardNameChanged(const QString &name)
|
||||
{
|
||||
if (name.isEmpty()) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
return;
|
||||
}
|
||||
|
||||
const ExactCard card = CardDatabaseManager::query()->getCard({name});
|
||||
if (!card) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
providerComboBox->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
currentCard.name = name;
|
||||
|
||||
populateProviderCombo(name);
|
||||
|
||||
if (providerComboBox->count() == 0) {
|
||||
currentPixmap = QPixmap();
|
||||
preview->setPixmap(currentPixmap);
|
||||
currentCard.providerId.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
currentCard.providerId = providerComboBox->currentData().toString();
|
||||
reloadPreview();
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::reloadPreview()
|
||||
{
|
||||
if (currentCard.name.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExactCard card = CardDatabaseManager::query()->getCard({currentCard.name, currentCard.providerId});
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnect(pixmapUpdatedConnection);
|
||||
|
||||
QPixmap fullRes;
|
||||
CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040));
|
||||
|
||||
if (fullRes.isNull()) {
|
||||
CardInfo *cardInfo = card.getCardPtr().data();
|
||||
if (cardInfo) {
|
||||
pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
currentPixmap = fullRes;
|
||||
preview->setPixmap(currentPixmap);
|
||||
preview->setParams(currentParams);
|
||||
preview->setAttribution(buildArtAttribution(card));
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::onParamChanged()
|
||||
{
|
||||
currentParams.marginPctL = marginLSpin->value();
|
||||
currentParams.marginPctR = marginRSpin->value();
|
||||
currentParams.verticalOffset = verticalOffsetSpin->value();
|
||||
currentParams.zoom = zoomSpin->value();
|
||||
preview->setParams(currentParams);
|
||||
}
|
||||
|
||||
void PlaymatSettingsDialog::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Playmat Settings"));
|
||||
searchBar->setPlaceholderText(tr("Type a card name..."));
|
||||
cardNameLabel->setText(tr("Card name:"));
|
||||
printingLabel->setText(tr("Printing:"));
|
||||
leftMarginLabel->setText(tr("Left margin (%):"));
|
||||
rightMarginLabel->setText(tr("Right margin (%):"));
|
||||
verticalOffsetLabel->setText(tr("Vertical offset:"));
|
||||
zoomLabel->setText(tr("Zoom:"));
|
||||
controlsGroup->setTitle(tr("Parameters"));
|
||||
previewGroup->setTitle(tr("Preview"));
|
||||
removeButton->setText(tr("Remove Playmat"));
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
#define COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPixmap>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
class QComboBox;
|
||||
class QCompleter;
|
||||
class QDoubleSpinBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class CardDatabaseModel;
|
||||
class CardDatabaseDisplayModel;
|
||||
class CardSearchModel;
|
||||
class CardCompleterProxyModel;
|
||||
class PlaymatPreviewWidget;
|
||||
|
||||
/**
|
||||
* @brief Dialog for configuring the playmat card art for a deck.
|
||||
*
|
||||
* Allows the user to select a card from the database and adjust
|
||||
* positioning parameters (margins, zoom, vertical offset) for how
|
||||
* the card art appears as a playmat background across the
|
||||
* combined table + stack play area.
|
||||
*/
|
||||
class PlaymatSettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlaymatSettingsDialog(const CardRef &initialCard = {},
|
||||
const PlaymatParams &initialParams = {},
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
CardRef card() const;
|
||||
PlaymatParams params() const;
|
||||
|
||||
private slots:
|
||||
void onCardNameChanged(const QString &name);
|
||||
void reloadPreview();
|
||||
void onParamChanged();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void populateProviderCombo(const QString &cardName);
|
||||
void initializeSearchBar();
|
||||
void retranslateUi();
|
||||
QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step);
|
||||
|
||||
QLineEdit *searchBar;
|
||||
QCompleter *completer;
|
||||
CardDatabaseModel *cardDatabaseModel;
|
||||
CardDatabaseDisplayModel *cardDatabaseDisplayModel;
|
||||
CardSearchModel *searchModel;
|
||||
CardCompleterProxyModel *proxyModel;
|
||||
|
||||
QComboBox *providerComboBox;
|
||||
|
||||
QMetaObject::Connection pixmapUpdatedConnection;
|
||||
|
||||
QLabel *cardNameLabel;
|
||||
QLabel *printingLabel;
|
||||
QLabel *leftMarginLabel;
|
||||
QLabel *rightMarginLabel;
|
||||
QLabel *verticalOffsetLabel;
|
||||
QLabel *zoomLabel;
|
||||
QGroupBox *controlsGroup;
|
||||
QGroupBox *previewGroup;
|
||||
QPushButton *removeButton;
|
||||
|
||||
QDoubleSpinBox *marginLSpin;
|
||||
QDoubleSpinBox *marginRSpin;
|
||||
QDoubleSpinBox *verticalOffsetSpin;
|
||||
QDoubleSpinBox *zoomSpin;
|
||||
PlaymatPreviewWidget *preview;
|
||||
|
||||
QPixmap currentPixmap;
|
||||
CardRef currentCard;
|
||||
PlaymatParams currentParams;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_SETTINGS_DIALOG_H
|
||||
69
cockatrice/src/interface/widgets/playmat/playmat_utils.h
Normal file
69
cockatrice/src/interface/widgets/playmat/playmat_utils.h
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_UTILS_H
|
||||
#define COCKATRICE_PLAYMAT_UTILS_H
|
||||
|
||||
#include <QRectF>
|
||||
#include <QSize>
|
||||
#include <QSizeF>
|
||||
#include <libcockatrice/deck_list/deck_list.h>
|
||||
|
||||
/**
|
||||
* @brief Computes the source region of the full-resolution card image to use as a playmat.
|
||||
*
|
||||
* Parameters are relative to the full card image: horizontal margins trim the card
|
||||
* borders, the vertical offset positions a square viewing window, and zoom scales
|
||||
* into that window. The result is clamped to the card image bounds.
|
||||
*
|
||||
* @param fullCardSize Size of the full card image.
|
||||
* @param params Positioning parameters.
|
||||
* @return Source rectangle in full-card image pixel coordinates.
|
||||
*/
|
||||
inline QRectF computeArtSourceRect(const QSize &fullCardSize, const PlaymatParams ¶ms)
|
||||
{
|
||||
const qreal srcW = fullCardSize.width();
|
||||
const qreal srcH = fullCardSize.height();
|
||||
|
||||
const qreal marginL = params.marginPctL * srcW;
|
||||
const qreal marginR = params.marginPctR * srcW;
|
||||
// Guard against margins summing to >= 1 (both are individually in range),
|
||||
// which would otherwise make the viewing window negative or zero.
|
||||
const qreal visibleW = qMax(0.0, srcW - marginL - marginR);
|
||||
const qreal visibleH = visibleW; // square viewing window, keeps art unskewed
|
||||
|
||||
const qreal vCenter = params.verticalOffset * srcH;
|
||||
qreal srcY = vCenter - visibleH / 2.0;
|
||||
srcY = qBound(0.0, srcY, srcH - visibleH);
|
||||
|
||||
// Guard the zoom divisor; everything that produces params clamps zoom to
|
||||
// [0.1, 4.0] already, this keeps the render path self-contained.
|
||||
const qreal zoom = qBound(0.1, params.zoom, 4.0);
|
||||
const qreal zoomedW = visibleW / zoom;
|
||||
const qreal zoomedH = visibleH / zoom;
|
||||
const qreal zoomedX = marginL + (visibleW - zoomedW) / 2.0;
|
||||
const qreal zoomedY = srcY + (visibleH - zoomedH) / 2.0;
|
||||
|
||||
return QRectF(zoomedX, zoomedY, zoomedW, zoomedH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the destination rectangle that fits a source of the given aspect
|
||||
* ratio into dstArea using "cover" semantics (no distortion, overflows cropped).
|
||||
*
|
||||
* @param dstArea Area to fill.
|
||||
* @param srcSize Size of the source; only its aspect ratio matters.
|
||||
* @return Destination rectangle centered in dstArea.
|
||||
*/
|
||||
inline QRectF coverFitRect(const QRectF &dstArea, const QSizeF &srcSize)
|
||||
{
|
||||
const qreal srcAspect = srcSize.width() / srcSize.height();
|
||||
const qreal dstAspect = dstArea.width() / dstArea.height();
|
||||
|
||||
if (srcAspect > dstAspect) {
|
||||
const qreal dstW = dstArea.height() * srcAspect;
|
||||
return QRectF(dstArea.left() + (dstArea.width() - dstW) / 2.0, dstArea.top(), dstW, dstArea.height());
|
||||
}
|
||||
|
||||
const qreal dstH = dstArea.width() / srcAspect;
|
||||
return QRectF(dstArea.left(), dstArea.top() + (dstArea.height() - dstH) / 2.0, dstArea.width(), dstH);
|
||||
}
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_UTILS_H
|
||||
|
|
@ -273,6 +273,14 @@ void ChatView::appendMessage(QString message,
|
|||
// messageType should be Event_RoomSay::UserMessage though we don't actually check
|
||||
bool isUserMessage = !(userName.toLower() == "servatrice" || userName.isEmpty());
|
||||
bool sameSender = isUserMessage && userName == lastSender;
|
||||
|
||||
if (isUserMessage) {
|
||||
chatHistory.append({userName, message, QDateTime::currentDateTime()});
|
||||
while (chatHistory.size() > MAX_CHAT_HISTORY) {
|
||||
chatHistory.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
QTextCursor cursor = prepareBlock(sameSender);
|
||||
lastSender = userName;
|
||||
|
||||
|
|
@ -650,6 +658,19 @@ void ChatView::clearChat()
|
|||
document()->clear();
|
||||
lastSender = "";
|
||||
evenNumber = true;
|
||||
chatHistory.clear();
|
||||
}
|
||||
|
||||
QString ChatView::getRecentChatLog(int maxMessages) const
|
||||
{
|
||||
QStringList lines;
|
||||
int start = qMax(0, chatHistory.size() - maxMessages);
|
||||
for (int i = start; i < chatHistory.size(); ++i) {
|
||||
const ChatLogEntry &entry = chatHistory.at(i);
|
||||
lines.append(
|
||||
QString("[%1] %2: %3").arg(entry.timestamp.toString("hh:mm:ss")).arg(entry.userName).arg(entry.message));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
void ChatView::redactMessages(const QString &userName, int amount)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ public:
|
|||
QTextBlock block;
|
||||
};
|
||||
|
||||
struct ChatLogEntry
|
||||
{
|
||||
QString userName;
|
||||
QString message;
|
||||
QDateTime timestamp;
|
||||
};
|
||||
|
||||
class ChatView : public QTextBrowser
|
||||
{
|
||||
Q_OBJECT
|
||||
|
|
@ -65,6 +72,8 @@ private:
|
|||
QString hoveredContent;
|
||||
QAction *messageClicked;
|
||||
QMap<QString, QVector<UserMessagePosition>> userMessagePositions;
|
||||
QList<ChatLogEntry> chatHistory;
|
||||
static constexpr int MAX_CHAT_HISTORY = 200;
|
||||
|
||||
[[nodiscard]] QTextFragment getFragmentUnderMouse(const QPoint &pos) const;
|
||||
QTextCursor prepareBlock(bool same = false);
|
||||
|
|
@ -107,6 +116,7 @@ public:
|
|||
bool playerBold = false);
|
||||
void clearChat();
|
||||
void redactMessages(const QString &userName, int amount);
|
||||
QString getRecentChatLog(int maxMessages = 50) const;
|
||||
|
||||
protected:
|
||||
void enterEvent(QEnterEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -359,7 +359,11 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
|
|||
return;
|
||||
}
|
||||
|
||||
const ServerInfo_Game &game = gameListModel->getGame(ind.data(Qt::UserRole).toInt());
|
||||
joinGame(gameListModel->getGame(ind.data(Qt::UserRole).toInt()), asSpectator, asJudge);
|
||||
}
|
||||
|
||||
void GameSelector::joinGame(const ServerInfo_Game &game, const bool asSpectator, const bool asJudge)
|
||||
{
|
||||
if (tabSupervisor->switchToGameTabIfAlreadyExists(game.game_id())) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -414,18 +418,16 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge)
|
|||
disableButtons();
|
||||
}
|
||||
|
||||
bool GameSelector::joinGameById(int gameId)
|
||||
bool GameSelector::joinGameById(const int gameId, const bool asSpectator)
|
||||
{
|
||||
auto *model = gameListView->model();
|
||||
|
||||
for (int row = 0; row < model->rowCount(); ++row) {
|
||||
QModelIndex idx = model->index(row, 0);
|
||||
const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt());
|
||||
if (game.game_id() == gameId) {
|
||||
gameListView->setCurrentIndex(idx);
|
||||
joinGame();
|
||||
return true;
|
||||
for (int row = 0; row < gameListModel->rowCount(); ++row) {
|
||||
const ServerInfo_Game &game = gameListModel->getGame(row);
|
||||
if (game.game_id() != gameId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
joinGame(game, asSpectator);
|
||||
return true;
|
||||
}
|
||||
|
||||
qWarning() << "Game" << gameId << "not found";
|
||||
|
|
|
|||
|
|
@ -171,6 +171,17 @@ private:
|
|||
*/
|
||||
void joinGame(bool asSpectator = false, bool asJudge = false);
|
||||
|
||||
/**
|
||||
* @brief Performs the join or spectate action for a specific game.
|
||||
* @param game The game to join.
|
||||
* @param asSpectator True to join as a spectator, false to join as a player.
|
||||
* @param asJudge True to join as a judge, false to join as a player.
|
||||
*
|
||||
* Unlike the selection-based overload, this does not depend on the game being
|
||||
* visible in the filtered game list.
|
||||
*/
|
||||
void joinGame(const ServerInfo_Game &game, bool asSpectator = false, bool asJudge = false);
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a GameSelector widget.
|
||||
|
|
@ -202,7 +213,16 @@ public:
|
|||
* @param info The ServerInfo_Game object containing information about the game to update.
|
||||
*/
|
||||
void processGameInfo(const ServerInfo_Game &info);
|
||||
bool joinGameById(int gameId);
|
||||
/**
|
||||
* @brief Finds a game by ID and joins or spectates it.
|
||||
* @param gameId The ID of the game to join.
|
||||
* @param asSpectator True to join as a spectator, false to join as a player.
|
||||
* @return True if the game was found and joined, false otherwise.
|
||||
*
|
||||
* Unlike the selection-based overload, this does not depend on the game
|
||||
* being visible in the filtered game list.
|
||||
*/
|
||||
bool joinGameById(int gameId, bool asSpectator = false);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "user_context_menu.h"
|
||||
|
||||
#include "../../dialogs/dlg_report_user.h"
|
||||
#include "../../interface/widgets/tabs/tab_account.h"
|
||||
#include "../../interface/widgets/tabs/tab_game.h"
|
||||
#include "../../interface/widgets/tabs/tab_supervisor.h"
|
||||
|
|
@ -41,6 +42,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aAddToIgnoreList = new QAction(QString(), this);
|
||||
aRemoveFromIgnoreList = new QAction(QString(), this);
|
||||
aKick = new QAction(QString(), this);
|
||||
aReport = new QAction(QString(), this);
|
||||
aWarnUser = new QAction(QString(), this);
|
||||
aWarnHistory = new QAction(QString(), this);
|
||||
aBan = new QAction(QString(), this);
|
||||
|
|
@ -50,6 +52,7 @@ UserContextMenu::UserContextMenu(TabSupervisor *_tabSupervisor, QWidget *parent,
|
|||
aPromoteToJudge = new QAction(QString(), this);
|
||||
aDemoteFromJudge = new QAction(QString(), this);
|
||||
aGetAdminNotes = new QAction(QString(), this);
|
||||
aInvestigateUser = new QAction(QString(), this);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
|
@ -64,6 +67,7 @@ void UserContextMenu::retranslateUi()
|
|||
aAddToIgnoreList->setText(tr("Add to &ignore list"));
|
||||
aRemoveFromIgnoreList->setText(tr("Remove from &ignore list"));
|
||||
aKick->setText(tr("Kick from &game"));
|
||||
aReport->setText(tr("Report user"));
|
||||
aWarnUser->setText(tr("Warn user"));
|
||||
aWarnHistory->setText(tr("View user's war&n history"));
|
||||
aBan->setText(tr("Ban from &server"));
|
||||
|
|
@ -73,6 +77,7 @@ void UserContextMenu::retranslateUi()
|
|||
aPromoteToJudge->setText(tr("Promote user to &judge"));
|
||||
aDemoteFromJudge->setText(tr("Demote user from judge"));
|
||||
aGetAdminNotes->setText(tr("View admin notes"));
|
||||
aInvestigateUser->setText(tr("Investigate user"));
|
||||
}
|
||||
|
||||
void UserContextMenu::gamesOfUserReceived(const Response &resp, const CommandContainer &commandContainer)
|
||||
|
|
@ -144,7 +149,8 @@ void UserContextMenu::warnUser_processGetWarningsListResponse(const Response &r)
|
|||
|
||||
if (response.warning_size() > 0) {
|
||||
for (int i = 0; i < response.warning_size(); ++i) {
|
||||
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified());
|
||||
int startingIl = i < response.warning_il_size() ? response.warning_il(i) : 1;
|
||||
dlg->addWarningOption(QString::fromStdString(response.warning(i)).simplified(), startingIl);
|
||||
}
|
||||
}
|
||||
dlg->show();
|
||||
|
|
@ -395,6 +401,9 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aRemoveMessages = new QAction(tr("Remove this user's messages"), this);
|
||||
menu->addAction(aRemoveMessages);
|
||||
}
|
||||
if (userListProxy->isOwnUserRegistered()) {
|
||||
menu->addAction(aReport);
|
||||
}
|
||||
if (game && (game->isHost() || !tabSupervisor->getAdminLocked())) {
|
||||
menu->addSeparator();
|
||||
menu->addAction(aKick);
|
||||
|
|
@ -408,6 +417,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
menu->addAction(aBanHistory);
|
||||
menu->addSeparator();
|
||||
menu->addAction(aGetAdminNotes);
|
||||
menu->addAction(aInvestigateUser);
|
||||
|
||||
menu->addSeparator();
|
||||
if (userLevel.testFlag(ServerInfo_User::IsModerator) &&
|
||||
|
|
@ -431,6 +441,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aDetails->setEnabled(true);
|
||||
aChat->setEnabled(anotherUser && online);
|
||||
aShowGames->setEnabled(online);
|
||||
aReport->setEnabled(anotherUser);
|
||||
aAddToBuddyList->setEnabled(anotherUser);
|
||||
aRemoveFromBuddyList->setEnabled(anotherUser);
|
||||
aAddToIgnoreList->setEnabled(anotherUser);
|
||||
|
|
@ -441,6 +452,7 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
aBan->setEnabled(anotherUser);
|
||||
aBanHistory->setEnabled(anotherUser);
|
||||
aGetAdminNotes->setEnabled(anotherUser);
|
||||
aInvestigateUser->setEnabled(anotherUser);
|
||||
aPromoteToMod->setEnabled(anotherUser);
|
||||
aDemoteFromMod->setEnabled(anotherUser);
|
||||
|
||||
|
|
@ -462,6 +474,15 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execRemoveFromIgnore(userName);
|
||||
} else if (actionClicked == aKick) {
|
||||
execKick(playerId);
|
||||
} else if (actionClicked == aReport) {
|
||||
int gameId = game ? game->getGameMetaInfo()->gameId() : -1;
|
||||
QString autoChatLog;
|
||||
if (chatView) {
|
||||
autoChatLog = chatView->getRecentChatLog(50);
|
||||
}
|
||||
auto dlgReport = new DlgReportUser(client, userName, gameId, autoChatLog, static_cast<QWidget *>(parent()));
|
||||
dlgReport->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlgReport->exec();
|
||||
} else if (actionClicked == aBan) {
|
||||
execBan(userName);
|
||||
} else if (actionClicked == aPromoteToMod || actionClicked == aDemoteFromMod) {
|
||||
|
|
@ -476,6 +497,8 @@ void UserContextMenu::showContextMenu(const QPoint &pos,
|
|||
execWarnHistory(userName);
|
||||
} else if (actionClicked == aGetAdminNotes) {
|
||||
execAdminNotes(userName);
|
||||
} else if (actionClicked == aInvestigateUser) {
|
||||
execInvestigateUser(userName);
|
||||
} else if (actionClicked == aCopyToClipBoard) {
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
clipboard->setText(deckHash);
|
||||
|
|
@ -652,6 +675,11 @@ void UserContextMenu::execAdminNotes(const QString &userName)
|
|||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void UserContextMenu::execInvestigateUser(const QString &userName)
|
||||
{
|
||||
tabSupervisor->openTabModeration(userName);
|
||||
}
|
||||
|
||||
void UserContextMenu::execAdjustMod(const QString &userName, bool shouldBeMod)
|
||||
{
|
||||
Command_AdjustMod cmd;
|
||||
|
|
|
|||
|
|
@ -41,12 +41,14 @@ private:
|
|||
QAction *aAddToBuddyList, *aRemoveFromBuddyList;
|
||||
QAction *aAddToIgnoreList, *aRemoveFromIgnoreList;
|
||||
QAction *aKick;
|
||||
QAction *aReport;
|
||||
QAction *aBan, *aBanHistory;
|
||||
QAction *aPromoteToMod, *aDemoteFromMod;
|
||||
QAction *aPromoteToJudge, *aDemoteFromJudge;
|
||||
QAction *aWarnUser, *aWarnHistory;
|
||||
QAction *aGetAdminNotes;
|
||||
std::function<QList<GameInviteOption>()> gameInviteLinkProvider;
|
||||
QAction *aInvestigateUser;
|
||||
signals:
|
||||
void openMessageDialog(const QString &userName, bool focus);
|
||||
private slots:
|
||||
|
|
@ -118,6 +120,7 @@ public:
|
|||
void execBanHistory(const QString &userName);
|
||||
void execWarnHistory(const QString &userName);
|
||||
void execAdminNotes(const QString &userName);
|
||||
void execInvestigateUser(const QString &userName);
|
||||
void execAdjustMod(const QString &userName, bool shouldBeMod);
|
||||
void execAdjustJudge(const QString &userName, bool shouldBeJudge);
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ WarningDialog::WarningDialog(const QString userName, const QString clientID, QWi
|
|||
warnClientID = new QLineEdit(clientID);
|
||||
warnClientID->setMaxLength(MAX_NAME_LENGTH);
|
||||
warningOption = new QComboBox();
|
||||
warningOption->addItem("");
|
||||
warningOption->addItem("", "");
|
||||
|
||||
deleteMessages = new QCheckBox(tr("Redact all messages from this user in all rooms"));
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ void WarningDialog::okClicked()
|
|||
return;
|
||||
}
|
||||
|
||||
if (warningOption->currentText().simplified().isEmpty()) {
|
||||
if (warningOption->currentData().toString().simplified().isEmpty()) {
|
||||
QMessageBox::critical(this, tr("Error"),
|
||||
tr("Warning to use can not be blank, please select a valid warning to send."));
|
||||
return;
|
||||
|
|
@ -205,7 +205,7 @@ QString WarningDialog::getWarnID() const
|
|||
|
||||
QString WarningDialog::getReason() const
|
||||
{
|
||||
return warningOption->currentText().simplified();
|
||||
return warningOption->currentData().toString().simplified();
|
||||
}
|
||||
|
||||
int WarningDialog::getDeleteMessages() const
|
||||
|
|
@ -213,9 +213,13 @@ int WarningDialog::getDeleteMessages() const
|
|||
return deleteMessages->isChecked() ? -1 : 0;
|
||||
}
|
||||
|
||||
void WarningDialog::addWarningOption(const QString warning)
|
||||
void WarningDialog::addWarningOption(const QString warning, int startingIl)
|
||||
{
|
||||
warningOption->addItem(warning);
|
||||
if (startingIl > 1) {
|
||||
warningOption->addItem(tr("%1 (IL %2)").arg(warning).arg(startingIl), warning);
|
||||
} else {
|
||||
warningOption->addItem(warning, warning);
|
||||
}
|
||||
}
|
||||
|
||||
void BanDialog::okClicked()
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public:
|
|||
[[nodiscard]] QString getWarnID() const;
|
||||
[[nodiscard]] QString getReason() const;
|
||||
[[nodiscard]] int getDeleteMessages() const;
|
||||
void addWarningOption(const QString warning);
|
||||
void addWarningOption(const QString warning, int startingIl = 1);
|
||||
};
|
||||
|
||||
class AdminNotesDialog : public QDialog
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@
|
|||
#include "../dialogs/override_printing_warning.h"
|
||||
#include "../interface/theme_manager.h"
|
||||
#include "../interface/widgets/general/background_sources.h"
|
||||
#include "../playmat/playmat_collection_dialog.h"
|
||||
#include "../playmat/playmat_settings_dialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColorDialog>
|
||||
#include <QDesktopServices>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QStyleFactory>
|
||||
#include <QTimer>
|
||||
|
|
@ -325,10 +328,52 @@ AppearanceSettingsPage::AppearanceSettingsPage()
|
|||
tableGroupBox = new QGroupBox;
|
||||
tableGroupBox->setLayout(tableGrid);
|
||||
|
||||
// Playmat settings
|
||||
playmatVisibilityCombo.addItem(tr("Show all playmats"), PlaymatVisibilityAll);
|
||||
playmatVisibilityCombo.addItem(tr("Show own playmat only"), PlaymatVisibilityOwnOnly);
|
||||
playmatVisibilityCombo.addItem(tr("Don't use playmats"), PlaymatVisibilityNone);
|
||||
int visIdx = playmatVisibilityCombo.findData(settings.userInterface().getPlaymatVisibility());
|
||||
if (visIdx >= 0) {
|
||||
playmatVisibilityCombo.setCurrentIndex(visIdx);
|
||||
}
|
||||
connect(&playmatVisibilityCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatVisibility(playmatVisibilityCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatVisibilityLabel.setBuddy(&playmatVisibilityCombo);
|
||||
|
||||
// Playmat mode: Override / Fallback / Deck-only
|
||||
playmatModeCombo.addItem(tr("Override deck playmat"), PlaymatModeOverrideDeck);
|
||||
playmatModeCombo.addItem(tr("Fallback if deck has none"), PlaymatModeFallback);
|
||||
playmatModeCombo.addItem(tr("Deck only, ignore collection"), PlaymatModeDeckOnly);
|
||||
int modeIdx = playmatModeCombo.findData(settings.userInterface().getPlaymatMode());
|
||||
if (modeIdx >= 0) {
|
||||
playmatModeCombo.setCurrentIndex(modeIdx);
|
||||
}
|
||||
connect(&playmatModeCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
|
||||
SettingsCache::instance().userInterface().setPlaymatMode(playmatModeCombo.itemData(index).toInt());
|
||||
});
|
||||
playmatModeLabel.setBuddy(&playmatModeCombo);
|
||||
|
||||
// User-level playmat settings: fallback collection.
|
||||
connect(&playmatDefaultEditButton, &QPushButton::clicked, this,
|
||||
&AppearanceSettingsPage::openPlaymatCollectionDialog);
|
||||
|
||||
auto *playmatGrid = new QGridLayout;
|
||||
playmatGrid->addWidget(&playmatVisibilityLabel, 0, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatVisibilityCombo, 0, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeLabel, 1, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatModeCombo, 1, 1, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultLabel, 2, 0, 1, 1);
|
||||
playmatGrid->addWidget(&playmatDefaultEditButton, 2, 1, 1, 1);
|
||||
|
||||
playmatGroupBox = new QGroupBox;
|
||||
playmatGroupBox->setLayout(playmatGrid);
|
||||
|
||||
// putting it all together
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(themeGroupBox);
|
||||
mainLayout->addWidget(homeTabGroupBox);
|
||||
mainLayout->addWidget(playmatGroupBox);
|
||||
mainLayout->addWidget(stylingGroupBox);
|
||||
mainLayout->addWidget(menuGroupBox);
|
||||
mainLayout->addWidget(printingsGroupBox);
|
||||
|
|
@ -431,6 +476,12 @@ void AppearanceSettingsPage::cardViewExpandedRowsMaxChanged(int value)
|
|||
}
|
||||
}
|
||||
|
||||
void AppearanceSettingsPage::openPlaymatCollectionDialog()
|
||||
{
|
||||
PlaymatCollectionDialog dialog(this);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void AppearanceSettingsPage::retranslateUi()
|
||||
{
|
||||
themeGroupBox->setTitle(tr("Theme settings"));
|
||||
|
|
@ -489,4 +540,9 @@ void AppearanceSettingsPage::retranslateUi()
|
|||
tableGroupBox->setTitle(tr("Table grid layout"));
|
||||
invertVerticalCoordinateCheckBox.setText(tr("Invert vertical coordinate"));
|
||||
minPlayersForMultiColumnLayoutLabel.setText(tr("Minimum player count for multi-column layout:"));
|
||||
}
|
||||
playmatGroupBox->setTitle(tr("Playmat settings"));
|
||||
playmatVisibilityLabel.setText(tr("Playmat visibility:"));
|
||||
playmatModeLabel.setText(tr("Default collection behavior:"));
|
||||
playmatDefaultLabel.setText(tr("Default playmat collection:"));
|
||||
playmatDefaultEditButton.setText(tr("Edit..."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ private slots:
|
|||
|
||||
void cardViewInitialRowsMaxChanged(int value);
|
||||
void cardViewExpandedRowsMaxChanged(int value);
|
||||
void openPlaymatCollectionDialog();
|
||||
|
||||
private:
|
||||
QLabel themeLabel;
|
||||
|
|
@ -59,6 +60,12 @@ private:
|
|||
QCheckBox horizontalHandCheckBox;
|
||||
QCheckBox leftJustifiedHandCheckBox;
|
||||
QCheckBox invertVerticalCoordinateCheckBox;
|
||||
QLabel playmatVisibilityLabel;
|
||||
QComboBox playmatVisibilityCombo;
|
||||
QLabel playmatModeLabel;
|
||||
QComboBox playmatModeCombo;
|
||||
QLabel playmatDefaultLabel;
|
||||
QPushButton playmatDefaultEditButton;
|
||||
QGroupBox *themeGroupBox;
|
||||
QGroupBox *homeTabGroupBox;
|
||||
QGroupBox *stylingGroupBox;
|
||||
|
|
@ -67,6 +74,7 @@ private:
|
|||
QGroupBox *cardsGroupBox;
|
||||
QGroupBox *cardLayoutGroupBox;
|
||||
QGroupBox *handGroupBox;
|
||||
QGroupBox *playmatGroupBox;
|
||||
QGroupBox *tableGroupBox;
|
||||
QGroupBox *cardCountersGroupBox;
|
||||
QList<QLabel *> cardCounterNames;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "tab_account.h"
|
||||
|
||||
#include "../client/sound_engine.h"
|
||||
#include "../interface/widgets/dialogs/dlg_my_reports.h"
|
||||
#include "../interface/widgets/server/user/user_info_box.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
#include "../interface/widgets/server/user/user_list_widget.h"
|
||||
|
|
@ -49,6 +50,11 @@ TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, c
|
|||
|
||||
auto *vbox = new QVBoxLayout;
|
||||
vbox->addWidget(userInfoBox);
|
||||
|
||||
myReportsButton = new QPushButton(tr("My Reports"));
|
||||
connect(myReportsButton, &QPushButton::clicked, this, &TabAccount::openMyReports);
|
||||
vbox->addWidget(myReportsButton);
|
||||
|
||||
vbox->addWidget(allUsersList);
|
||||
|
||||
auto *addToBuddyList = new QHBoxLayout;
|
||||
|
|
@ -126,6 +132,7 @@ void TabAccount::addToList(const std::string &listName, const QString &userName)
|
|||
|
||||
void TabAccount::retranslateUi()
|
||||
{
|
||||
myReportsButton->setText(tr("My Reports"));
|
||||
allUsersList->retranslateUi();
|
||||
buddyList->retranslateUi();
|
||||
ignoreList->retranslateUi();
|
||||
|
|
@ -240,3 +247,10 @@ void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
|
|||
|
||||
userList->deleteUser(user);
|
||||
}
|
||||
|
||||
void TabAccount::openMyReports()
|
||||
{
|
||||
auto *dlg = new DlgMyReports(client, this);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dlg->exec();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class Event_RemoveFromList;
|
|||
class Event_UserJoined;
|
||||
class Event_UserLeft;
|
||||
class LineEditUnfocusable;
|
||||
class QPushButton;
|
||||
class Response;
|
||||
class ServerInfo_User;
|
||||
class UserInfoBox;
|
||||
|
|
@ -41,6 +42,7 @@ private slots:
|
|||
void processRemoveFromListEvent(const Event_RemoveFromList &event);
|
||||
void addToIgnoreList();
|
||||
void addToBuddyList();
|
||||
void openMyReports();
|
||||
|
||||
private:
|
||||
AbstractClient *client;
|
||||
|
|
@ -50,6 +52,7 @@ private:
|
|||
UserInfoBox *userInfoBox;
|
||||
LineEditUnfocusable *addBuddyEdit;
|
||||
LineEditUnfocusable *addIgnoreEdit;
|
||||
QPushButton *myReportsButton;
|
||||
void addToList(const std::string &listName, const QString &userName);
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -911,6 +911,7 @@ void TabGame::stopGame()
|
|||
QMapIterator<int, TabbedDeckViewContainer *> i(deckViewContainers);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
i.value()->playerDeckView->advancePlaymatRotation();
|
||||
i.value()->show();
|
||||
}
|
||||
|
||||
|
|
|
|||
462
cockatrice/src/interface/widgets/tabs/tab_moderation.cpp
Normal file
462
cockatrice/src/interface/widgets/tabs/tab_moderation.cpp
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
#include "tab_moderation.h"
|
||||
|
||||
#include "abstract_client.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_moderator_last_logins.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_remove_user_avatar.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_reset_user_password.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_user_alts.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_user_sessions.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int COL_ALTS_USER = 0;
|
||||
constexpr int COL_ALTS_EMAIL = 1;
|
||||
constexpr int COL_ALTS_CLIENTID = 2;
|
||||
constexpr int COL_ALTS_REGISTERED = 3;
|
||||
constexpr int COL_ALTS_LAST_LOGIN = 4;
|
||||
constexpr int COL_ALTS_WARNS = 5;
|
||||
constexpr int COL_ALTS_BANS = 6;
|
||||
constexpr int COL_ALTS_ACTIVE = 7;
|
||||
constexpr int COL_ALTS_COUNT = 8;
|
||||
|
||||
constexpr int COL_SESSIONS_IP = 0;
|
||||
constexpr int COL_SESSIONS_CLIENTID = 1;
|
||||
constexpr int COL_SESSIONS_START = 2;
|
||||
constexpr int COL_SESSIONS_END = 3;
|
||||
constexpr int COL_SESSIONS_TYPE = 4;
|
||||
constexpr int COL_SESSIONS_COUNT = 5;
|
||||
|
||||
constexpr int COL_STAFF_USER = 0;
|
||||
constexpr int COL_STAFF_LEVEL = 1;
|
||||
constexpr int COL_STAFF_LAST_LOGIN = 2;
|
||||
constexpr int COL_STAFF_COUNT = 3;
|
||||
} // namespace
|
||||
|
||||
TabModeration::TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
auto *centralWidget = new QWidget(this);
|
||||
setCentralWidget(centralWidget);
|
||||
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
connect(searchEdit, &QLineEdit::returnPressed, this, &TabModeration::investigateUser);
|
||||
|
||||
investigateButton = new QPushButton;
|
||||
connect(investigateButton, &QPushButton::clicked, this, &TabModeration::investigateUser);
|
||||
|
||||
resetPasswordButton = new QPushButton;
|
||||
connect(resetPasswordButton, &QPushButton::clicked, this, &TabModeration::resetPassword);
|
||||
|
||||
removeAvatarButton = new QPushButton;
|
||||
connect(removeAvatarButton, &QPushButton::clicked, this, &TabModeration::removeAvatar);
|
||||
|
||||
auto *topBar = new QHBoxLayout;
|
||||
topBar->addWidget(searchEdit);
|
||||
topBar->addWidget(investigateButton);
|
||||
topBar->addStretch();
|
||||
topBar->addWidget(resetPasswordButton);
|
||||
topBar->addWidget(removeAvatarButton);
|
||||
|
||||
userInfoGroup = new QGroupBox;
|
||||
userInfoNameLabel = new QLabel;
|
||||
userInfoNameValue = new QLabel;
|
||||
userInfoRegisteredLabel = new QLabel;
|
||||
userInfoRegisteredValue = new QLabel;
|
||||
userInfoLastLoginLabel = new QLabel;
|
||||
userInfoLastLoginValue = new QLabel;
|
||||
userInfoStatusLabel = new QLabel;
|
||||
userInfoStatusValue = new QLabel;
|
||||
userInfoCountsLabel = new QLabel;
|
||||
userInfoCountsValue = new QLabel;
|
||||
userInfoNotesLabel = new QLabel;
|
||||
userInfoNotesEdit = new QTextEdit;
|
||||
userInfoNotesEdit->setReadOnly(true);
|
||||
|
||||
auto *infoGrid = new QGridLayout;
|
||||
infoGrid->addWidget(userInfoNameLabel, 0, 0);
|
||||
infoGrid->addWidget(userInfoNameValue, 0, 1);
|
||||
infoGrid->addWidget(userInfoRegisteredLabel, 0, 2);
|
||||
infoGrid->addWidget(userInfoRegisteredValue, 0, 3);
|
||||
infoGrid->addWidget(userInfoLastLoginLabel, 1, 0);
|
||||
infoGrid->addWidget(userInfoLastLoginValue, 1, 1);
|
||||
infoGrid->addWidget(userInfoStatusLabel, 1, 2);
|
||||
infoGrid->addWidget(userInfoStatusValue, 1, 3);
|
||||
infoGrid->addWidget(userInfoCountsLabel, 2, 0);
|
||||
infoGrid->addWidget(userInfoCountsValue, 2, 1, 1, 3);
|
||||
infoGrid->addWidget(userInfoNotesLabel, 3, 0, Qt::AlignTop);
|
||||
infoGrid->addWidget(userInfoNotesEdit, 3, 1, 1, 3);
|
||||
infoGrid->setColumnStretch(1, 1);
|
||||
infoGrid->setColumnStretch(3, 1);
|
||||
|
||||
auto *infoLayout = new QVBoxLayout(userInfoGroup);
|
||||
infoLayout->addLayout(infoGrid);
|
||||
|
||||
auto configureTable = [](QTableWidget *table) {
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setStretchLastSection(true);
|
||||
};
|
||||
|
||||
altsGroup = new QGroupBox;
|
||||
altsTable = new QTableWidget(0, COL_ALTS_COUNT);
|
||||
configureTable(altsTable);
|
||||
auto *altsLayout = new QVBoxLayout(altsGroup);
|
||||
altsLayout->addWidget(altsTable);
|
||||
|
||||
sessionsGroup = new QGroupBox;
|
||||
sessionsTable = new QTableWidget(0, COL_SESSIONS_COUNT);
|
||||
configureTable(sessionsTable);
|
||||
auto *sessionsLayout = new QVBoxLayout(sessionsGroup);
|
||||
sessionsLayout->addWidget(sessionsTable);
|
||||
|
||||
staffGroup = new QGroupBox;
|
||||
staffTable = new QTableWidget(0, COL_STAFF_COUNT);
|
||||
configureTable(staffTable);
|
||||
refreshStaffButton = new QPushButton;
|
||||
connect(refreshStaffButton, &QPushButton::clicked, this, &TabModeration::requestModeratorLogins);
|
||||
auto *staffHeader = new QHBoxLayout;
|
||||
staffHeader->addStretch();
|
||||
staffHeader->addWidget(refreshStaffButton);
|
||||
auto *staffLayout = new QVBoxLayout(staffGroup);
|
||||
staffLayout->addWidget(staffTable);
|
||||
staffLayout->addLayout(staffHeader);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Vertical);
|
||||
splitter->addWidget(userInfoGroup);
|
||||
splitter->addWidget(altsGroup);
|
||||
splitter->addWidget(sessionsGroup);
|
||||
splitter->addWidget(staffGroup);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 2);
|
||||
splitter->setStretchFactor(2, 2);
|
||||
splitter->setStretchFactor(3, 1);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(centralWidget);
|
||||
mainLayout->addLayout(topBar);
|
||||
mainLayout->addWidget(splitter);
|
||||
|
||||
retranslateUi();
|
||||
clearUserData();
|
||||
requestModeratorLogins();
|
||||
investigate(initialUser);
|
||||
}
|
||||
|
||||
void TabModeration::retranslateUi()
|
||||
{
|
||||
searchEdit->setPlaceholderText(tr("User name"));
|
||||
investigateButton->setText(tr("Investigate"));
|
||||
resetPasswordButton->setText(tr("Reset Password"));
|
||||
removeAvatarButton->setText(tr("Remove Avatar"));
|
||||
refreshStaffButton->setText(tr("Refresh"));
|
||||
|
||||
userInfoGroup->setTitle(tr("User Info"));
|
||||
userInfoNameLabel->setText(tr("Name:"));
|
||||
userInfoRegisteredLabel->setText(tr("Registered:"));
|
||||
userInfoLastLoginLabel->setText(tr("Last login:"));
|
||||
userInfoStatusLabel->setText(tr("Status:"));
|
||||
userInfoCountsLabel->setText(tr("Counts:"));
|
||||
userInfoNotesLabel->setText(tr("Admin notes:"));
|
||||
|
||||
altsGroup->setTitle(tr("Alts"));
|
||||
sessionsGroup->setTitle(tr("Sessions"));
|
||||
staffGroup->setTitle(tr("Staff Last Logins"));
|
||||
|
||||
altsTable->setHorizontalHeaderLabels({tr("User"), tr("eMail"), tr("Client ID"), tr("Registered"), tr("Last login"),
|
||||
tr("Warns"), tr("Bans"), tr("Active")});
|
||||
sessionsTable->setHorizontalHeaderLabels({tr("IP"), tr("Client ID"), tr("Start"), tr("End"), tr("Type")});
|
||||
staffTable->setHorizontalHeaderLabels({tr("User"), tr("Level"), tr("Last login")});
|
||||
}
|
||||
|
||||
QString TabModeration::formatEpoch(quint64 ts) const
|
||||
{
|
||||
if (ts == 0) {
|
||||
return tr("Unknown");
|
||||
}
|
||||
return QDateTime::fromSecsSinceEpoch(ts).toLocalTime().toString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
void TabModeration::clearUserData()
|
||||
{
|
||||
currentUser.clear();
|
||||
userInfoNameValue->clear();
|
||||
userInfoRegisteredValue->clear();
|
||||
userInfoLastLoginValue->clear();
|
||||
userInfoStatusValue->clear();
|
||||
userInfoCountsValue->clear();
|
||||
userInfoNotesEdit->clear();
|
||||
altsTable->setRowCount(0);
|
||||
sessionsTable->setRowCount(0);
|
||||
resetPasswordButton->setEnabled(false);
|
||||
removeAvatarButton->setEnabled(false);
|
||||
}
|
||||
|
||||
void TabModeration::investigate(const QString &userName)
|
||||
{
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
searchEdit->setText(userName);
|
||||
investigateUser();
|
||||
}
|
||||
|
||||
void TabModeration::investigateUser()
|
||||
{
|
||||
const QString userName = searchEdit->text().simplified();
|
||||
if (userName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
currentUser = userName;
|
||||
resetPasswordButton->setEnabled(true);
|
||||
removeAvatarButton->setEnabled(true);
|
||||
altsTable->setRowCount(0);
|
||||
sessionsTable->setRowCount(0);
|
||||
requestUserInfo(userName);
|
||||
requestSessions(userName);
|
||||
requestAlts(userName);
|
||||
}
|
||||
|
||||
void TabModeration::requestUserInfo(const QString &userName)
|
||||
{
|
||||
userInfoNameValue->setText(userName);
|
||||
userInfoRegisteredValue->setText(tr("Loading..."));
|
||||
userInfoLastLoginValue->setText(tr("Loading..."));
|
||||
userInfoStatusValue->clear();
|
||||
userInfoCountsValue->clear();
|
||||
userInfoNotesEdit->clear();
|
||||
|
||||
Command_ReportUserInfo cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::userInfoResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::userInfoResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
userInfoRegisteredValue->clear();
|
||||
userInfoLastLoginValue->clear();
|
||||
userInfoStatusValue->setText(tr("Error loading user info."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext);
|
||||
if (resp.has_user_name() && QString::fromStdString(resp.user_name()) != currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
userInfoRegisteredValue->setText(formatEpoch(resp.registration_time()));
|
||||
userInfoLastLoginValue->setText(formatEpoch(resp.last_login()));
|
||||
|
||||
QStringList statusParts;
|
||||
statusParts << (resp.is_active() ? tr("active") : tr("inactive"));
|
||||
if (resp.has_is_admin() && resp.is_admin()) {
|
||||
statusParts << tr("admin");
|
||||
}
|
||||
userInfoStatusValue->setText(statusParts.join(", "));
|
||||
|
||||
userInfoCountsValue->setText(tr("Reports: %1 Bans: %2 Warnings: %3")
|
||||
.arg(resp.total_reports())
|
||||
.arg(resp.total_bans())
|
||||
.arg(resp.total_warns()));
|
||||
|
||||
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
|
||||
userInfoNotesEdit->setPlainText(QString::fromStdString(resp.admin_notes()));
|
||||
} else {
|
||||
userInfoNotesEdit->setPlainText(tr("(none)"));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::requestSessions(const QString &userName)
|
||||
{
|
||||
Command_GetUserSessions cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::sessionsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::sessionsResponse(const Response &response)
|
||||
{
|
||||
sessionsTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_UserSessions &resp = response.GetExtension(Response_UserSessions::ext);
|
||||
sessionsTable->setRowCount(resp.sessions_size());
|
||||
for (int i = 0; i < resp.sessions_size(); ++i) {
|
||||
const ServerInfo_UserSession &session = resp.sessions(i);
|
||||
sessionsTable->setItem(i, COL_SESSIONS_IP, new QTableWidgetItem(QString::fromStdString(session.ip_address())));
|
||||
sessionsTable->setItem(i, COL_SESSIONS_CLIENTID,
|
||||
new QTableWidgetItem(QString::fromStdString(session.clientid())));
|
||||
sessionsTable->setItem(i, COL_SESSIONS_START, new QTableWidgetItem(formatEpoch(session.start_time())));
|
||||
sessionsTable->setItem(
|
||||
i, COL_SESSIONS_END,
|
||||
new QTableWidgetItem(session.end_time() == 0 ? tr("Active") : formatEpoch(session.end_time())));
|
||||
sessionsTable->setItem(i, COL_SESSIONS_TYPE,
|
||||
new QTableWidgetItem(QString::fromStdString(session.connection_type())));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::requestAlts(const QString &userName)
|
||||
{
|
||||
Command_GetUserAlts cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::altsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::altsResponse(const Response &response)
|
||||
{
|
||||
altsTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_UserAlts &resp = response.GetExtension(Response_UserAlts::ext);
|
||||
altsTable->setRowCount(resp.alts_size());
|
||||
for (int i = 0; i < resp.alts_size(); ++i) {
|
||||
const ServerInfo_UserAlt &alt = resp.alts(i);
|
||||
altsTable->setItem(i, COL_ALTS_USER, new QTableWidgetItem(QString::fromStdString(alt.user_name())));
|
||||
altsTable->setItem(i, COL_ALTS_EMAIL, new QTableWidgetItem(QString::fromStdString(alt.email())));
|
||||
altsTable->setItem(i, COL_ALTS_CLIENTID, new QTableWidgetItem(QString::fromStdString(alt.clientid())));
|
||||
altsTable->setItem(i, COL_ALTS_REGISTERED, new QTableWidgetItem(formatEpoch(alt.registration_time())));
|
||||
altsTable->setItem(i, COL_ALTS_LAST_LOGIN, new QTableWidgetItem(formatEpoch(alt.last_login())));
|
||||
altsTable->setItem(i, COL_ALTS_WARNS, new QTableWidgetItem(QString::number(alt.warn_count())));
|
||||
altsTable->setItem(i, COL_ALTS_BANS, new QTableWidgetItem(QString::number(alt.ban_count())));
|
||||
altsTable->setItem(i, COL_ALTS_ACTIVE, new QTableWidgetItem(alt.is_active() ? tr("yes") : tr("no")));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::requestModeratorLogins()
|
||||
{
|
||||
Command_GetModeratorLastLogins cmd;
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::moderatorLoginsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::moderatorLoginsResponse(const Response &response)
|
||||
{
|
||||
staffTable->setRowCount(0);
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ModeratorLastLogins &resp = response.GetExtension(Response_ModeratorLastLogins::ext);
|
||||
staffTable->setRowCount(resp.logins_size());
|
||||
for (int i = 0; i < resp.logins_size(); ++i) {
|
||||
const ServerInfo_ModeratorLogin &login = resp.logins(i);
|
||||
staffTable->setItem(i, COL_STAFF_USER, new QTableWidgetItem(QString::fromStdString(login.user_name())));
|
||||
staffTable->setItem(i, COL_STAFF_LAST_LOGIN, new QTableWidgetItem(formatEpoch(login.last_login())));
|
||||
|
||||
QStringList levels;
|
||||
if (login.user_level() & ServerInfo_User::IsAdmin) {
|
||||
levels << tr("Admin");
|
||||
}
|
||||
if (login.user_level() & ServerInfo_User::IsModerator) {
|
||||
levels << tr("Moderator");
|
||||
}
|
||||
if (login.user_level() & ServerInfo_User::IsJudge) {
|
||||
levels << tr("Judge");
|
||||
}
|
||||
staffTable->setItem(i, COL_STAFF_LEVEL, new QTableWidgetItem(levels.join(" / ")));
|
||||
}
|
||||
}
|
||||
|
||||
void TabModeration::resetPassword()
|
||||
{
|
||||
if (currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton choice =
|
||||
QMessageBox::warning(this, tr("Reset Password"),
|
||||
tr("Reset the password of %1? A temporary password will be generated and shown to you. "
|
||||
"The user must change it on their first login.")
|
||||
.arg(currentUser),
|
||||
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
|
||||
if (choice != QMessageBox::Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_ResetUserPassword cmd;
|
||||
cmd.set_user_name(currentUser.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::resetPasswordResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::resetPasswordResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Password reset failed."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ResetUserPassword &resp = response.GetExtension(Response_ResetUserPassword::ext);
|
||||
QMessageBox::information(this, tr("Password reset"),
|
||||
tr("Temporary password for %1:\n\n%2\n\nPass it to the user through a secure channel.")
|
||||
.arg(QString::fromStdString(resp.user_name()))
|
||||
.arg(QString::fromStdString(resp.temporary_password())));
|
||||
}
|
||||
|
||||
void TabModeration::removeAvatar()
|
||||
{
|
||||
if (currentUser.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton choice =
|
||||
QMessageBox::warning(this, tr("Remove Avatar"),
|
||||
tr("Remove the avatar of %1? The user will have to upload a new one.").arg(currentUser),
|
||||
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);
|
||||
if (choice != QMessageBox::Ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
Command_RemoveUserAvatar cmd;
|
||||
cmd.set_user_name(currentUser.toStdString());
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabModeration::removeAvatarResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabModeration::removeAvatarResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Could not remove the avatar."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_RemoveUserAvatar &resp = response.GetExtension(Response_RemoveUserAvatar::ext);
|
||||
QMessageBox::information(this, tr("Avatar removed"),
|
||||
tr("The avatar of %1 has been removed.").arg(QString::fromStdString(resp.user_name())));
|
||||
}
|
||||
83
cockatrice/src/interface/widgets/tabs/tab_moderation.h
Normal file
83
cockatrice/src/interface/widgets/tabs/tab_moderation.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#ifndef TAB_MODERATION_H
|
||||
#define TAB_MODERATION_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
|
||||
/**
|
||||
* Staff investigation tool. Lets moderators look up a user's account data,
|
||||
* alternate accounts, login sessions, and staff login activity, and offers
|
||||
* the password-reset and remove-avatar actions.
|
||||
*/
|
||||
class TabModeration : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TabModeration(TabSupervisor *_tabSupervisor, AbstractClient *_client, const QString &initialUser = {});
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Moderation");
|
||||
}
|
||||
void investigate(const QString &userName);
|
||||
|
||||
private slots:
|
||||
void investigateUser();
|
||||
void userInfoResponse(const Response &response);
|
||||
void sessionsResponse(const Response &response);
|
||||
void altsResponse(const Response &response);
|
||||
void moderatorLoginsResponse(const Response &response);
|
||||
void resetPassword();
|
||||
void resetPasswordResponse(const Response &response);
|
||||
void removeAvatar();
|
||||
void removeAvatarResponse(const Response &response);
|
||||
|
||||
private:
|
||||
void requestUserInfo(const QString &userName);
|
||||
void requestSessions(const QString &userName);
|
||||
void requestAlts(const QString &userName);
|
||||
void requestModeratorLogins();
|
||||
void clearUserData();
|
||||
[[nodiscard]] QString formatEpoch(quint64 ts) const;
|
||||
|
||||
AbstractClient *client;
|
||||
QString currentUser;
|
||||
|
||||
QLineEdit *searchEdit;
|
||||
QPushButton *investigateButton;
|
||||
QPushButton *resetPasswordButton;
|
||||
QPushButton *removeAvatarButton;
|
||||
|
||||
QGroupBox *userInfoGroup;
|
||||
QLabel *userInfoNameLabel;
|
||||
QLabel *userInfoNameValue;
|
||||
QLabel *userInfoRegisteredLabel;
|
||||
QLabel *userInfoRegisteredValue;
|
||||
QLabel *userInfoLastLoginLabel;
|
||||
QLabel *userInfoLastLoginValue;
|
||||
QLabel *userInfoStatusLabel;
|
||||
QLabel *userInfoStatusValue;
|
||||
QLabel *userInfoCountsLabel;
|
||||
QLabel *userInfoCountsValue;
|
||||
QLabel *userInfoNotesLabel;
|
||||
QTextEdit *userInfoNotesEdit;
|
||||
|
||||
QGroupBox *altsGroup;
|
||||
QTableWidget *altsTable;
|
||||
QGroupBox *sessionsGroup;
|
||||
QTableWidget *sessionsTable;
|
||||
QGroupBox *staffGroup;
|
||||
QTableWidget *staffTable;
|
||||
QPushButton *refreshStaffButton;
|
||||
};
|
||||
|
||||
#endif // TAB_MODERATION_H
|
||||
927
cockatrice/src/interface/widgets/tabs/tab_report.cpp
Normal file
927
cockatrice/src/interface/widgets/tabs/tab_report.cpp
Normal file
|
|
@ -0,0 +1,927 @@
|
|||
#include "tab_report.h"
|
||||
|
||||
#include "../utility/report_utils.h"
|
||||
#include "abstract_client.h"
|
||||
#include "tab_supervisor.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDateTime>
|
||||
#include <QFontDatabase>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSplitter>
|
||||
#include <QTableWidget>
|
||||
#include <QTextCursor>
|
||||
#include <QTextEdit>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_replay_download_by_game_id.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_details.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_list.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_stats.pb.h>
|
||||
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int COL_ID = 0;
|
||||
constexpr int COL_TIME = 1;
|
||||
constexpr int COL_REPORTER = 2;
|
||||
constexpr int COL_REPORTED = 3;
|
||||
constexpr int COL_CATEGORY = 4;
|
||||
constexpr int COL_GAMEID = 5;
|
||||
constexpr int COL_STATUS = 6;
|
||||
constexpr int COL_ASSIGNED = 7;
|
||||
constexpr int COL_REPLAY = 8;
|
||||
constexpr int COL_ROOM = 9;
|
||||
constexpr int COL_COUNT = 10;
|
||||
constexpr int REFRESH_INTERVAL_MS = 300000;
|
||||
} // namespace
|
||||
|
||||
TabReport::TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
auto *centralWidget = new QWidget(this);
|
||||
setCentralWidget(centralWidget);
|
||||
|
||||
searchEdit = new QLineEdit;
|
||||
searchEdit->setClearButtonEnabled(true);
|
||||
connect(searchEdit, &QLineEdit::textChanged, this, &TabReport::applyFilters);
|
||||
|
||||
statusFilter = new QComboBox;
|
||||
connect(statusFilter, &QComboBox::currentIndexChanged, this, &TabReport::applyFilters);
|
||||
|
||||
unresolvedOnlyBox = new QCheckBox;
|
||||
unresolvedOnlyBox->setChecked(true);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
|
||||
connect(unresolvedOnlyBox, &QCheckBox::checkStateChanged, this, &TabReport::refreshList);
|
||||
#else
|
||||
connect(unresolvedOnlyBox, &QCheckBox::stateChanged, this, &TabReport::refreshList);
|
||||
#endif
|
||||
|
||||
refreshButton = new QPushButton;
|
||||
connect(refreshButton, &QPushButton::clicked, this, &TabReport::refreshList);
|
||||
|
||||
refreshTimer = new QTimer(this);
|
||||
refreshTimer->setInterval(REFRESH_INTERVAL_MS);
|
||||
connect(refreshTimer, &QTimer::timeout, this, [this]() {
|
||||
if (tabSupervisor->currentWidget() == this) {
|
||||
refreshList();
|
||||
}
|
||||
});
|
||||
refreshTimer->start();
|
||||
|
||||
auto *topBar = new QHBoxLayout;
|
||||
topBar->addWidget(searchEdit);
|
||||
topBar->addWidget(statusFilter);
|
||||
topBar->addWidget(unresolvedOnlyBox);
|
||||
topBar->addStretch();
|
||||
topBar->addWidget(refreshButton);
|
||||
|
||||
statsLabel = new QLabel;
|
||||
|
||||
table = new QTableWidget(0, COL_COUNT);
|
||||
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
table->setSortingEnabled(true);
|
||||
table->verticalHeader()->setVisible(false);
|
||||
table->setAlternatingRowColors(true);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_TIME, QHeaderView::ResizeToContents);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTER, QHeaderView::ResizeToContents);
|
||||
connect(table, &QTableWidget::itemSelectionChanged, this, &TabReport::onSelectionChanged);
|
||||
|
||||
descGroup = new QGroupBox;
|
||||
descriptionEdit = new QTextEdit;
|
||||
descriptionEdit->setReadOnly(true);
|
||||
auto *descLayout = new QVBoxLayout(descGroup);
|
||||
descLayout->setContentsMargins(4, 4, 4, 4);
|
||||
descLayout->addWidget(descriptionEdit);
|
||||
|
||||
chatGroup = new QGroupBox;
|
||||
chatLogEdit = new QTextEdit;
|
||||
chatLogEdit->setReadOnly(true);
|
||||
QFont monoFont("monospace");
|
||||
monoFont.setStyleHint(QFont::Monospace);
|
||||
const int systemPointSize = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize();
|
||||
if (systemPointSize > 0) {
|
||||
monoFont.setPointSize(systemPointSize);
|
||||
}
|
||||
chatLogEdit->setFont(monoFont);
|
||||
auto *chatLayout = new QVBoxLayout(chatGroup);
|
||||
chatLayout->setContentsMargins(4, 4, 4, 4);
|
||||
chatLayout->addWidget(chatLogEdit);
|
||||
|
||||
commentsGroup = new QGroupBox;
|
||||
commentsEdit = new QTextEdit;
|
||||
commentsEdit->setReadOnly(true);
|
||||
auto *commentInputRow = new QHBoxLayout;
|
||||
commentInput = new QLineEdit;
|
||||
commentButton = new QPushButton;
|
||||
commentButton->setEnabled(false);
|
||||
connect(commentButton, &QPushButton::clicked, this, &TabReport::addComment);
|
||||
connect(commentInput, &QLineEdit::returnPressed, this, &TabReport::addComment);
|
||||
commentInputRow->addWidget(commentInput);
|
||||
commentInputRow->addWidget(commentButton);
|
||||
auto *commentsLayout = new QVBoxLayout(commentsGroup);
|
||||
commentsLayout->setContentsMargins(4, 4, 4, 4);
|
||||
commentsLayout->addWidget(commentsEdit);
|
||||
commentsLayout->addLayout(commentInputRow);
|
||||
|
||||
userContextGroup = new QGroupBox;
|
||||
userContextName = new QLabel;
|
||||
userContextAccountAge = new QLabel;
|
||||
userContextReports = new QLabel;
|
||||
userContextBans = new QLabel;
|
||||
userContextWarns = new QLabel;
|
||||
userContextNotes = new QTextEdit;
|
||||
userContextNotes->setReadOnly(true);
|
||||
userContextNotes->setMaximumHeight(80);
|
||||
userContextRecentReports = new QTextEdit;
|
||||
userContextRecentReports->setReadOnly(true);
|
||||
userContextRecentReports->setMaximumHeight(120);
|
||||
userContextUserLabel = new QLabel;
|
||||
userContextAgeLabel = new QLabel;
|
||||
userContextReportsLabel = new QLabel;
|
||||
userContextBansLabel = new QLabel;
|
||||
userContextWarnsLabel = new QLabel;
|
||||
userContextNotesLabel = new QLabel;
|
||||
userContextRecentReportsLabel = new QLabel;
|
||||
auto *ucLayout = new QGridLayout(userContextGroup);
|
||||
ucLayout->setContentsMargins(8, 8, 8, 8);
|
||||
ucLayout->addWidget(userContextUserLabel, 0, 0);
|
||||
ucLayout->addWidget(userContextName, 0, 1, 1, 3);
|
||||
ucLayout->addWidget(userContextAgeLabel, 1, 0);
|
||||
ucLayout->addWidget(userContextAccountAge, 1, 1);
|
||||
ucLayout->addWidget(userContextReportsLabel, 1, 2);
|
||||
ucLayout->addWidget(userContextReports, 1, 3);
|
||||
ucLayout->addWidget(userContextBansLabel, 2, 0);
|
||||
ucLayout->addWidget(userContextBans, 2, 1);
|
||||
ucLayout->addWidget(userContextWarnsLabel, 2, 2);
|
||||
ucLayout->addWidget(userContextWarns, 2, 3);
|
||||
ucLayout->addWidget(userContextNotesLabel, 3, 0, Qt::AlignTop);
|
||||
ucLayout->addWidget(userContextNotes, 3, 1, 1, 3);
|
||||
ucLayout->addWidget(userContextRecentReportsLabel, 4, 0, 1, 4);
|
||||
ucLayout->addWidget(userContextRecentReports, 5, 0, 1, 4);
|
||||
userContextGroup->setVisible(false);
|
||||
|
||||
statsGroup = new QGroupBox;
|
||||
statsGroup->setCheckable(true);
|
||||
statsTotalLabel = new QLabel;
|
||||
statsTrendLabel = new QLabel;
|
||||
statsCategoriesLabel = new QLabel;
|
||||
statsDetailText = new QTextEdit;
|
||||
statsDetailText->setReadOnly(true);
|
||||
statsDetailText->setMaximumHeight(150);
|
||||
auto *statsContent = new QWidget;
|
||||
auto *sgLayout = new QVBoxLayout(statsContent);
|
||||
sgLayout->setContentsMargins(8, 8, 8, 8);
|
||||
sgLayout->addWidget(statsTotalLabel);
|
||||
sgLayout->addWidget(statsTrendLabel);
|
||||
sgLayout->addWidget(statsCategoriesLabel);
|
||||
sgLayout->addWidget(statsDetailText);
|
||||
auto *statsGroupLayout = new QVBoxLayout(statsGroup);
|
||||
statsGroupLayout->setContentsMargins(0, 0, 0, 0);
|
||||
statsGroupLayout->addWidget(statsContent);
|
||||
statsGroup->setChecked(true);
|
||||
connect(statsGroup, &QGroupBox::toggled, this, [this, statsContent](bool checked) {
|
||||
statsContent->setVisible(checked);
|
||||
if (checked) {
|
||||
requestStats();
|
||||
}
|
||||
});
|
||||
|
||||
detailSplitter = new QSplitter(Qt::Vertical);
|
||||
detailSplitter->addWidget(descGroup);
|
||||
detailSplitter->addWidget(chatGroup);
|
||||
detailSplitter->addWidget(commentsGroup);
|
||||
detailSplitter->setStretchFactor(0, 1);
|
||||
detailSplitter->setStretchFactor(1, 1);
|
||||
detailSplitter->setStretchFactor(2, 2);
|
||||
|
||||
assignButton = new QPushButton;
|
||||
resolveButton = new QPushButton;
|
||||
resolveWithNoteButton = new QPushButton;
|
||||
dismissButton = new QPushButton;
|
||||
viewReplayButton = new QPushButton;
|
||||
joinGameButton = new QPushButton;
|
||||
connect(assignButton, &QPushButton::clicked, this, &TabReport::assignReport);
|
||||
connect(resolveButton, &QPushButton::clicked, this, [this]() { resolveReport(false, false); });
|
||||
connect(resolveWithNoteButton, &QPushButton::clicked, this, [this]() { resolveReport(false, true); });
|
||||
connect(dismissButton, &QPushButton::clicked, this, [this]() { resolveReport(true, true); });
|
||||
connect(viewReplayButton, &QPushButton::clicked, this, &TabReport::viewReplay);
|
||||
connect(joinGameButton, &QPushButton::clicked, this, &TabReport::joinGame);
|
||||
|
||||
statusLabel = new QLabel;
|
||||
|
||||
auto *actionBar = new QHBoxLayout;
|
||||
actionBar->addWidget(assignButton);
|
||||
actionBar->addWidget(resolveButton);
|
||||
actionBar->addWidget(resolveWithNoteButton);
|
||||
actionBar->addWidget(dismissButton);
|
||||
actionBar->addSpacing(20);
|
||||
actionBar->addWidget(viewReplayButton);
|
||||
actionBar->addWidget(joinGameButton);
|
||||
actionBar->addStretch();
|
||||
actionBar->addWidget(statusLabel);
|
||||
|
||||
auto *layout = new QVBoxLayout(centralWidget);
|
||||
layout->addLayout(topBar);
|
||||
layout->addWidget(statsLabel);
|
||||
layout->addWidget(table, 2);
|
||||
layout->addWidget(detailSplitter, 1);
|
||||
layout->addWidget(userContextGroup);
|
||||
layout->addWidget(statsGroup);
|
||||
layout->addLayout(actionBar);
|
||||
|
||||
retranslateUi();
|
||||
|
||||
setActionsEnabled(false);
|
||||
refreshList();
|
||||
}
|
||||
|
||||
void TabReport::retranslateUi()
|
||||
{
|
||||
searchEdit->setPlaceholderText(tr("Search by username, category..."));
|
||||
statusFilter->clear();
|
||||
statusFilter->addItem(tr("All Statuses"), "");
|
||||
statusFilter->addItem(tr("Open"), "open");
|
||||
statusFilter->addItem(tr("Assigned"), "assigned");
|
||||
statusFilter->addItem(tr("Resolved"), "resolved");
|
||||
statusFilter->addItem(tr("Dismissed"), "dismissed");
|
||||
unresolvedOnlyBox->setText(tr("Unresolved only"));
|
||||
refreshButton->setText(tr("Refresh"));
|
||||
|
||||
table->setHorizontalHeaderLabels({tr("#"), tr("Time"), tr("Reporter"), tr("Reported User"), tr("Category"),
|
||||
tr("Game ID"), tr("Status"), tr("Assigned To"), tr("Replay"), tr("Room")});
|
||||
|
||||
descGroup->setTitle(tr("Description"));
|
||||
chatGroup->setTitle(tr("Chat Log Context"));
|
||||
chatGroup->setToolTip(
|
||||
tr("Chat log attached by the reporter. It is captured from their client and may be incomplete or edited."));
|
||||
commentsGroup->setTitle(tr("Comments / Thread"));
|
||||
descriptionEdit->setPlaceholderText(tr("No description."));
|
||||
chatLogEdit->setPlaceholderText(tr("No chat log attached."));
|
||||
commentsEdit->setPlaceholderText(tr("No comments yet."));
|
||||
commentInput->setPlaceholderText(tr("Type a reply..."));
|
||||
commentButton->setText(tr("Send"));
|
||||
|
||||
userContextGroup->setTitle(tr("Reported User Context"));
|
||||
userContextUserLabel->setText(tr("User:"));
|
||||
userContextAgeLabel->setText(tr("Account Age:"));
|
||||
userContextReportsLabel->setText(tr("Reports:"));
|
||||
userContextBansLabel->setText(tr("Bans:"));
|
||||
userContextWarnsLabel->setText(tr("Warns:"));
|
||||
userContextNotesLabel->setText(tr("Admin Notes:"));
|
||||
userContextRecentReportsLabel->setText(tr("Recent Reports Against User:"));
|
||||
userContextAccountAge->setText(QString());
|
||||
userContextReports->setText(QString());
|
||||
userContextBans->setText(QString());
|
||||
userContextWarns->setText(QString());
|
||||
|
||||
statsGroup->setTitle(tr("Report Statistics"));
|
||||
|
||||
assignButton->setText(tr("Assign to Me"));
|
||||
resolveButton->setText(tr("Resolve"));
|
||||
resolveWithNoteButton->setText(tr("Resolve with note..."));
|
||||
dismissButton->setText(tr("Dismiss..."));
|
||||
viewReplayButton->setText(tr("View Replay"));
|
||||
joinGameButton->setText(tr("Join Game"));
|
||||
}
|
||||
|
||||
void TabReport::refreshList()
|
||||
{
|
||||
selectedReportIdBeforeRefresh = selectedReportId();
|
||||
commentDraftBeforeRefresh = commentInput->text();
|
||||
previousSelectedReportValid = selectedReportInfo(previousSelectedReport);
|
||||
|
||||
refreshButton->setEnabled(false);
|
||||
statusLabel->setText(tr("Loading..."));
|
||||
table->setRowCount(0);
|
||||
allReports.clear();
|
||||
filteredReports.clear();
|
||||
commentButton->setEnabled(false);
|
||||
setActionsEnabled(false);
|
||||
|
||||
Command_ReportList cmd;
|
||||
cmd.set_unresolved_only(unresolvedOnlyBox->isChecked());
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::reportListResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::reportListResponse(const Response &response)
|
||||
{
|
||||
refreshButton->setEnabled(true);
|
||||
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("Failed to load reports."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportList &resp = response.GetExtension(Response_ReportList::ext);
|
||||
allReports.clear();
|
||||
for (int i = 0; i < resp.reports_size(); ++i) {
|
||||
allReports.append(resp.reports(i));
|
||||
}
|
||||
|
||||
applyFilters();
|
||||
updateStats();
|
||||
|
||||
if (selectedReportIdBeforeRefresh >= 0) {
|
||||
bool found = false;
|
||||
for (int row = 0; row < table->rowCount(); ++row) {
|
||||
if (table->item(row, COL_ID) &&
|
||||
table->item(row, COL_ID)->data(Qt::UserRole).toInt() == selectedReportIdBeforeRefresh) {
|
||||
found = true;
|
||||
{
|
||||
QSignalBlocker blocker(table);
|
||||
table->setCurrentCell(row, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
bool dataChanged = false;
|
||||
ServerInfo_Report newReport;
|
||||
if (!selectedReportInfo(newReport) || !previousSelectedReportValid ||
|
||||
previousSelectedReport.description() != newReport.description() ||
|
||||
previousSelectedReport.status() != newReport.status() ||
|
||||
previousSelectedReport.resolution_note() != newReport.resolution_note() ||
|
||||
previousSelectedReport.assigned_mod_name() != newReport.assigned_mod_name()) {
|
||||
dataChanged = true;
|
||||
}
|
||||
|
||||
if (dataChanged || detailsRequestedReportId != selectedReportIdBeforeRefresh) {
|
||||
loadReportDetails(selectedReportIdBeforeRefresh);
|
||||
}
|
||||
updateActionStates();
|
||||
} else {
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
setActionsEnabled(false);
|
||||
userContextGroup->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (commentInput->text().isEmpty()) {
|
||||
commentInput->setText(commentDraftBeforeRefresh);
|
||||
}
|
||||
|
||||
if (statsGroup->isChecked()) {
|
||||
requestStats();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::applyFilters()
|
||||
{
|
||||
filteredReports.clear();
|
||||
|
||||
const QString searchText = searchEdit->text().toLower();
|
||||
const QString statusFilterValue = statusFilter->currentData().toString();
|
||||
|
||||
for (const ServerInfo_Report &r : allReports) {
|
||||
bool matchesSearch = searchText.isEmpty() ||
|
||||
QString::fromStdString(r.reporter_name()).toLower().contains(searchText) ||
|
||||
QString::fromStdString(r.reported_user_name()).toLower().contains(searchText) ||
|
||||
QString::fromStdString(r.category()).toLower().contains(searchText) ||
|
||||
QString::fromStdString(r.description()).toLower().contains(searchText);
|
||||
|
||||
bool matchesStatus = statusFilterValue.isEmpty() || QString::fromStdString(r.status()) == statusFilterValue;
|
||||
|
||||
if (matchesSearch && matchesStatus) {
|
||||
filteredReports.append(r);
|
||||
}
|
||||
}
|
||||
|
||||
table->setSortingEnabled(false);
|
||||
table->setRowCount(filteredReports.size());
|
||||
|
||||
for (int row = 0; row < filteredReports.size(); ++row) {
|
||||
const ServerInfo_Report &r = filteredReports[row];
|
||||
|
||||
report_utils::fillReportTableRow(table, row, r, COL_ID, COL_TIME, COL_REPORTED, COL_CATEGORY, COL_GAMEID,
|
||||
COL_STATUS, COL_ASSIGNED);
|
||||
|
||||
table->setItem(row, COL_REPORTER, new QTableWidgetItem(QString::fromStdString(r.reporter_name())));
|
||||
|
||||
table->setItem(row, COL_REPLAY,
|
||||
new QTableWidgetItem(r.has_replay_id() && r.replay_id() > 0 ? tr("Yes") : tr("No")));
|
||||
|
||||
table->setItem(row, COL_ROOM,
|
||||
new QTableWidgetItem(r.has_room_id() && r.room_id() > 0 ? QString::number(r.room_id()) : ""));
|
||||
}
|
||||
|
||||
table->setSortingEnabled(true);
|
||||
table->resizeColumnsToContents();
|
||||
table->horizontalHeader()->setSectionResizeMode(COL_REPORTED, QHeaderView::Stretch);
|
||||
|
||||
statusLabel->setText(tr("%1 report(s)").arg(filteredReports.size()));
|
||||
}
|
||||
|
||||
void TabReport::updateStats()
|
||||
{
|
||||
int open = 0, assigned = 0, resolved = 0, dismissed = 0;
|
||||
for (const ServerInfo_Report &r : allReports) {
|
||||
QString status = QString::fromStdString(r.status());
|
||||
if (status == "open") {
|
||||
open++;
|
||||
} else if (status == "assigned") {
|
||||
assigned++;
|
||||
} else if (status == "resolved") {
|
||||
resolved++;
|
||||
} else if (status == "dismissed") {
|
||||
dismissed++;
|
||||
}
|
||||
}
|
||||
|
||||
statsLabel->setText(tr("%1 open | %2 assigned | %3 resolved | %4 dismissed")
|
||||
.arg(open)
|
||||
.arg(assigned)
|
||||
.arg(resolved)
|
||||
.arg(dismissed));
|
||||
}
|
||||
|
||||
void TabReport::onSelectionChanged()
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_ID)) {
|
||||
descriptionEdit->clear();
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->clear();
|
||||
commentInput->clear();
|
||||
commentButton->setEnabled(false);
|
||||
setActionsEnabled(false);
|
||||
userContextGroup->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const int reportId = table->item(row, COL_ID)->data(Qt::UserRole).toInt();
|
||||
loadReportDetails(reportId);
|
||||
updateActionStates();
|
||||
}
|
||||
|
||||
void TabReport::loadReportDetails(int reportId)
|
||||
{
|
||||
detailsRequestedReportId = reportId;
|
||||
|
||||
for (const ServerInfo_Report &r : filteredReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
descriptionEdit->setPlainText(QString::fromStdString(r.description()));
|
||||
|
||||
QString reportedUser = QString::fromStdString(r.reported_user_name());
|
||||
if (!reportedUser.isEmpty()) {
|
||||
requestUserInfo(reportedUser);
|
||||
} else {
|
||||
userContextGroup->setVisible(false);
|
||||
}
|
||||
|
||||
QString status = QString::fromStdString(r.status());
|
||||
bool canComment = (status == "open" || status == "assigned");
|
||||
commentButton->setEnabled(canComment);
|
||||
commentInput->setEnabled(canComment);
|
||||
if (!canComment) {
|
||||
commentInput->setPlaceholderText(tr("This report is closed."));
|
||||
} else {
|
||||
commentInput->setPlaceholderText(tr("Type a reply..."));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chatLogEdit->setPlainText(tr("Loading..."));
|
||||
commentsEdit->setPlainText(tr("Loading..."));
|
||||
|
||||
Command_ReportDetails cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
|
||||
const int seq = ++detailsRequestSeq;
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, [this, seq](const Response &response) {
|
||||
if (seq != detailsRequestSeq) {
|
||||
return;
|
||||
}
|
||||
reportDetailsResponse(response);
|
||||
});
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::reportDetailsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
if (selectedReportId() == detailsRequestedReportId) {
|
||||
chatLogEdit->clear();
|
||||
commentsEdit->setPlainText(tr("Failed to load report details."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportDetails &resp = response.GetExtension(Response_ReportDetails::ext);
|
||||
const ServerInfo_Report &r = resp.report();
|
||||
|
||||
if (selectedReportId() != r.report_id()) {
|
||||
return;
|
||||
}
|
||||
|
||||
report_utils::renderReportDetails(chatLogEdit, commentsEdit, r, tr("No comments yet."), tr("[Moderator]"),
|
||||
tr("[Reporter]"));
|
||||
}
|
||||
|
||||
void TabReport::updateActionStates()
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_STATUS)) {
|
||||
setActionsEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ServerInfo_Report report;
|
||||
if (!selectedReportInfo(report)) {
|
||||
setActionsEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString status = QString::fromStdString(report.status());
|
||||
assignButton->setEnabled(status == "open");
|
||||
resolveButton->setEnabled(status == "open" || status == "assigned");
|
||||
resolveWithNoteButton->setEnabled(status == "open" || status == "assigned");
|
||||
dismissButton->setEnabled(status == "open" || status == "assigned");
|
||||
|
||||
const bool hasGameId = report.game_id() > 0;
|
||||
const bool hasReplay = hasGameId && report.has_replay_id() && report.replay_id() > 0;
|
||||
viewReplayButton->setEnabled(hasReplay);
|
||||
joinGameButton->setEnabled(hasGameId && report.has_room_id() && report.room_id() > 0);
|
||||
}
|
||||
|
||||
void TabReport::setActionsEnabled(bool enabled)
|
||||
{
|
||||
assignButton->setEnabled(enabled);
|
||||
resolveButton->setEnabled(enabled);
|
||||
resolveWithNoteButton->setEnabled(enabled);
|
||||
dismissButton->setEnabled(enabled);
|
||||
viewReplayButton->setEnabled(false);
|
||||
joinGameButton->setEnabled(false);
|
||||
commentButton->setEnabled(enabled);
|
||||
commentInput->setEnabled(enabled);
|
||||
}
|
||||
|
||||
int TabReport::selectedReportId() const
|
||||
{
|
||||
const int row = table->currentRow();
|
||||
if (row < 0 || !table->item(row, COL_ID)) {
|
||||
return -1;
|
||||
}
|
||||
return table->item(row, COL_ID)->data(Qt::UserRole).toInt();
|
||||
}
|
||||
|
||||
bool TabReport::selectedReportInfo(ServerInfo_Report &info) const
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ServerInfo_Report &r : filteredReports) {
|
||||
if (r.report_id() == reportId) {
|
||||
info = r;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void TabReport::assignReport()
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(tr("Assigning..."));
|
||||
|
||||
Command_ReportAssign cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::assignResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::assignResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
statusLabel->setText(tr("Assigned."));
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Assignment failed."));
|
||||
updateActionStates();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::resolveReport(bool dismissed, bool promptNote)
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString note;
|
||||
if (promptNote) {
|
||||
bool ok;
|
||||
note = QInputDialog::getText(this, dismissed ? tr("Dismiss Report") : tr("Resolve Report"),
|
||||
dismissed ? tr("Optional note:") : tr("Resolution note (optional):"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(dismissed ? tr("Dismissing...") : tr("Resolving..."));
|
||||
|
||||
Command_ReportResolve cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
cmd.set_dismissed(dismissed);
|
||||
if (!note.isEmpty()) {
|
||||
cmd.set_resolution_note(note.toStdString());
|
||||
}
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::resolveResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::resolveResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
statusLabel->setText(tr("Done."));
|
||||
refreshList();
|
||||
} else {
|
||||
statusLabel->setText(tr("Action failed."));
|
||||
updateActionStates();
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::viewReplay()
|
||||
{
|
||||
ServerInfo_Report report;
|
||||
if (!selectedReportInfo(report) || report.game_id() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionsEnabled(false);
|
||||
statusLabel->setText(tr("Loading replay..."));
|
||||
|
||||
Command_ReplayDownloadByGameId cmd;
|
||||
cmd.set_game_id(report.game_id());
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::viewReplayResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::viewReplayResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statusLabel->setText(tr("No replay available for this game."));
|
||||
updateActionStates();
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReplayDownloadByGameId &resp = response.GetExtension(Response_ReplayDownloadByGameId::ext);
|
||||
GameReplay *replay = new GameReplay;
|
||||
if (replay->ParseFromString(resp.replay_data())) {
|
||||
emit openReplay(replay);
|
||||
statusLabel->setText(tr("Replay opened."));
|
||||
} else {
|
||||
delete replay;
|
||||
statusLabel->setText(tr("Failed to parse replay."));
|
||||
}
|
||||
|
||||
updateActionStates();
|
||||
}
|
||||
|
||||
void TabReport::joinGame()
|
||||
{
|
||||
ServerInfo_Report report;
|
||||
if (!selectedReportInfo(report) || report.game_id() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int roomId = report.has_room_id() ? report.room_id() : -1;
|
||||
if (roomId <= 0) {
|
||||
statusLabel->setText(tr("No room recorded for this report, use the replay instead."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit requestJoinGame(report.game_id(), roomId);
|
||||
statusLabel->setText(tr("Attempting to join game..."));
|
||||
}
|
||||
|
||||
void TabReport::addComment()
|
||||
{
|
||||
const int reportId = selectedReportId();
|
||||
if (reportId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = commentInput->text().trimmed();
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentButton->setEnabled(false);
|
||||
|
||||
Command_ReportAddComment cmd;
|
||||
cmd.set_report_id(reportId);
|
||||
cmd.set_comment(text.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareSessionCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::addCommentResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::addCommentResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() == Response::RespOk) {
|
||||
commentInput->clear();
|
||||
refreshList();
|
||||
} else {
|
||||
commentButton->setEnabled(true);
|
||||
statusLabel->setText(tr("Failed to send comment."));
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::requestUserInfo(const QString &userName)
|
||||
{
|
||||
if (userName.isEmpty()) {
|
||||
userContextGroup->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
lastRequestedUser = userName;
|
||||
userContextGroup->setVisible(true);
|
||||
userContextName->setText(userName);
|
||||
userContextAccountAge->setText(tr("Loading..."));
|
||||
userContextReports->clear();
|
||||
userContextBans->clear();
|
||||
userContextWarns->clear();
|
||||
userContextNotes->clear();
|
||||
userContextRecentReports->clear();
|
||||
|
||||
Command_ReportUserInfo cmd;
|
||||
cmd.set_user_name(userName.toStdString());
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::userInfoResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::userInfoResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
userContextAccountAge->setText(tr("Error loading user info."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportUserInfo &resp = response.GetExtension(Response_ReportUserInfo::ext);
|
||||
|
||||
if (resp.has_user_name() && QString::fromStdString(resp.user_name()) != lastRequestedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDateTime regTime = QDateTime::fromSecsSinceEpoch(resp.registration_time());
|
||||
qint64 daysSinceReg = regTime.daysTo(QDateTime::currentDateTime());
|
||||
userContextAccountAge->setText(tr("%1 days (since %2)").arg(daysSinceReg).arg(regTime.toString("yyyy-MM-dd")));
|
||||
|
||||
userContextReports->setText(QString::number(resp.total_reports()));
|
||||
userContextBans->setText(QString::number(resp.total_bans()));
|
||||
userContextWarns->setText(QString::number(resp.total_warns()));
|
||||
|
||||
if (resp.has_admin_notes() && !resp.admin_notes().empty()) {
|
||||
userContextNotes->setPlainText(QString::fromStdString(resp.admin_notes()));
|
||||
} else {
|
||||
userContextNotes->setPlainText(tr("(none)"));
|
||||
}
|
||||
|
||||
userContextRecentReports->clear();
|
||||
if (resp.recent_reports_size() == 0) {
|
||||
userContextRecentReports->setPlainText(tr("No previous reports against this user."));
|
||||
} else {
|
||||
for (int i = 0; i < resp.recent_reports_size(); ++i) {
|
||||
const ServerInfo_Report &r = resp.recent_reports(i);
|
||||
QDateTime dt = QDateTime::fromSecsSinceEpoch(r.report_time());
|
||||
userContextRecentReports->moveCursor(QTextCursor::End);
|
||||
userContextRecentReports->insertPlainText(QString("[%1] #%2 by %3 [%4]: %5\n")
|
||||
.arg(dt.toString("yyyy-MM-dd"))
|
||||
.arg(r.report_id())
|
||||
.arg(QString::fromStdString(r.reporter_name()))
|
||||
.arg(QString::fromStdString(r.status()))
|
||||
.arg(QString::fromStdString(r.category())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabReport::requestStats()
|
||||
{
|
||||
statsTotalLabel->setText(tr("Loading statistics..."));
|
||||
statsTrendLabel->clear();
|
||||
statsCategoriesLabel->clear();
|
||||
statsDetailText->clear();
|
||||
|
||||
Command_ReportStats cmd;
|
||||
|
||||
PendingCommand *pend = client->prepareModeratorCommand(cmd);
|
||||
connect(pend, &PendingCommand::finished, this, &TabReport::statsResponse);
|
||||
client->sendCommand(pend);
|
||||
}
|
||||
|
||||
void TabReport::statsResponse(const Response &response)
|
||||
{
|
||||
if (response.response_code() != Response::RespOk) {
|
||||
statsTotalLabel->setText(tr("Error loading statistics."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Response_ReportStats &resp = response.GetExtension(Response_ReportStats::ext);
|
||||
|
||||
statsTotalLabel->setText(tr("Total: %1 reports (%2 open, %3 assigned, %4 resolved/dismissed)")
|
||||
.arg(resp.total_reports())
|
||||
.arg(resp.total_pending())
|
||||
.arg(resp.total_assigned())
|
||||
.arg(resp.total_resolved()));
|
||||
|
||||
statsTrendLabel->setText(tr("Last 24h: %1 | Last 7d: %2 | Last 30d: %3 | Avg resolution: %4h")
|
||||
.arg(resp.reports_last_24h())
|
||||
.arg(resp.reports_last_7d())
|
||||
.arg(resp.reports_last_30d())
|
||||
.arg(resp.avg_resolution_hours(), 0, 'f', 1));
|
||||
|
||||
QString weekCompare =
|
||||
tr("This week: %1 vs last week: %2 (%3%)")
|
||||
.arg(resp.reports_this_week())
|
||||
.arg(resp.reports_last_week())
|
||||
.arg(resp.reports_last_week() > 0 ? QString::number(((resp.reports_this_week() - resp.reports_last_week()) *
|
||||
100.0 / resp.reports_last_week()),
|
||||
'f', 0)
|
||||
: resp.reports_this_week() > 0 ? "new"
|
||||
: "0");
|
||||
statsTrendLabel->setText(statsTrendLabel->text() + " | " + weekCompare);
|
||||
|
||||
statsCategoriesLabel->setText(tr("By category:"));
|
||||
statsDetailText->clear();
|
||||
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(tr("=== Top Categories ===") + "\n");
|
||||
for (int i = 0; i < resp.category_counts_size(); ++i) {
|
||||
const ReportCategoryCount &cc = resp.category_counts(i);
|
||||
QString cat = QString::fromStdString(cc.category());
|
||||
cat.replace('_', ' ');
|
||||
cat = cat.left(1).toUpper() + cat.mid(1);
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(QString(" %1: %2\n").arg(cat).arg(cc.count()));
|
||||
}
|
||||
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText("\n" + tr("=== Most Reported Users ===") + "\n");
|
||||
for (int i = 0; i < resp.top_reported_users_size(); ++i) {
|
||||
const ReportTopUser &tu = resp.top_reported_users(i);
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(
|
||||
QString(" %1: %2 reports\n").arg(QString::fromStdString(tu.user_name())).arg(tu.count()));
|
||||
}
|
||||
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText("\n" + tr("=== Top Reporters ===") + "\n");
|
||||
for (int i = 0; i < resp.top_reporters_size(); ++i) {
|
||||
const ReportTopUser &tu = resp.top_reporters(i);
|
||||
statsDetailText->moveCursor(QTextCursor::End);
|
||||
statsDetailText->insertPlainText(
|
||||
QString(" %1: %2 reports filed\n").arg(QString::fromStdString(tu.user_name())).arg(tu.count()));
|
||||
}
|
||||
}
|
||||
124
cockatrice/src/interface/widgets/tabs/tab_report.h
Normal file
124
cockatrice/src/interface/widgets/tabs/tab_report.h
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#ifndef TAB_REPORT_H
|
||||
#define TAB_REPORT_H
|
||||
|
||||
#include "tab.h"
|
||||
|
||||
#include <QList>
|
||||
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
|
||||
class AbstractClient;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QSplitter;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
class QTimer;
|
||||
class GameReplay;
|
||||
|
||||
class TabReport : public Tab
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TabReport(TabSupervisor *_tabSupervisor, AbstractClient *_client);
|
||||
void retranslateUi() override;
|
||||
[[nodiscard]] QString getTabText() const override
|
||||
{
|
||||
return tr("Report Queue");
|
||||
}
|
||||
|
||||
signals:
|
||||
void openReplay(GameReplay *replay);
|
||||
void requestJoinGame(int gameId, int roomId);
|
||||
|
||||
private slots:
|
||||
void refreshList();
|
||||
void reportListResponse(const Response &response);
|
||||
void assignReport();
|
||||
void assignResponse(const Response &response);
|
||||
void resolveReport(bool dismissed, bool promptNote);
|
||||
void resolveResponse(const Response &response);
|
||||
void onSelectionChanged();
|
||||
void viewReplay();
|
||||
void viewReplayResponse(const Response &response);
|
||||
void joinGame();
|
||||
void addComment();
|
||||
void addCommentResponse(const Response &response);
|
||||
void requestUserInfo(const QString &userName);
|
||||
void userInfoResponse(const Response &response);
|
||||
void reportDetailsResponse(const Response &response);
|
||||
void requestStats();
|
||||
void statsResponse(const Response &response);
|
||||
|
||||
private:
|
||||
int selectedReportId() const;
|
||||
bool selectedReportInfo(ServerInfo_Report &info) const;
|
||||
void setActionsEnabled(bool enabled);
|
||||
void updateActionStates();
|
||||
void applyFilters();
|
||||
void updateStats();
|
||||
void loadReportDetails(int reportId);
|
||||
|
||||
AbstractClient *client;
|
||||
|
||||
QLineEdit *searchEdit;
|
||||
QComboBox *statusFilter;
|
||||
QCheckBox *unresolvedOnlyBox;
|
||||
QPushButton *refreshButton;
|
||||
QLabel *statsLabel;
|
||||
QTableWidget *table;
|
||||
QSplitter *detailSplitter;
|
||||
QGroupBox *descGroup;
|
||||
QGroupBox *chatGroup;
|
||||
QGroupBox *commentsGroup;
|
||||
QTextEdit *descriptionEdit;
|
||||
QTextEdit *chatLogEdit;
|
||||
QTextEdit *commentsEdit;
|
||||
QLineEdit *commentInput;
|
||||
QPushButton *commentButton;
|
||||
QPushButton *assignButton;
|
||||
QPushButton *resolveButton;
|
||||
QPushButton *resolveWithNoteButton;
|
||||
QPushButton *dismissButton;
|
||||
QPushButton *viewReplayButton;
|
||||
QPushButton *joinGameButton;
|
||||
QLabel *statusLabel;
|
||||
QTimer *refreshTimer;
|
||||
|
||||
QGroupBox *userContextGroup;
|
||||
QLabel *userContextName;
|
||||
QLabel *userContextAccountAge;
|
||||
QLabel *userContextReports;
|
||||
QLabel *userContextBans;
|
||||
QLabel *userContextWarns;
|
||||
QTextEdit *userContextNotes;
|
||||
QTextEdit *userContextRecentReports;
|
||||
QLabel *userContextUserLabel;
|
||||
QLabel *userContextAgeLabel;
|
||||
QLabel *userContextReportsLabel;
|
||||
QLabel *userContextBansLabel;
|
||||
QLabel *userContextWarnsLabel;
|
||||
QLabel *userContextNotesLabel;
|
||||
QLabel *userContextRecentReportsLabel;
|
||||
QString lastRequestedUser;
|
||||
QGroupBox *statsGroup;
|
||||
QLabel *statsTotalLabel;
|
||||
QLabel *statsTrendLabel;
|
||||
QLabel *statsCategoriesLabel;
|
||||
QTextEdit *statsDetailText;
|
||||
|
||||
QList<ServerInfo_Report> allReports;
|
||||
QList<ServerInfo_Report> filteredReports;
|
||||
int detailsRequestedReportId = -1;
|
||||
int detailsRequestSeq = 0;
|
||||
int selectedReportIdBeforeRefresh = -1;
|
||||
QString commentDraftBeforeRefresh;
|
||||
ServerInfo_Report previousSelectedReport;
|
||||
bool previousSelectedReportValid = false;
|
||||
};
|
||||
|
||||
#endif // TAB_REPORT_H
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../../intents/intent_join_server_game.h"
|
||||
#include "../interface/pixel_map_generator.h"
|
||||
#include "../interface/widgets/server/game_link.h"
|
||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||
|
|
@ -18,7 +19,9 @@
|
|||
#include "tab_home.h"
|
||||
#include "tab_logs.h"
|
||||
#include "tab_message.h"
|
||||
#include "tab_moderation.h"
|
||||
#include "tab_replays.h"
|
||||
#include "tab_report.h"
|
||||
#include "tab_room.h"
|
||||
#include "tab_server.h"
|
||||
#include "tab_visual_database_display.h"
|
||||
|
|
@ -31,6 +34,7 @@
|
|||
#include <QPainter>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||
#include <libcockatrice/network/client/remote/remote_client.h>
|
||||
#include <libcockatrice/protocol/pb/event_game_joined.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_notify_user.pb.h>
|
||||
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
|
||||
|
|
@ -39,6 +43,7 @@
|
|||
#include <libcockatrice/protocol/pb/room_event.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_room.pb.h>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
||||
#include <libcockatrice/protocol/pending_command.h>
|
||||
#include <libcockatrice/settings/chat_settings.h>
|
||||
#include <libcockatrice/settings/deck_editor_settings.h>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
|
@ -113,7 +118,7 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
|
|||
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
|
||||
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
|
||||
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
|
||||
tabLog(nullptr), isLocalGame(false)
|
||||
tabLog(nullptr), tabReport(nullptr), tabModeration(nullptr), isLocalGame(false)
|
||||
{
|
||||
setElideMode(Qt::ElideRight);
|
||||
setMovable(true);
|
||||
|
|
@ -190,6 +195,14 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *
|
|||
aTabLog->setCheckable(true);
|
||||
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
|
||||
|
||||
aTabReport = new QAction(this);
|
||||
aTabReport->setCheckable(true);
|
||||
connect(aTabReport, &QAction::triggered, this, &TabSupervisor::actTabReport);
|
||||
|
||||
aTabModeration = new QAction(this);
|
||||
aTabModeration->setCheckable(true);
|
||||
connect(aTabModeration, &QAction::triggered, this, &TabSupervisor::actTabModeration);
|
||||
|
||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||
&TabSupervisor::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
|
|
@ -229,6 +242,8 @@ void TabSupervisor::retranslateUi()
|
|||
aTabReplays->setText(tr("Game Replays"));
|
||||
aTabAdmin->setText(tr("Administration"));
|
||||
aTabLog->setText(tr("Logs"));
|
||||
aTabReport->setText(tr("Report Queue"));
|
||||
aTabModeration->setText(tr("Moderation"));
|
||||
|
||||
// tabs
|
||||
QList<Tab *> tabs;
|
||||
|
|
@ -238,6 +253,8 @@ void TabSupervisor::retranslateUi()
|
|||
tabs.append(tabAdmin);
|
||||
tabs.append(tabAccount);
|
||||
tabs.append(tabLog);
|
||||
tabs.append(tabReport);
|
||||
tabs.append(tabModeration);
|
||||
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
|
||||
while (roomIterator.hasNext()) {
|
||||
tabs.append(roomIterator.next().value());
|
||||
|
|
@ -284,6 +301,8 @@ void TabSupervisor::refreshShortcuts()
|
|||
aTabReplays->setShortcuts(shortcuts.getShortcut("Tabs/aTabReplays"));
|
||||
aTabAdmin->setShortcuts(shortcuts.getShortcut("Tabs/aTabAdmin"));
|
||||
aTabLog->setShortcuts(shortcuts.getShortcut("Tabs/aTabLog"));
|
||||
aTabReport->setShortcuts(shortcuts.getShortcut("Tabs/aTabReport"));
|
||||
aTabModeration->setShortcuts(shortcuts.getShortcut("Tabs/aTabModeration"));
|
||||
}
|
||||
|
||||
void TabSupervisor::closeEvent(QCloseEvent *event)
|
||||
|
|
@ -485,6 +504,8 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
tabsMenu->addAction(aTabAdmin);
|
||||
tabsMenu->addAction(aTabLog);
|
||||
tabsMenu->addAction(aTabCardArtRules);
|
||||
tabsMenu->addAction(aTabReport);
|
||||
tabsMenu->addAction(aTabModeration);
|
||||
|
||||
if (SettingsCache::instance().tabs().getTabAdminOpen()) {
|
||||
openTabAdmin();
|
||||
|
|
@ -492,6 +513,12 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
|
|||
if (SettingsCache::instance().tabs().getTabLogOpen()) {
|
||||
openTabLog();
|
||||
}
|
||||
if (SettingsCache::instance().tabs().getTabReportOpen()) {
|
||||
openTabReport();
|
||||
}
|
||||
if (SettingsCache::instance().tabs().getTabModerationOpen()) {
|
||||
openTabModeration();
|
||||
}
|
||||
openTabCardArtRules();
|
||||
}
|
||||
|
||||
|
|
@ -505,6 +532,8 @@ void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
|
|||
tabAccount = nullptr;
|
||||
tabAdmin = nullptr;
|
||||
tabLog = nullptr;
|
||||
tabReport = nullptr;
|
||||
tabModeration = nullptr;
|
||||
isLocalGame = true;
|
||||
userInfo = new ServerInfo_User;
|
||||
localClients = _clients;
|
||||
|
|
@ -546,6 +575,12 @@ void TabSupervisor::stop()
|
|||
if (tabLog) {
|
||||
tabLog->close();
|
||||
}
|
||||
if (tabReport) {
|
||||
tabReport->close();
|
||||
}
|
||||
if (tabModeration) {
|
||||
tabModeration->close();
|
||||
}
|
||||
}
|
||||
|
||||
QList<Tab *> tabsToDelete;
|
||||
|
|
@ -783,6 +818,59 @@ void TabSupervisor::openTabLog()
|
|||
aTabLog->setChecked(true);
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabReport(bool checked)
|
||||
{
|
||||
SettingsCache::instance().tabs().setTabReportOpen(checked);
|
||||
if (checked && !tabReport) {
|
||||
openTabReport();
|
||||
setCurrentWidget(tabReport);
|
||||
} else if (!checked && tabReport) {
|
||||
tabReport->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::openTabReport()
|
||||
{
|
||||
tabReport = new TabReport(this, client);
|
||||
myAddTab(tabReport, aTabReport);
|
||||
connect(tabReport, &TabReport::openReplay, this, &TabSupervisor::openReplay);
|
||||
connect(tabReport, &TabReport::requestJoinGame, this, &TabSupervisor::joinReportGame);
|
||||
connect(tabReport, &QObject::destroyed, this, [this] {
|
||||
tabReport = nullptr;
|
||||
aTabReport->setChecked(false);
|
||||
});
|
||||
aTabReport->setChecked(true);
|
||||
}
|
||||
|
||||
void TabSupervisor::actTabModeration(bool checked)
|
||||
{
|
||||
SettingsCache::instance().tabs().setTabModerationOpen(checked);
|
||||
if (checked && !tabModeration) {
|
||||
openTabModeration();
|
||||
setCurrentWidget(tabModeration);
|
||||
} else if (!checked && tabModeration) {
|
||||
tabModeration->closeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
void TabSupervisor::openTabModeration(const QString &userName)
|
||||
{
|
||||
if (tabModeration) {
|
||||
setCurrentWidget(tabModeration);
|
||||
if (!userName.isEmpty()) {
|
||||
tabModeration->investigate(userName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
tabModeration = new TabModeration(this, client, userName);
|
||||
myAddTab(tabModeration, aTabModeration);
|
||||
connect(tabModeration, &QObject::destroyed, this, [this] {
|
||||
tabModeration = nullptr;
|
||||
aTabModeration->setChecked(false);
|
||||
});
|
||||
aTabModeration->setChecked(true);
|
||||
}
|
||||
|
||||
void TabSupervisor::updatePingTime(int value, int max)
|
||||
{
|
||||
if (!tabServer) {
|
||||
|
|
@ -900,6 +988,30 @@ void TabSupervisor::replayLeft(TabGame *tab)
|
|||
replayTabs.removeOne(tab);
|
||||
}
|
||||
|
||||
void TabSupervisor::joinReportGame(const int gameId, const int roomId)
|
||||
{
|
||||
auto *remoteClient = qobject_cast<RemoteClient *>(client);
|
||||
if (!remoteClient) {
|
||||
actShowPopup(tr("Report joins are only available on a remote server."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto ctx = std::make_unique<ContextJoinGame>();
|
||||
ctx->roomContext.serverContext.hostname = remoteClient->peerName();
|
||||
ctx->roomContext.serverContext.port = QString::number(remoteClient->peerPort());
|
||||
ctx->roomContext.roomId = roomId;
|
||||
ctx->gameId = gameId;
|
||||
ctx->asSpectator = true;
|
||||
|
||||
auto *joinGameIntent = new IntentJoinServerGame(this, remoteClient, std::move(ctx));
|
||||
joinGameIntent->setParent(this);
|
||||
connect(joinGameIntent, &Intent::failed, this, [gameId](const QString &reason) {
|
||||
actShowPopup(tr("Could not join game %1.\n%2").arg(gameId).arg(reason));
|
||||
});
|
||||
|
||||
joinGameIntent->execute();
|
||||
}
|
||||
|
||||
TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus)
|
||||
{
|
||||
if (receiverName == QString::fromStdString(userInfo->name())) {
|
||||
|
|
@ -1284,6 +1396,24 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
|
|||
}
|
||||
break;
|
||||
}
|
||||
case Event_NotifyUser::REPORT_RESOLVED: {
|
||||
QString title = QString::fromStdString(event.custom_title()).simplified();
|
||||
QString content = QString::fromStdString(event.custom_content()).trimmed();
|
||||
if (!title.isEmpty() && !content.isEmpty()) {
|
||||
actShowPopup(title + "\n" + content);
|
||||
QApplication::alert(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Event_NotifyUser::REPORT_COMMENT: {
|
||||
QString title = QString::fromStdString(event.custom_title()).simplified();
|
||||
QString content = QString::fromStdString(event.custom_content()).trimmed();
|
||||
if (!title.isEmpty() && !content.isEmpty()) {
|
||||
actShowPopup(title + "\n" + content);
|
||||
QApplication::alert(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class TabDeckStorage;
|
|||
class TabReplays;
|
||||
class TabAdmin;
|
||||
class TabMessage;
|
||||
class TabReport;
|
||||
class TabModeration;
|
||||
class TabAccount;
|
||||
class TabDeckEditor;
|
||||
class TabLog;
|
||||
|
|
@ -103,6 +105,8 @@ private:
|
|||
TabAdmin *tabAdmin;
|
||||
TabCardArtRules *tabCardArtRules;
|
||||
TabLog *tabLog;
|
||||
TabReport *tabReport;
|
||||
TabModeration *tabModeration;
|
||||
QMap<int, TabRoom *> roomTabs;
|
||||
QMap<int, TabGame *> gameTabs;
|
||||
QList<TabGame *> replayTabs;
|
||||
|
|
@ -112,7 +116,7 @@ private:
|
|||
|
||||
QAction *aTabHome, *aTabDeckEditor, *aTabVisualDeckEditor, *aTabEdhRec, *aTabArchidekt, *aTabVisualDeckStorage,
|
||||
*aTabVisualDatabaseDisplay, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays, *aTabAdmin,
|
||||
*aTabCardArtRules, *aTabLog;
|
||||
*aTabCardArtRules, *aTabLog, *aTabReport, *aTabModeration;
|
||||
|
||||
int myAddTab(Tab *tab, QAction *manager = nullptr);
|
||||
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
|
||||
|
|
@ -183,6 +187,8 @@ public slots:
|
|||
TabArchidekt *addArchidektTab();
|
||||
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
|
||||
void openReplay(GameReplay *replay);
|
||||
void joinReportGame(int gameId, int roomId);
|
||||
void openTabModeration(const QString &userName = {});
|
||||
void switchToFirstAvailableNetworkTab();
|
||||
void maximizeMainWindow();
|
||||
void actTabVisualDeckStorage(bool checked);
|
||||
|
|
@ -198,6 +204,8 @@ private slots:
|
|||
void actTabDeckStorage(bool checked);
|
||||
void actTabAdmin(bool checked);
|
||||
void actTabLog(bool checked);
|
||||
void actTabReport(bool checked);
|
||||
void actTabModeration(bool checked);
|
||||
|
||||
void openTabVisualDeckStorage();
|
||||
void openTabHome();
|
||||
|
|
@ -208,6 +216,7 @@ private slots:
|
|||
void actTabCardArtRules(bool checked);
|
||||
void openTabCardArtRules();
|
||||
void openTabLog();
|
||||
void openTabReport();
|
||||
|
||||
void updateCurrent(int index);
|
||||
void updatePingTime(int value, int max);
|
||||
|
|
|
|||
119
cockatrice/src/interface/widgets/utility/report_utils.cpp
Normal file
119
cockatrice/src/interface/widgets/utility/report_utils.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
#include "report_utils.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBrush>
|
||||
#include <QDateTime>
|
||||
#include <QPalette>
|
||||
#include <QTableWidget>
|
||||
#include <QTextCursor>
|
||||
#include <QTextEdit>
|
||||
|
||||
namespace report_utils
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
QColor reportStatusColor(const QString &status)
|
||||
{
|
||||
const QColor text = qApp->palette().color(QPalette::Text);
|
||||
const int luminance = (299 * text.red() + 587 * text.green() + 114 * text.blue()) / 1000;
|
||||
const bool dark = luminance < 128;
|
||||
|
||||
if (status == "open") {
|
||||
return dark ? QColor("#e06060") : QColor("#c0392b");
|
||||
}
|
||||
if (status == "assigned") {
|
||||
return dark ? QColor("#e0a030") : QColor("#d68910");
|
||||
}
|
||||
if (status == "resolved") {
|
||||
return dark ? QColor("#50c878") : QColor("#1e8449");
|
||||
}
|
||||
if (status == "dismissed") {
|
||||
return qApp->palette().color(QPalette::PlaceholderText);
|
||||
}
|
||||
return QColor();
|
||||
}
|
||||
|
||||
void setReportStatusItemColor(QTableWidgetItem *item, const QString &status)
|
||||
{
|
||||
const QColor color = reportStatusColor(status);
|
||||
if (color.isValid()) {
|
||||
item->setForeground(QBrush(color));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QString formatReportCategory(const QString &category)
|
||||
{
|
||||
QString result = category;
|
||||
result.replace('_', ' ');
|
||||
return result.left(1).toUpper() + result.mid(1);
|
||||
}
|
||||
|
||||
QString formatReportTime(qint64 secondsSinceEpoch)
|
||||
{
|
||||
const QDateTime dt = QDateTime::fromSecsSinceEpoch(secondsSinceEpoch);
|
||||
return dt.toString("yyyy-MM-dd hh:mm");
|
||||
}
|
||||
|
||||
void fillReportTableRow(QTableWidget *table,
|
||||
int row,
|
||||
const ServerInfo_Report &report,
|
||||
int idColumn,
|
||||
int timeColumn,
|
||||
int reportedColumn,
|
||||
int categoryColumn,
|
||||
int gameIdColumn,
|
||||
int statusColumn,
|
||||
int assignedColumn)
|
||||
{
|
||||
auto *idItem = new QTableWidgetItem(QString::number(report.report_id()));
|
||||
idItem->setData(Qt::UserRole, report.report_id());
|
||||
table->setItem(row, idColumn, idItem);
|
||||
|
||||
table->setItem(row, timeColumn, new QTableWidgetItem(formatReportTime(report.report_time())));
|
||||
table->setItem(row, reportedColumn, new QTableWidgetItem(QString::fromStdString(report.reported_user_name())));
|
||||
table->setItem(row, categoryColumn,
|
||||
new QTableWidgetItem(formatReportCategory(QString::fromStdString(report.category()))));
|
||||
table->setItem(row, gameIdColumn,
|
||||
new QTableWidgetItem(report.game_id() > 0 ? QString::number(report.game_id()) : QString()));
|
||||
|
||||
auto *statusItem = new QTableWidgetItem(QString::fromStdString(report.status()));
|
||||
setReportStatusItemColor(statusItem, QString::fromStdString(report.status()));
|
||||
table->setItem(row, statusColumn, statusItem);
|
||||
|
||||
table->setItem(row, assignedColumn, new QTableWidgetItem(QString::fromStdString(report.assigned_mod_name())));
|
||||
}
|
||||
|
||||
void renderReportDetails(QTextEdit *chatLogEdit,
|
||||
QTextEdit *commentsEdit,
|
||||
const ServerInfo_Report &report,
|
||||
const QString &commentsEmptyText,
|
||||
const QString &moderatorPrefix,
|
||||
const QString &nonModeratorPrefix)
|
||||
{
|
||||
if (report.has_chat_log() && !report.chat_log().empty()) {
|
||||
chatLogEdit->setPlainText(QString::fromStdString(report.chat_log()));
|
||||
} else {
|
||||
chatLogEdit->clear();
|
||||
}
|
||||
|
||||
commentsEdit->clear();
|
||||
|
||||
if (report.comments_size() == 0) {
|
||||
commentsEdit->setPlainText(commentsEmptyText);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < report.comments_size(); ++i) {
|
||||
const ServerInfo_ReportComment &c = report.comments(i);
|
||||
const QString author = QString::fromStdString(c.author_name());
|
||||
const QString text = QString::fromStdString(c.comment_text());
|
||||
const QString prefix = c.is_moderator() ? moderatorPrefix : nonModeratorPrefix;
|
||||
commentsEdit->moveCursor(QTextCursor::End);
|
||||
commentsEdit->insertPlainText(
|
||||
QString("[%1] %2 %3:\n%4\n\n").arg(formatReportTime(c.comment_time()), prefix, author, text));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace report_utils
|
||||
36
cockatrice/src/interface/widgets/utility/report_utils.h
Normal file
36
cockatrice/src/interface/widgets/utility/report_utils.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#ifndef REPORT_UTILS_H
|
||||
#define REPORT_UTILS_H
|
||||
|
||||
#include <QString>
|
||||
#include <libcockatrice/protocol/pb/serverinfo_report.pb.h>
|
||||
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
|
||||
namespace report_utils
|
||||
{
|
||||
|
||||
QString formatReportCategory(const QString &category);
|
||||
QString formatReportTime(qint64 secondsSinceEpoch);
|
||||
|
||||
void fillReportTableRow(QTableWidget *table,
|
||||
int row,
|
||||
const ServerInfo_Report &report,
|
||||
int idColumn,
|
||||
int timeColumn,
|
||||
int reportedColumn,
|
||||
int categoryColumn,
|
||||
int gameIdColumn,
|
||||
int statusColumn,
|
||||
int assignedColumn);
|
||||
|
||||
void renderReportDetails(QTextEdit *chatLogEdit,
|
||||
QTextEdit *commentsEdit,
|
||||
const ServerInfo_Report &report,
|
||||
const QString &commentsEmptyText,
|
||||
const QString &moderatorPrefix,
|
||||
const QString &nonModeratorPrefix);
|
||||
|
||||
} // namespace report_utils
|
||||
|
||||
#endif // REPORT_UTILS_H
|
||||
|
|
@ -11,6 +11,7 @@ set(HEADERS
|
|||
libcockatrice/deck_list/deck_list_history_manager.h
|
||||
libcockatrice/deck_list/deck_list_node_tree.h
|
||||
libcockatrice/deck_list/deck_list_memento.h
|
||||
libcockatrice/deck_list/playmat_resolver.h
|
||||
libcockatrice/deck_list/sideboard_plan.h
|
||||
)
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ add_library(
|
|||
libcockatrice/deck_list/deck_list.cpp
|
||||
libcockatrice/deck_list/deck_list_history_manager.cpp
|
||||
libcockatrice/deck_list/deck_list_node_tree.cpp
|
||||
libcockatrice/deck_list/playmat_resolver.cpp
|
||||
libcockatrice/deck_list/sideboard_plan.cpp
|
||||
)
|
||||
|
||||
|
|
@ -33,4 +35,7 @@ add_dependencies(libcockatrice_deck_list libcockatrice_protocol)
|
|||
|
||||
target_include_directories(libcockatrice_deck_list PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_deck_list PUBLIC libcockatrice_protocol libcockatrice_utility ${QT_CORE_MODULE})
|
||||
target_link_libraries(
|
||||
libcockatrice_deck_list PUBLIC libcockatrice_interfaces libcockatrice_protocol libcockatrice_utility
|
||||
${QT_CORE_MODULE}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ static const QString CURRENT_SIDEBOARD_PLAN_KEY = "";
|
|||
|
||||
bool DeckList::Metadata::isEmpty() const
|
||||
{
|
||||
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty();
|
||||
return name.isEmpty() && comments.isEmpty() && bannerCard.isEmpty() && tags.isEmpty() && playmat.card.isEmpty();
|
||||
}
|
||||
|
||||
DeckList::DeckList()
|
||||
|
|
@ -74,6 +74,36 @@ bool DeckList::readElement(QXmlStreamReader *xml)
|
|||
QString providerId = xml->attributes().value("providerId").toString();
|
||||
QString cardName = xml->readElementText();
|
||||
metadata.bannerCard = {cardName, providerId};
|
||||
} else if (childName == "playmatCard") {
|
||||
QString providerId = xml->attributes().value("providerId").toString();
|
||||
bool ok;
|
||||
QString marginLStr = xml->attributes().value("marginPctL").toString();
|
||||
QString marginRStr = xml->attributes().value("marginPctR").toString();
|
||||
QString vOffStr = xml->attributes().value("verticalOffset").toString();
|
||||
QString zoomStr = xml->attributes().value("zoom").toString();
|
||||
QString cardName = xml->readElementText();
|
||||
PlaymatInfo playmat;
|
||||
playmat.card = {cardName, providerId};
|
||||
// Clamp to the same ranges as the settings dialog and the remote
|
||||
// player-properties path so malformed deck files cannot produce
|
||||
// degenerate art rectangles (e.g. a zoom of 0 dividing by zero).
|
||||
playmat.params.marginPctL = qBound(0.0, marginLStr.toDouble(&ok), 0.95);
|
||||
if (!ok) {
|
||||
playmat.params.marginPctL = 0.07;
|
||||
}
|
||||
playmat.params.marginPctR = qBound(0.0, marginRStr.toDouble(&ok), 0.95);
|
||||
if (!ok) {
|
||||
playmat.params.marginPctR = 0.07;
|
||||
}
|
||||
playmat.params.verticalOffset = qBound(0.0, vOffStr.toDouble(&ok), 1.0);
|
||||
if (!ok) {
|
||||
playmat.params.verticalOffset = 0.33;
|
||||
}
|
||||
playmat.params.zoom = qBound(0.1, zoomStr.toDouble(&ok), 4.0);
|
||||
if (!ok) {
|
||||
playmat.params.zoom = 1.0;
|
||||
}
|
||||
metadata.playmat = playmat;
|
||||
} else if (childName == "tags") {
|
||||
metadata.tags.clear(); // Clear existing tags
|
||||
while (xml->readNextStartElement()) {
|
||||
|
|
@ -104,6 +134,16 @@ static void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metad
|
|||
xml->writeAttribute("providerId", metadata.bannerCard.providerId);
|
||||
xml->writeCharacters(metadata.bannerCard.name);
|
||||
xml->writeEndElement();
|
||||
if (!metadata.playmat.card.isEmpty()) {
|
||||
xml->writeStartElement("playmatCard");
|
||||
xml->writeAttribute("providerId", metadata.playmat.card.providerId);
|
||||
xml->writeAttribute("marginPctL", QString::number(metadata.playmat.params.marginPctL, 'f', 4));
|
||||
xml->writeAttribute("marginPctR", QString::number(metadata.playmat.params.marginPctR, 'f', 4));
|
||||
xml->writeAttribute("verticalOffset", QString::number(metadata.playmat.params.verticalOffset, 'f', 4));
|
||||
xml->writeAttribute("zoom", QString::number(metadata.playmat.params.zoom, 'f', 4));
|
||||
xml->writeCharacters(metadata.playmat.card.name);
|
||||
xml->writeEndElement();
|
||||
}
|
||||
xml->writeTextElement("comments", metadata.comments);
|
||||
|
||||
// Write tags
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
#include <QMap>
|
||||
#include <QVector>
|
||||
#include <QtCore/QXmlStreamReader>
|
||||
#include <libcockatrice/utility/card_ref.h>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
class AbstractDecklistNode;
|
||||
class DecklistCardNode;
|
||||
|
|
@ -70,6 +70,7 @@ public:
|
|||
CardRef bannerCard; ///< Optional representative card for the deck.
|
||||
QStringList tags; ///< User-defined tags for deck classification.
|
||||
QString lastLoadedTimestamp; ///< Timestamp string of last load.
|
||||
PlaymatInfo playmat; ///< Optional playmat background for table+stack zones.
|
||||
|
||||
/**
|
||||
* @brief Checks if all values (except for lastLoadedTimestamp) in the metadata is empty.
|
||||
|
|
@ -115,6 +116,10 @@ public:
|
|||
{
|
||||
metadata.bannerCard = _bannerCard;
|
||||
}
|
||||
void setPlaymat(const PlaymatInfo &_playmat = {})
|
||||
{
|
||||
metadata.playmat = _playmat;
|
||||
}
|
||||
void setLastLoadedTimestamp(const QString &_lastLoadedTimestamp = QString())
|
||||
{
|
||||
metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
|
||||
|
|
@ -170,6 +175,10 @@ public:
|
|||
{
|
||||
return metadata.bannerCard;
|
||||
}
|
||||
PlaymatInfo getPlaymat() const
|
||||
{
|
||||
return metadata.playmat;
|
||||
}
|
||||
QString getLastLoadedTimestamp() const
|
||||
{
|
||||
return metadata.lastLoadedTimestamp;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
#include "playmat_resolver.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
|
||||
PlaymatInfo resolveEffectivePlaymat(const DeckList &deck,
|
||||
const PlaymatInfo &force,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatFallbackMode fallbackMode,
|
||||
int rotationIndex)
|
||||
{
|
||||
if (!force.card.isEmpty()) {
|
||||
return force;
|
||||
}
|
||||
|
||||
const PlaymatInfo &deckPlaymat = deck.getPlaymat();
|
||||
if (!deckPlaymat.card.isEmpty()) {
|
||||
return deckPlaymat;
|
||||
}
|
||||
|
||||
if (fallbackList.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (fallbackMode) {
|
||||
case PlaymatFallbackModeFixed:
|
||||
return fallbackList.first();
|
||||
case PlaymatFallbackModeRoundRobin:
|
||||
return fallbackList.at(rotationIndex % fallbackList.size());
|
||||
case PlaymatFallbackModeRandom:
|
||||
return fallbackList.at(QRandomGenerator::global()->bounded(fallbackList.size()));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
PlaymatInfo resolvePlaymatForDeck(const DeckList &deck,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatMode mode,
|
||||
PlaymatFallbackMode fallbackBehavior,
|
||||
int rotationIndex)
|
||||
{
|
||||
switch (mode) {
|
||||
case PlaymatModeOverrideDeck: {
|
||||
const DeckList emptyDeck;
|
||||
return resolveEffectivePlaymat(emptyDeck, {}, fallbackList, fallbackBehavior, rotationIndex);
|
||||
}
|
||||
case PlaymatModeFallback:
|
||||
return resolveEffectivePlaymat(deck, {}, fallbackList, fallbackBehavior, rotationIndex);
|
||||
case PlaymatModeDeckOnly:
|
||||
return deck.getPlaymat();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#ifndef COCKATRICE_PLAYMAT_RESOLVER_H
|
||||
#define COCKATRICE_PLAYMAT_RESOLVER_H
|
||||
|
||||
#include "deck_list.h"
|
||||
|
||||
#include <libcockatrice/interfaces/interface_interface_settings_provider.h>
|
||||
|
||||
/**
|
||||
* @brief Resolves the effective playmat for a deck per the resolution chain:
|
||||
* force override > deck-configured playmat > fallback list > none.
|
||||
*
|
||||
* @param deck The deck to resolve a playmat for.
|
||||
* @param force An optional user-level override; wins over everything. Pass an
|
||||
* empty @ref PlaymatInfo::card to skip it.
|
||||
* @param fallbackList User-level fallback playmats, consulted only when the
|
||||
* deck has no configured playmat.
|
||||
* @param fallbackMode How @p fallbackList is consulted (ignored when empty).
|
||||
* @param rotationIndex In/out cursor for @c PlaymatFallbackModeRoundRobin;
|
||||
* advanced once per call. Unused for the other modes.
|
||||
* @return The effective playmat; an empty @ref PlaymatInfo::card when
|
||||
* nothing in the chain resolves.
|
||||
*/
|
||||
PlaymatInfo resolveEffectivePlaymat(const DeckList &deck,
|
||||
const PlaymatInfo &force,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatFallbackMode fallbackMode,
|
||||
int rotationIndex);
|
||||
|
||||
/**
|
||||
* @brief Resolves the playmat to display for a deck according to the user's
|
||||
* collection mode (@ref PlaymatMode), combining the deck with the
|
||||
* given fallback list.
|
||||
*
|
||||
* @param deck The deck to resolve a playmat for.
|
||||
* @param fallbackList User-level fallback playmats.
|
||||
* @param mode How the collection interacts with the deck-configured playmat.
|
||||
* @param fallbackBehavior How @p fallbackList is picked from.
|
||||
* @param rotationIndex Cursor for @c PlaymatFallbackModeRoundRobin.
|
||||
* @return The effective playmat; an empty @ref PlaymatInfo::card when nothing resolves.
|
||||
*/
|
||||
PlaymatInfo resolvePlaymatForDeck(const DeckList &deck,
|
||||
const QList<PlaymatInfo> &fallbackList,
|
||||
PlaymatMode mode,
|
||||
PlaymatFallbackMode fallbackBehavior,
|
||||
int rotationIndex);
|
||||
|
||||
#endif // COCKATRICE_PLAYMAT_RESOLVER_H
|
||||
|
|
@ -30,4 +30,4 @@ add_library(libcockatrice_interfaces STATIC ${MOC_SOURCES})
|
|||
|
||||
target_include_directories(libcockatrice_interfaces PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_link_libraries(libcockatrice_interfaces PUBLIC ${QT_CORE_MODULE})
|
||||
target_link_libraries(libcockatrice_interfaces PUBLIC libcockatrice_utility ${QT_CORE_MODULE})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,40 @@
|
|||
#ifndef COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
#define COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <libcockatrice/utility/playmat_params.h>
|
||||
|
||||
/**
|
||||
* @brief Whether playmats are rendered in-game, and for whom.
|
||||
*/
|
||||
enum PlaymatVisibility
|
||||
{
|
||||
PlaymatVisibilityNone = 0, ///< Don't use playmats.
|
||||
PlaymatVisibilityOwnOnly = 1, ///< Show the local player's playmat only.
|
||||
PlaymatVisibilityAll = 2 ///< Show playmats for all players.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief How the user-level playmat collection interacts with the deck-configured playmat.
|
||||
*/
|
||||
enum PlaymatMode
|
||||
{
|
||||
PlaymatModeOverrideDeck = 0, ///< Always use the collection, ignoring any deck-configured playmat.
|
||||
PlaymatModeFallback = 1, ///< Prefer the deck-configured playmat; fall back to the collection when absent.
|
||||
PlaymatModeDeckOnly = 2 ///< Use only the deck-configured playmat, ignoring the collection.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief How the user-level fallback playmat list is consulted when a deck has no playmat configured.
|
||||
*/
|
||||
enum PlaymatFallbackMode
|
||||
{
|
||||
PlaymatFallbackModeFixed = 0, ///< Always use the first entry of the fallback list.
|
||||
PlaymatFallbackModeRoundRobin = 1, ///< Cycle through the list, advancing one entry per resolution.
|
||||
PlaymatFallbackModeRandom = 2 ///< Pick a random entry per resolution.
|
||||
};
|
||||
|
||||
class IInterfaceSettingsProvider
|
||||
{
|
||||
|
|
@ -43,6 +75,21 @@ public:
|
|||
[[nodiscard]] virtual bool getLifeCounterAnimationsEnabled() const = 0;
|
||||
[[nodiscard]] virtual bool getBattlefieldFlashEnabled() const = 0;
|
||||
[[nodiscard]] virtual QStringList getUserListExpandedSections() const = 0;
|
||||
|
||||
/** @brief Who gets playmats rendered: @ref PlaymatVisibility. */
|
||||
[[nodiscard]] virtual int getPlaymatVisibility() const = 0;
|
||||
|
||||
/** @brief User-level playmat collection. Used either as a forced playmat
|
||||
* (mode == @ref PlaymatModeOverrideDeck) or as a fallback when a deck has none
|
||||
* (mode == @ref PlaymatModeFallback). */
|
||||
[[nodiscard]] virtual QList<PlaymatInfo> getPlaymatFallbackList() const = 0;
|
||||
|
||||
/** @brief How the fallback list is applied: @ref PlaymatMode. */
|
||||
[[nodiscard]] virtual int getPlaymatMode() const = 0;
|
||||
|
||||
/** @brief How the fallback list is picked from when mode is @ref PlaymatModeFallback:
|
||||
* @ref PlaymatFallbackMode. */
|
||||
[[nodiscard]] virtual int getPlaymatFallbackBehavior() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_INTERFACE_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public:
|
|||
[[nodiscard]] virtual bool getTabReplaysOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabAdminOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabLogOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabReportOpen() const = 0;
|
||||
[[nodiscard]] virtual bool getTabModerationOpen() const = 0;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_card_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
|
|
@ -144,6 +145,13 @@ Response::ResponseCode Server_AbstractParticipant::cmdSetSideboardLock(const Com
|
|||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdSetPlaymat(const Command_SetPlaymat & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
{
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
Response::ResponseCode Server_AbstractParticipant::cmdConcede(const Command_Concede & /*cmd*/,
|
||||
ResponseContainer & /*rc*/,
|
||||
GameEventStorage & /*ges*/)
|
||||
|
|
@ -525,6 +533,9 @@ Server_AbstractParticipant::processGameCommand(const GameCommand &command, Respo
|
|||
case GameCommand::REVERSE_TURN:
|
||||
return cmdReverseTurn(command.GetExtension(Command_ReverseTurn::ext), rc, ges);
|
||||
break;
|
||||
case GameCommand::SET_PLAYMAT:
|
||||
return cmdSetPlaymat(command.GetExtension(Command_SetPlaymat::ext), rc, ges);
|
||||
break;
|
||||
default:
|
||||
return Response::RespInvalidCommand;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class Command_SetSideboardPlan;
|
|||
class Command_DeckSelect;
|
||||
class Command_SetSideboardLock;
|
||||
class Command_ChangeZoneProperties;
|
||||
class Command_SetPlaymat;
|
||||
|
||||
class Server_AbstractParticipant : public Server_ArrowTarget, public ServerInfo_User_Container
|
||||
{
|
||||
|
|
@ -124,6 +125,8 @@ public:
|
|||
cmdSetSideboardPlan(const Command_SetSideboardPlan &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdSetSideboardLock(const Command_SetSideboardLock &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode cmdGameSay(const Command_GameSay &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode cmdShuffle(const Command_Shuffle &cmd, ResponseContainer &rc, GameEventStorage &ges);
|
||||
virtual Response::ResponseCode
|
||||
|
|
|
|||
|
|
@ -1653,5 +1653,13 @@ void Server_AbstractPlayer::getPlayerProperties(ServerInfo_PlayerProperties &res
|
|||
result.set_ready_start(readyStart);
|
||||
if (deck) {
|
||||
result.set_deck_hash(deck->getDeckHash().toStdString());
|
||||
const auto &playmat = deck->getPlaymat();
|
||||
auto *playmatParams = result.mutable_playmat_params();
|
||||
playmatParams->set_card_name(playmat.card.name.toStdString());
|
||||
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
|
||||
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
|
||||
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
|
||||
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
|
||||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include <libcockatrice/protocol/pb/command_mulligan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
|
||||
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
|
||||
|
|
@ -250,6 +251,14 @@ Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &r
|
|||
Event_PlayerPropertiesChanged event;
|
||||
event.mutable_player_properties()->set_sideboard_locked(true);
|
||||
event.mutable_player_properties()->set_deck_hash(deck->getDeckHash().toStdString());
|
||||
const auto &playmat = deck->getPlaymat();
|
||||
auto *playmatParams = event.mutable_player_properties()->mutable_playmat_params();
|
||||
playmatParams->set_card_name(playmat.card.name.left(MAX_NAME_LENGTH).toStdString());
|
||||
playmatParams->set_card_provider_id(playmat.card.providerId.left(MAX_NAME_LENGTH).toStdString());
|
||||
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
|
||||
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
|
||||
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
|
||||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
|
||||
Context_DeckSelect context;
|
||||
|
|
@ -594,6 +603,46 @@ Server_Player::cmdReverseTurn(const Command_ReverseTurn &cmd, ResponseContainer
|
|||
return Server_AbstractParticipant::cmdReverseTurn(cmd, rc, ges);
|
||||
}
|
||||
|
||||
Response::ResponseCode
|
||||
Server_Player::cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges)
|
||||
{
|
||||
Q_UNUSED(rc);
|
||||
|
||||
if (!deck) {
|
||||
return Response::RespContextError;
|
||||
}
|
||||
|
||||
const auto &pp = cmd.playmat_params();
|
||||
const auto rawName = QString::fromStdString(pp.card_name());
|
||||
const auto rawProviderId = QString::fromStdString(pp.card_provider_id());
|
||||
if (rawName.length() > MAX_NAME_LENGTH || rawProviderId.length() > MAX_NAME_LENGTH) {
|
||||
return Response::RespInvalidData;
|
||||
}
|
||||
PlaymatInfo playmat;
|
||||
playmat.card.name = rawName;
|
||||
playmat.card.providerId = rawProviderId;
|
||||
playmat.params.marginPctL = qBound(0.0, pp.margin_pct_l(), 0.95);
|
||||
playmat.params.marginPctR = qBound(0.0, pp.margin_pct_r(), 0.95);
|
||||
playmat.params.verticalOffset = qBound(0.0, pp.vertical_offset(), 1.0);
|
||||
playmat.params.zoom = qBound(0.1, pp.zoom(), 4.0);
|
||||
deck->setPlaymat(playmat);
|
||||
|
||||
Event_PlayerPropertiesChanged event;
|
||||
auto *props = event.mutable_player_properties();
|
||||
props->set_sideboard_locked(sideboardLocked);
|
||||
props->set_deck_hash(deck->getDeckHash().toStdString());
|
||||
auto *playmatParams = props->mutable_playmat_params();
|
||||
playmatParams->set_card_name(playmat.card.name.toStdString());
|
||||
playmatParams->set_card_provider_id(playmat.card.providerId.toStdString());
|
||||
playmatParams->set_margin_pct_l(playmat.params.marginPctL);
|
||||
playmatParams->set_margin_pct_r(playmat.params.marginPctR);
|
||||
playmatParams->set_vertical_offset(playmat.params.verticalOffset);
|
||||
playmatParams->set_zoom(playmat.params.zoom);
|
||||
ges.enqueueGameEvent(event, playerId);
|
||||
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
||||
void Server_Player::getInfo(ServerInfo_Player *info,
|
||||
Server_AbstractParticipant *recipient,
|
||||
bool omniscient,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ public:
|
|||
cmdSetActivePhase(const Command_SetActivePhase &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
Response::ResponseCode
|
||||
cmdReverseTurn(const Command_ReverseTurn & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) override;
|
||||
Response::ResponseCode
|
||||
cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &rc, GameEventStorage &ges) override;
|
||||
Response::ResponseCode cmdChangeZoneProperties(const Command_ChangeZoneProperties &cmd,
|
||||
ResponseContainer &rc,
|
||||
GameEventStorage &ges) override;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ enum AuthenticationResult
|
|||
UsernameInvalid,
|
||||
RegistrationRequired,
|
||||
UserIsInactive,
|
||||
ClientIdRequired
|
||||
ClientIdRequired,
|
||||
PasswordChangeRequired
|
||||
};
|
||||
|
||||
class Server : public QObject
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@ void Server_AbstractUserInterface::sendResponseContainer(const ResponseContainer
|
|||
}
|
||||
}
|
||||
|
||||
void Server_AbstractUserInterface::onLogin(ResponseContainer &)
|
||||
{
|
||||
}
|
||||
|
||||
void Server_AbstractUserInterface::playerRemovedFromGame(Server_Game *game)
|
||||
{
|
||||
qDebug() << "Server_AbstractUserInterface::playerRemovedFromGame(): gameId =" << game->getGameId();
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public:
|
|||
void playerRemovedFromGame(Server_Game *game);
|
||||
void playerAddedToGame(int gameId, int roomId, int playerId);
|
||||
void joinPersistentGames(ResponseContainer &rc);
|
||||
virtual void onLogin(ResponseContainer &rc);
|
||||
|
||||
QMap<int, QPair<int, int>> getGames() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -172,6 +172,9 @@ public:
|
|||
{
|
||||
return false;
|
||||
}
|
||||
virtual void setForcePasswordChange(const QString & /* user */, bool /* force */)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -564,6 +564,8 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
|||
return Response::RespClientIdRequired;
|
||||
case UserIsInactive:
|
||||
return Response::RespAccountNotActivated;
|
||||
case PasswordChangeRequired:
|
||||
return Response::RespPasswordChangeRequired;
|
||||
default:
|
||||
authState = res;
|
||||
usingRealPassword = needsHash;
|
||||
|
|
@ -614,6 +616,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
|
|||
|
||||
joinPersistentGames(rc);
|
||||
databaseInterface->removeForgotPassword(userName);
|
||||
onLogin(rc);
|
||||
rc.setResponseExtension(re);
|
||||
return Response::RespOk;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,20 @@ set(PROTO_FILES
|
|||
command_ready_start.proto
|
||||
command_replay_delete_match.proto
|
||||
command_replay_download.proto
|
||||
command_replay_download_by_game_id.proto
|
||||
command_replay_get_code.proto
|
||||
command_replay_list.proto
|
||||
command_replay_modify_match.proto
|
||||
command_replay_submit_code.proto
|
||||
command_report.proto
|
||||
command_report_add_comment.proto
|
||||
command_report_assign.proto
|
||||
command_report_details.proto
|
||||
command_report_list.proto
|
||||
command_report_my_list.proto
|
||||
command_report_resolve.proto
|
||||
command_report_stats.proto
|
||||
command_report_user_info.proto
|
||||
command_reveal_cards.proto
|
||||
command_reverse_turn.proto
|
||||
command_roll_die.proto
|
||||
|
|
@ -46,6 +56,7 @@ set(PROTO_FILES
|
|||
command_set_card_attr.proto
|
||||
command_set_card_counter.proto
|
||||
command_set_counter.proto
|
||||
command_set_playmat.proto
|
||||
command_set_sideboard_lock.proto
|
||||
command_set_sideboard_plan.proto
|
||||
command_shuffle.proto
|
||||
|
|
@ -137,8 +148,19 @@ set(PROTO_FILES
|
|||
response_password_salt.proto
|
||||
response_register.proto
|
||||
response_replay_download.proto
|
||||
response_replay_download_by_game_id.proto
|
||||
response_replay_get_code.proto
|
||||
response_replay_list.proto
|
||||
response_report_details.proto
|
||||
response_report_list.proto
|
||||
response_report_my_list.proto
|
||||
response_report_stats.proto
|
||||
response_report_user_info.proto
|
||||
response_moderator_last_logins.proto
|
||||
response_remove_user_avatar.proto
|
||||
response_reset_user_password.proto
|
||||
response_user_alts.proto
|
||||
response_user_sessions.proto
|
||||
response_viewlog_history.proto
|
||||
response_warn_history.proto
|
||||
response_warn_list.proto
|
||||
|
|
@ -154,13 +176,17 @@ set(PROTO_FILES
|
|||
serverinfo_deckstorage.proto
|
||||
serverinfo_game.proto
|
||||
serverinfo_gametype.proto
|
||||
serverinfo_moderator_login.proto
|
||||
serverinfo_player.proto
|
||||
serverinfo_playerping.proto
|
||||
serverinfo_playerproperties.proto
|
||||
serverinfo_replay.proto
|
||||
serverinfo_replay_match.proto
|
||||
serverinfo_report.proto
|
||||
serverinfo_room.proto
|
||||
serverinfo_user.proto
|
||||
serverinfo_user_alt.proto
|
||||
serverinfo_user_session.proto
|
||||
serverinfo_warning.proto
|
||||
serverinfo_zone.proto
|
||||
session_commands.proto
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ message AdminCommand {
|
|||
SHUTDOWN_SERVER = 1001;
|
||||
RELOAD_CONFIG = 1002;
|
||||
ADJUST_MOD = 1003;
|
||||
RESET_USER_PASSWORD = 1016;
|
||||
}
|
||||
extensions 100 to max;
|
||||
}
|
||||
|
|
@ -37,3 +38,10 @@ message Command_AdjustMod {
|
|||
optional bool should_be_mod = 2;
|
||||
optional bool should_be_judge = 3;
|
||||
}
|
||||
|
||||
message Command_ResetUserPassword {
|
||||
extend AdminCommand {
|
||||
optional Command_ResetUserPassword ext = 1016;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReplayDownloadByGameId {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReplayDownloadByGameId ext = 1203;
|
||||
}
|
||||
required sint32 game_id = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
syntax = "proto2";
|
||||
import "session_commands.proto";
|
||||
|
||||
message Command_Report {
|
||||
extend SessionCommand {
|
||||
optional Command_Report ext = 1200;
|
||||
}
|
||||
optional string reported_user = 1;
|
||||
optional int32 game_id = 2;
|
||||
optional string category = 3;
|
||||
optional string description = 4;
|
||||
optional string chat_log = 5;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
import "session_commands.proto";
|
||||
|
||||
message Command_ReportAddComment {
|
||||
extend SessionCommand {
|
||||
optional Command_ReportAddComment ext = 1205;
|
||||
}
|
||||
required int32 report_id = 1;
|
||||
required string comment = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReportAssign {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReportAssign ext = 1201;
|
||||
}
|
||||
required int32 report_id = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "session_commands.proto";
|
||||
|
||||
message Command_ReportDetails {
|
||||
extend SessionCommand {
|
||||
optional Command_ReportDetails ext = 1206;
|
||||
}
|
||||
required int32 report_id = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReportList {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReportList ext = 1200;
|
||||
}
|
||||
optional bool unresolved_only = 1;
|
||||
optional uint32 offset = 2 [default = 0];
|
||||
optional uint32 limit = 3 [default = 100];
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
syntax = "proto2";
|
||||
import "session_commands.proto";
|
||||
|
||||
message Command_ReportMyList {
|
||||
extend SessionCommand {
|
||||
optional Command_ReportMyList ext = 1204;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReportResolve {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReportResolve ext = 1202;
|
||||
}
|
||||
required int32 report_id = 1;
|
||||
optional string resolution_note = 2;
|
||||
optional bool dismissed = 3;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReportStats {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReportStats ext = 1205;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "moderator_commands.proto";
|
||||
|
||||
message Command_ReportUserInfo {
|
||||
extend ModeratorCommand {
|
||||
optional Command_ReportUserInfo ext = 1204;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "game_commands.proto";
|
||||
import "serverinfo_playerproperties.proto";
|
||||
message Command_SetPlaymat {
|
||||
extend GameCommand {
|
||||
optional Command_SetPlaymat ext = 1035;
|
||||
}
|
||||
optional ServerInfo_PlayerProperties.PlaymatParams playmat_params = 1;
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ message Event_NotifyUser {
|
|||
WARNING = 2;
|
||||
IDLEWARNING = 3;
|
||||
CUSTOM = 4;
|
||||
REPORT_RESOLVED = 5;
|
||||
REPORT_COMMENT = 6;
|
||||
}
|
||||
|
||||
extend SessionEvent {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,11 @@ message GameCommand {
|
|||
/// Server: Server_Player::cmdReverseTurn
|
||||
/// Client: reflected via subsequent turn events
|
||||
REVERSE_TURN = 1034;
|
||||
|
||||
/// Set the player's playmat independently of the deck.
|
||||
/// Server: Server_Player::cmdSetPlaymat
|
||||
/// Client: reflected via player properties changed event
|
||||
SET_PLAYMAT = 1035;
|
||||
}
|
||||
|
||||
extensions 100 to max;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@ message ModeratorCommand {
|
|||
ADD_CARD_ART_RULE = 1010;
|
||||
REMOVE_CARD_ART_RULE = 1011;
|
||||
LIST_CARD_ART_RULES = 1012;
|
||||
GET_USER_SESSIONS = 1013;
|
||||
GET_USER_ALTS = 1014;
|
||||
GET_MODERATOR_LAST_LOGINS = 1015;
|
||||
RESET_USER_PASSWORD = 1016;
|
||||
REMOVE_USER_AVATAR = 1017;
|
||||
REPORT_LIST = 1200;
|
||||
REPORT_ASSIGN = 1201;
|
||||
REPORT_RESOLVE = 1202;
|
||||
REPLAY_DOWNLOAD_BY_GAME_ID = 1203;
|
||||
REPORT_USER_INFO = 1204;
|
||||
REPORT_STATS = 1205;
|
||||
}
|
||||
extensions 100 to max;
|
||||
}
|
||||
|
|
@ -135,3 +146,31 @@ message Command_ListCardArtRules {
|
|||
optional Command_ListCardArtRules ext = 1012;
|
||||
}
|
||||
}
|
||||
|
||||
message Command_GetUserSessions {
|
||||
extend ModeratorCommand {
|
||||
optional Command_GetUserSessions ext = 1013;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
optional uint32 limit = 2 [default = 110];
|
||||
}
|
||||
|
||||
message Command_GetUserAlts {
|
||||
extend ModeratorCommand {
|
||||
optional Command_GetUserAlts ext = 1014;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
}
|
||||
|
||||
message Command_GetModeratorLastLogins {
|
||||
extend ModeratorCommand {
|
||||
optional Command_GetModeratorLastLogins ext = 1015;
|
||||
}
|
||||
}
|
||||
|
||||
message Command_RemoveUserAvatar {
|
||||
extend ModeratorCommand {
|
||||
optional Command_RemoveUserAvatar ext = 1017;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ message Response {
|
|||
RespClientUpdateRequired = 35; // Client is missing features that the server is requiring
|
||||
RespServerFull = 36; // Server user limit reached
|
||||
RespEmailBlackListed = 37; // Server has blocked the email address provided for registration for some reason
|
||||
RespPasswordChangeRequired = 38; // Server requires the user to change their password before proceeding
|
||||
}
|
||||
|
||||
// Type of response, used to route handling on the client
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
import "serverinfo_moderator_login.proto";
|
||||
|
||||
message Response_ModeratorLastLogins {
|
||||
extend Response {
|
||||
optional Response_ModeratorLastLogins ext = 1217;
|
||||
}
|
||||
repeated ServerInfo_ModeratorLogin logins = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
|
||||
message Response_RemoveUserAvatar {
|
||||
extend Response {
|
||||
optional Response_RemoveUserAvatar ext = 1219;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
|
||||
message Response_ReplayDownloadByGameId {
|
||||
extend Response {
|
||||
optional Response_ReplayDownloadByGameId ext = 1203;
|
||||
}
|
||||
optional bytes replay_data = 1;
|
||||
optional sint32 replay_id = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
import "serverinfo_report.proto";
|
||||
|
||||
message Response_ReportDetails {
|
||||
extend Response {
|
||||
optional Response_ReportDetails ext = 1214;
|
||||
}
|
||||
optional ServerInfo_Report report = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
import "serverinfo_report.proto";
|
||||
|
||||
message Response_ReportList {
|
||||
extend Response {
|
||||
optional Response_ReportList ext = 1210;
|
||||
}
|
||||
repeated ServerInfo_Report reports = 1;
|
||||
optional uint32 total_count = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
import "serverinfo_report.proto";
|
||||
|
||||
message Response_ReportMyList {
|
||||
extend Response {
|
||||
optional Response_ReportMyList ext = 1213;
|
||||
}
|
||||
repeated ServerInfo_Report reports = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
|
||||
message Response_ReportStats {
|
||||
extend Response {
|
||||
optional Response_ReportStats ext = 1212;
|
||||
}
|
||||
|
||||
optional int32 total_reports = 1;
|
||||
optional int32 total_pending = 2;
|
||||
optional int32 total_assigned = 3;
|
||||
optional int32 total_resolved = 4;
|
||||
|
||||
optional int32 reports_last_24h = 5;
|
||||
optional int32 reports_last_7d = 6;
|
||||
optional int32 reports_last_30d = 7;
|
||||
|
||||
optional double avg_resolution_hours = 8;
|
||||
|
||||
optional int32 reports_this_week = 9;
|
||||
optional int32 reports_last_week = 10;
|
||||
|
||||
repeated ReportCategoryCount category_counts = 11;
|
||||
repeated ReportTopUser top_reported_users = 12;
|
||||
repeated ReportTopUser top_reporters = 13;
|
||||
}
|
||||
|
||||
message ReportCategoryCount {
|
||||
optional string category = 1;
|
||||
optional int32 count = 2;
|
||||
}
|
||||
|
||||
message ReportTopUser {
|
||||
optional string user_name = 1;
|
||||
optional int32 count = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
import "serverinfo_report.proto";
|
||||
|
||||
message Response_ReportUserInfo {
|
||||
extend Response {
|
||||
optional Response_ReportUserInfo ext = 1211;
|
||||
}
|
||||
|
||||
optional string user_name = 1;
|
||||
optional int32 total_reports = 2;
|
||||
optional int32 total_bans = 3;
|
||||
optional int32 total_warns = 4;
|
||||
optional int64 registration_time = 5;
|
||||
optional bool is_admin = 6;
|
||||
optional bool is_active = 7;
|
||||
optional string admin_notes = 8;
|
||||
repeated ServerInfo_Report recent_reports = 9;
|
||||
// Last known login of the user, epoch seconds; 0 = unknown.
|
||||
optional int64 last_login = 10;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
syntax = "proto2";
|
||||
import "response.proto";
|
||||
|
||||
message Response_ResetUserPassword {
|
||||
extend Response {
|
||||
optional Response_ResetUserPassword ext = 1218;
|
||||
}
|
||||
optional string user_name = 1;
|
||||
// The generated temporary password, shown to the moderator who
|
||||
// requested the reset. The affected user must change it on first login.
|
||||
optional string temporary_password = 2;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue