diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 7ffe97994..fd5e230b1 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -33,6 +33,7 @@ set(cockatrice_SOURCES src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp src/interface/widgets/dialogs/dlg_create_game.cpp src/interface/widgets/dialogs/dlg_crash_report.cpp + src/interface/widgets/dialogs/dlg_deck_history.cpp src/interface/widgets/dialogs/dlg_default_tags_editor.cpp src/interface/widgets/dialogs/dlg_edit_avatar.cpp src/interface/widgets/dialogs/dlg_edit_password.cpp diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_deck_history.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_deck_history.cpp new file mode 100644 index 000000000..1e76ff7ed --- /dev/null +++ b/cockatrice/src/interface/widgets/dialogs/dlg_deck_history.cpp @@ -0,0 +1,176 @@ +#include "dlg_deck_history.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +QString formatVersionTime(qint64 secsSinceEpoch) +{ + return QLocale().toString(QDateTime::fromSecsSinceEpoch(secsSinceEpoch), QLocale::ShortFormat); +} +} // namespace + +DlgDeckHistory::DlgDeckHistory(int _deckId, + const QString &_deckName, + int _limit, + AbstractClient *_client, + QWidget *parent) + : QDialog(parent), deckId(_deckId), deckName(_deckName), limit(_limit), client(_client) +{ + auto *layout = new QVBoxLayout(this); + + detailLabel = new QLabel(this); + detailLabel->setWordWrap(true); + versionsList = new QListWidget(this); + previewList = new QListWidget(this); + restoreButton = new QPushButton(this); + restoreButton->setEnabled(false); + + layout->addWidget(detailLabel); + layout->addWidget(versionsList); + layout->addWidget(previewList); + layout->addWidget(restoreButton); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + layout->addWidget(buttons); + + connect(versionsList, &QListWidget::currentRowChanged, this, &DlgDeckHistory::onSelectionChanged); + connect(restoreButton, &QPushButton::clicked, this, &DlgDeckHistory::onRestoreClicked); + + Command_DeckHistoryList listCmd; + listCmd.set_deck_id(deckId); + listCmd.set_limit(static_cast(limit)); + PendingCommand *listPend = AbstractClient::prepareSessionCommand(listCmd); + connect(listPend, &PendingCommand::finished, this, &DlgDeckHistory::onHistoryFinished); + client->sendCommand(listPend); + + Command_DeckDownload downloadCmd; + downloadCmd.set_deck_id(deckId); + PendingCommand *downloadPend = AbstractClient::prepareSessionCommand(downloadCmd); + connect(downloadPend, &PendingCommand::finished, this, &DlgDeckHistory::onCurrentFinished); + client->sendCommand(downloadPend); + + retranslateUi(); +} + +void DlgDeckHistory::retranslateUi() +{ + setWindowTitle(tr("Deck history")); + detailLabel->setText( + tr("Stored versions of %1 (newest first). Restoring uploads the version as new content.").arg(deckName)); + restoreButton->setText(tr("Restore selected version")); + renderVersions(); + renderPreview(); +} + +void DlgDeckHistory::onHistoryFinished(const Response &response, const CommandContainer & /*commandContainer*/) +{ + if (response.response_code() != Response::RespOk) { + detailLabel->setText(tr("Could not load version history.")); + return; + } + const Response_DeckHistoryList &list = response.GetExtension(Response_DeckHistoryList::ext); + versions.clear(); + for (const DeckVersionInfo &version : list.versions()) { + DeckHistoryVersion entry; + entry.versionNo = version.version_no(); + entry.createdAtSecs = static_cast(version.created_at()); + entry.createdByClientId = QString::fromStdString(version.created_by_clientid()); + entry.hash = QString::fromStdString(version.hash()); + entry.content = QString::fromStdString(version.content()); + versions.append(entry); + } + historyArrived = true; + renderVersions(); + renderPreview(); +} + +void DlgDeckHistory::onCurrentFinished(const Response &response, const CommandContainer & /*commandContainer*/) +{ + if (response.response_code() != Response::RespOk) { + detailLabel->setText(tr("Could not load the current deck.")); + return; + } + const Response_DeckDownload &download = response.GetExtension(Response_DeckDownload::ext); + currentContent = QString::fromStdString(download.deck()); + currentArrived = true; + renderPreview(); +} + +void DlgDeckHistory::renderVersions() +{ + const int row = versionsList->currentRow(); + versionsList->clear(); + for (const DeckHistoryVersion &version : versions) { + QString label = tr("Version %1 — %2").arg(version.versionNo).arg(formatVersionTime(version.createdAtSecs)); + if (!version.createdByClientId.isEmpty()) { + label += tr(" — %1").arg(version.createdByClientId); + } + versionsList->addItem(label); + } + if (row >= 0 && row < versionsList->count()) { + versionsList->setCurrentRow(row); + } else if (versionsList->count() > 0) { + versionsList->setCurrentRow(0); + } + onSelectionChanged(); +} + +void DlgDeckHistory::renderPreview() +{ + previewList->clear(); + restoreButton->setEnabled(false); + const int row = versionsList->currentRow(); + if (!historyArrived || !currentArrived || row < 0 || row >= versions.size()) { + return; + } + DeckList currentDeck; + DeckList selectedDeck; + if (!currentDeck.loadFromString_Native(currentContent) || + !selectedDeck.loadFromString_Native(versions.at(row).content)) { + return; + } + const DeckDiffResult shown = DeckDiff::diff(currentDeck, selectedDeck); + for (const DeckCardChange &change : shown.changes) { + previewList->addItem( + tr("%1: %2 %3 → %4").arg(change.zone).arg(change.cardName).arg(change.oldCount).arg(change.newCount)); + } + restoreButton->setEnabled(true); +} + +void DlgDeckHistory::onSelectionChanged() +{ + renderPreview(); +} + +void DlgDeckHistory::onRestoreClicked() +{ + const int row = versionsList->currentRow(); + if (row < 0 || row >= versions.size()) { + return; + } + if (QMessageBox::question(this, tr("Restore version"), + tr("Restore version %1 as the current deck content? This becomes a new version.") + .arg(versions.at(row).versionNo)) != QMessageBox::Yes) { + return; + } + emit restoreRequested(deckId, versions.at(row).content); + accept(); +} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_deck_history.h b/cockatrice/src/interface/widgets/dialogs/dlg_deck_history.h new file mode 100644 index 000000000..cba196915 --- /dev/null +++ b/cockatrice/src/interface/widgets/dialogs/dlg_deck_history.h @@ -0,0 +1,73 @@ +/** + * @file dlg_deck_history.h + * @brief Browser for the stored versions of one server deck. + */ + +#ifndef DLG_DECK_HISTORY_H +#define DLG_DECK_HISTORY_H + +#include +#include +#include + +class AbstractClient; +class CommandContainer; +class QLabel; +class QListWidget; +class QPushButton; +class Response; + +/** @brief One stored deck version for display. */ +struct DeckHistoryVersion +{ + unsigned int versionNo = 0; + qint64 createdAtSecs = 0; + QString createdByClientId; + QString hash; + QString content; +}; + +/** + * @class DlgDeckHistory + * @brief Lists stored versions with a diff preview against the current deck. + * + * Restoring uploads the selected version as the new current content through + * the regular deck-upload path, so it becomes a version itself instead of + * rewriting history. Rejected dialogs restore nothing. + */ +class DlgDeckHistory : public QDialog +{ + Q_OBJECT +public: + DlgDeckHistory(int deckId, const QString &deckName, int limit, AbstractClient *client, QWidget *parent = nullptr); + void retranslateUi(); + +signals: + void restoreRequested(int deckId, const QString &content); + +private slots: + void onHistoryFinished(const Response &response, const CommandContainer &commandContainer); + void onCurrentFinished(const Response &response, const CommandContainer &commandContainer); + void onSelectionChanged(); + void onRestoreClicked(); + +private: + void renderVersions(); + void renderPreview(); + + int deckId = -1; + QString deckName; + int limit = 20; + AbstractClient *client = nullptr; + QList versions; + QString currentContent; + bool historyArrived = false; + bool currentArrived = false; + + QLabel *detailLabel; + QListWidget *versionsList; + QListWidget *previewList; + QPushButton *restoreButton; +}; + +#endif // DLG_DECK_HISTORY_H diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 6a24f7db6..84e47c33b 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -383,12 +383,20 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&syncIntervalEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().deckSync(), &DeckSyncSettings::setSyncIntervalMinutes); + historyLimitEdit.setMinimum(1); + historyLimitEdit.setMaximum(100); + historyLimitEdit.setValue(settings.deckSync().getHistoryLimit()); + connect(&historyLimitEdit, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance().deckSync(), + &DeckSyncSettings::setHistoryLimit); + auto *deckSyncGrid = new QGridLayout; deckSyncGrid->addWidget(&conflictDefaultLabel, 0, 0); deckSyncGrid->addWidget(&conflictDefaultSelector, 0, 1); deckSyncGrid->addWidget(&autoSyncCheckBox, 1, 0, 1, 2); deckSyncGrid->addWidget(&syncIntervalLabel, 2, 0); deckSyncGrid->addWidget(&syncIntervalEdit, 2, 1); + deckSyncGrid->addWidget(&historyLimitLabel, 3, 0); + deckSyncGrid->addWidget(&historyLimitEdit, 3, 1); deckSyncGroupBox = new QGroupBox; deckSyncGroupBox->setLayout(deckSyncGrid); @@ -692,5 +700,6 @@ void GeneralSettingsPage::retranslateUi() autoSyncCheckBox.setText(tr("Sync decks automatically")); syncIntervalLabel.setText(tr("Check every:")); syncIntervalEdit.setSuffix(" " + tr("min")); + historyLimitLabel.setText(tr("Kept versions per deck:")); resetAllPathsButton->setText(tr("Reset all paths")); } diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index f410abdf2..4ed25b279 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -111,6 +111,8 @@ private: QCheckBox autoSyncCheckBox; QLabel syncIntervalLabel; QSpinBox syncIntervalEdit; + QLabel historyLimitLabel; + QSpinBox historyLimitEdit; }; #endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 8f8555292..a7522d256 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -8,6 +8,7 @@ #include "../cards/additional_info/deck_color_identity.h" #include "../deck_share/deck_share_utils.h" #include "../deck_share/share_bar_widget.h" +#include "../dialogs/dlg_deck_history.h" #include "../dialogs/dlg_sync_conflict.h" #include "../dialogs/dlg_sync_onboarding.h" #include "../interface/widgets/server/remote/remote_decklist_tree_widget.h" @@ -241,6 +242,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, aSyncNow->setIcon(qApp->style()->standardIcon(QStyle::SP_BrowserReload)); connect(aSyncNow, &QAction::triggered, this, &TabDeckStorage::actSyncNow); + aDeckHistory = new QAction(this); + aDeckHistory->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogDetailedView)); + connect(aDeckHistory, &QAction::triggered, this, &TabDeckStorage::actDeckHistory); + // Add actions to toolbars leftToolBar->addAction(aOpenLocalDeck); leftToolBar->addAction(aRenameLocal); @@ -253,6 +258,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, rightToolBar->addAction(aOpenRemoteDeck); rightToolBar->addAction(aDownload); rightToolBar->addAction(aSyncNow); + rightToolBar->addAction(aDeckHistory); rightToolBar->addAction(aShareDecks); rightToolBar->addAction(aPublishDeck); rightToolBar->addAction(aNewFolder); @@ -314,6 +320,7 @@ void TabDeckStorage::retranslateUi() aPublishDeck->setText(tr("Publish/unpublish deck")); aOpenDecksFolder->setText(tr("Open decks folder")); aSyncNow->setText(tr("Sync now")); + aDeckHistory->setText(tr("Version history")); renderSyncStatus(); shareBar->retranslateUi(); if (shareBar->isVisible()) { @@ -369,6 +376,7 @@ void TabDeckStorage::setRemoteEnabled(bool enabled) aPublishDeck->setEnabled(enabled); aNewFolder->setEnabled(enabled); aDeleteRemoteDeck->setEnabled(enabled); + aDeckHistory->setEnabled(enabled); aSyncNow->setEnabled(enabled && !syncManager->isSyncing()); if (enabled) { @@ -654,6 +662,58 @@ void TabDeckStorage::renderSyncStatus() } } +void TabDeckStorage::actDeckHistory() +{ + QList files; + for (const auto curRight : serverDirView->getCurrentSelection()) { + if (auto *file = dynamic_cast(curRight)) { + files.append(file); + } + } + if (files.size() != 1) { + QMessageBox::information(this, tr("Version history"), tr("Select a single server deck to view its history.")); + return; + } + + auto *dialog = new DlgDeckHistory(files.first()->getId(), files.first()->getName(), + SettingsCache::instance().deckSync().getHistoryLimit(), client, this); + connect(dialog, &DlgDeckHistory::restoreRequested, this, &TabDeckStorage::onHistoryRestoreRequested); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + +void TabDeckStorage::onHistoryRestoreRequested(int deckId, const QString &content) +{ + DeckList deck; + if (!deck.loadFromString_Native(content)) { + return; + } + const QString deckString = deck.writeToString_Native(); + if (deckString.length() > MAX_FILE_LENGTH) { + QMessageBox::critical(this, tr("Error"), tr("Restored deck is too large")); + return; + } + + Command_DeckUpload cmd; + cmd.set_deck_id(deckId); + cmd.set_deck_list(deckString.toStdString()); + cmd.set_color_identity(getDeckColorIdentity(deck, CardDatabaseManager::query()).toStdString()); + + PendingCommand *pend = client->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, &TabDeckStorage::onHistoryRestoreFinished); + client->sendCommand(pend); +} + +void TabDeckStorage::onHistoryRestoreFinished(const Response &response, const CommandContainer & /*commandContainer*/) +{ + if (response.response_code() != Response::RespOk) { + QMessageBox::critical(this, tr("Error"), tr("Failed to restore deck version")); + return; + } + onSyncStatusMessage(tr("Version restored.")); + serverDirView->refreshTree(); +} + void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight) { if (dynamic_cast(serverDirView->getNode(curRight))) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h index 610cce891..6254c6505 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h @@ -55,6 +55,7 @@ private: QAction *aOpenDecksFolder; QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck; QAction *aSyncNow; + QAction *aDeckHistory; QLabel *syncStatusLabel; DeckSyncManager *syncManager; QString syncUsername; @@ -130,6 +131,9 @@ private slots: void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer); void actSyncNow(); + void actDeckHistory(); + void onHistoryRestoreRequested(int deckId, const QString &content); + void onHistoryRestoreFinished(const Response &response, const CommandContainer &commandContainer); void onFirstSyncDetected(int localCount, int serverCount); void onConflictRequest(const DeckSyncConflict &conflict); void onSyncStatusMessage(const QString &message); diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt index 0860b407b..75345f236 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/CMakeLists.txt @@ -16,6 +16,7 @@ set(PROTO_FILES command_deck_del_dir.proto command_deck_download.proto command_deck_download_public.proto + command_deck_history_list.proto command_deck_list.proto command_deck_list_other_user.proto command_deck_new_dir.proto @@ -147,6 +148,7 @@ set(PROTO_FILES response_ban_history.proto response_card_art_rule_entry.proto response_deck_download.proto + response_deck_history_list.proto response_deck_list.proto response_deck_share_create.proto response_deck_share_download.proto diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_history_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_history_list.proto new file mode 100644 index 000000000..5931503f4 --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/command_deck_history_list.proto @@ -0,0 +1,12 @@ +syntax = "proto2"; +import "session_commands.proto"; + +message Command_DeckHistoryList { + extend SessionCommand { + optional Command_DeckHistoryList ext = 1035; + } + optional sint32 deck_id = 1 [default = -1]; + // Maximum versions to return, newest first. Capped server-side by + // deck_sync/max_versions. + optional uint32 limit = 2 [default = 20]; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto index 8ec4c3554..65986c153 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response.proto @@ -86,6 +86,7 @@ message Response { DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares DECK_SYNC_STATUS = 1107; // Response with changed decks since a timestamp + DECK_HISTORY_LIST = 1108; // Response listing deck versions CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules } diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_history_list.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_history_list.proto new file mode 100644 index 000000000..bcb8da7ca --- /dev/null +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_deck_history_list.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; +import "response.proto"; + +message DeckVersionInfo { + optional uint32 version_no = 1; + optional uint64 created_at = 2; + optional string created_by_clientid = 3; + optional string hash = 4; + // Full deck content in native format, so the client can diff and + // restore without further round trips. + optional string content = 5; +} + +message Response_DeckHistoryList { + extend Response { + optional Response_DeckHistoryList ext = 1108; + } + repeated DeckVersionInfo versions = 1; +} diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index b9a80021f..13ef307a9 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -38,6 +38,7 @@ message SessionCommand { DECK_SHARE_REMOVE = 1033; SUBMIT_PASSWORD_VERIFIER = 1034; DECK_SYNC_CHECK = 1035; + DECK_HISTORY_LIST = 1036; REPLAY_LIST = 1100; REPLAY_DOWNLOAD = 1101; REPLAY_MODIFY_MATCH = 1102; diff --git a/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.cpp b/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.cpp index caa9ce9c6..2b03f4119 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.cpp @@ -42,3 +42,14 @@ void DeckSyncSettings::setSyncIntervalMinutes(int _syncIntervalMinutes) setValue(_syncIntervalMinutes, "syncIntervalMinutes"); emit syncIntervalMinutesChanged(_syncIntervalMinutes); } + +int DeckSyncSettings::getHistoryLimit() const +{ + return qBound(1, getValue("historyLimit", QString(), QString(), 20).toInt(), 100); +} + +void DeckSyncSettings::setHistoryLimit(int _historyLimit) +{ + setValue(_historyLimit, "historyLimit"); + emit historyLimitChanged(_historyLimit); +} diff --git a/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.h b/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.h index 2552f52a9..5475ff343 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/deck_sync_settings.h @@ -30,11 +30,14 @@ public: void setAutoSync(bool _autoSync); [[nodiscard]] int getSyncIntervalMinutes() const; void setSyncIntervalMinutes(int _syncIntervalMinutes); + [[nodiscard]] int getHistoryLimit() const; + void setHistoryLimit(int _historyLimit); signals: void conflictDefaultChanged(int conflictDefault); void autoSyncChanged(bool autoSync); void syncIntervalMinutesChanged(int syncIntervalMinutes); + void historyLimitChanged(int historyLimit); private: DeckSyncSettings(const DeckSyncSettings & /*other*/); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 51c5b8d92..8f448d170 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -89,6 +89,7 @@ #include #include #include +#include #include #include #include @@ -130,6 +131,7 @@ #include #include #include +#include #include #include #include @@ -375,6 +377,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc); case SessionCommand::DECK_SYNC_CHECK: return cmdDeckSyncCheck(cmd.GetExtension(Command_DeckSyncCheck::ext), rc); + case SessionCommand::DECK_HISTORY_LIST: + return cmdDeckHistoryList(cmd.GetExtension(Command_DeckHistoryList::ext), rc); case SessionCommand::REPLAY_LIST: return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc); case SessionCommand::REPLAY_DOWNLOAD: @@ -953,6 +957,50 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSyncCheck(const Com return Response::RespOk; } +Response::ResponseCode AbstractServerSocketInterface::cmdDeckHistoryList(const Command_DeckHistoryList &cmd, + ResponseContainer &rc) +{ + if (authState != PasswordRight) { + return Response::RespFunctionNotAllowed; + } + + sqlInterface->checkSql(); + + QSqlQuery *ownerQuery = + sqlInterface->prepareQuery("select 1 from {prefix}_decklist_files where id = :id and id_user = :id_user"); + ownerQuery->bindValue(":id", cmd.deck_id()); + ownerQuery->bindValue(":id_user", userInfo->id()); + if (!sqlInterface->execSqlQuery(ownerQuery)) { + return Response::RespContextError; + } + if (!ownerQuery->next()) { + return Response::RespNameNotFound; + } + + const int limit = qMin(cmd.has_limit() ? static_cast(cmd.limit()) : 20, servatrice->getDeckSyncMaxVersions()); + QSqlQuery *query = + sqlInterface->prepareQuery("select version_no, unix_timestamp(created_at), created_by_clientid, hash, " + "content from {prefix}_deck_versions where deck_id = :deck_id order by version_no " + "desc limit :limit"); + query->bindValue(":deck_id", cmd.deck_id()); + query->bindValue(":limit", qMax(1, limit)); + if (!sqlInterface->execSqlQuery(query)) { + return Response::RespContextError; + } + + Response_DeckHistoryList *re = new Response_DeckHistoryList; + while (query->next()) { + DeckVersionInfo *version = re->add_versions(); + version->set_version_no(query->value(0).toUInt()); + version->set_created_at(query->value(1).toULongLong()); + version->set_created_by_clientid(query->value(2).toString().toStdString()); + version->set_hash(query->value(3).toString().toStdString()); + version->set_content(query->value(4).toString().toStdString()); + } + rc.setResponseExtension(re); + return Response::RespOk; +} + Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc) { diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index a55f8df0f..20695accd 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -56,6 +56,7 @@ class Command_DeckShareList; class Command_DeckShareListMine; class Command_DeckShareRemove; class Command_DeckShareDownload; +class Command_DeckHistoryList; class Command_DeckSyncCheck; class Command_ReplayList; class Command_ReplayDownload; @@ -112,6 +113,7 @@ class Command_ViewLogHistory; class Command_WarnUser; class QObject; class ResponseContainer; +class Response_DeckHistoryList; class Response_DeckSyncStatus; class AbstractServerSocketInterface : public Server_ProtocolHandler @@ -167,6 +169,7 @@ private: Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc); bool syncCheckHelper(int userId, qint64 sinceSecs, Response_DeckSyncStatus *status); Response::ResponseCode cmdDeckSyncCheck(const Command_DeckSyncCheck &cmd, ResponseContainer &rc); + Response::ResponseCode cmdDeckHistoryList(const Command_DeckHistoryList &cmd, ResponseContainer &rc); Response::ResponseCode cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc); Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc); Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index cc8b21ecd..04545b533 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -633,6 +633,18 @@ TEST_F(SettingsDefaultsTest, DeckSync_SyncIntervalMinutes_DefaultAndBounds) ASSERT_EQ(s.getSyncIntervalMinutes(), 60); } +TEST_F(SettingsDefaultsTest, DeckSync_HistoryLimit_DefaultAndBounds) +{ + DeckSyncSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getHistoryLimit(), 20); + s.setHistoryLimit(50); + ASSERT_EQ(s.getHistoryLimit(), 50); + s.setHistoryLimit(0); + ASSERT_EQ(s.getHistoryLimit(), 1); + s.setHistoryLimit(500); + ASSERT_EQ(s.getHistoryLimit(), 100); +} + // --- NetworkSettings --- TEST_F(SettingsDefaultsTest, Network_ClientID_Default)