mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Client+Server] Deck version history with restore
This commit is contained in:
parent
875e87d1bc
commit
44f57aa2b9
17 changed files with 437 additions and 0 deletions
|
|
@ -33,6 +33,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
|
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
|
||||||
src/interface/widgets/dialogs/dlg_create_game.cpp
|
src/interface/widgets/dialogs/dlg_create_game.cpp
|
||||||
src/interface/widgets/dialogs/dlg_crash_report.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_default_tags_editor.cpp
|
||||||
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
||||||
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
||||||
|
|
|
||||||
176
cockatrice/src/interface/widgets/dialogs/dlg_deck_history.cpp
Normal file
176
cockatrice/src/interface/widgets/dialogs/dlg_deck_history.cpp
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
#include "dlg_deck_history.h"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <libcockatrice/deck_list/deck_diff.h>
|
||||||
|
#include <libcockatrice/deck_list/deck_list.h>
|
||||||
|
#include <libcockatrice/network/client/abstract/abstract_client.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_history_list.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_history_list.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pending_command.h>
|
||||||
|
|
||||||
|
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<google::protobuf::uint32>(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<qint64>(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();
|
||||||
|
}
|
||||||
73
cockatrice/src/interface/widgets/dialogs/dlg_deck_history.h
Normal file
73
cockatrice/src/interface/widgets/dialogs/dlg_deck_history.h
Normal file
|
|
@ -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 <QDialog>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
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<DeckHistoryVersion> versions;
|
||||||
|
QString currentContent;
|
||||||
|
bool historyArrived = false;
|
||||||
|
bool currentArrived = false;
|
||||||
|
|
||||||
|
QLabel *detailLabel;
|
||||||
|
QListWidget *versionsList;
|
||||||
|
QListWidget *previewList;
|
||||||
|
QPushButton *restoreButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DLG_DECK_HISTORY_H
|
||||||
|
|
@ -383,12 +383,20 @@ GeneralSettingsPage::GeneralSettingsPage()
|
||||||
connect(&syncIntervalEdit, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().deckSync(),
|
connect(&syncIntervalEdit, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().deckSync(),
|
||||||
&DeckSyncSettings::setSyncIntervalMinutes);
|
&DeckSyncSettings::setSyncIntervalMinutes);
|
||||||
|
|
||||||
|
historyLimitEdit.setMinimum(1);
|
||||||
|
historyLimitEdit.setMaximum(100);
|
||||||
|
historyLimitEdit.setValue(settings.deckSync().getHistoryLimit());
|
||||||
|
connect(&historyLimitEdit, qOverload<int>(&QSpinBox::valueChanged), &SettingsCache::instance().deckSync(),
|
||||||
|
&DeckSyncSettings::setHistoryLimit);
|
||||||
|
|
||||||
auto *deckSyncGrid = new QGridLayout;
|
auto *deckSyncGrid = new QGridLayout;
|
||||||
deckSyncGrid->addWidget(&conflictDefaultLabel, 0, 0);
|
deckSyncGrid->addWidget(&conflictDefaultLabel, 0, 0);
|
||||||
deckSyncGrid->addWidget(&conflictDefaultSelector, 0, 1);
|
deckSyncGrid->addWidget(&conflictDefaultSelector, 0, 1);
|
||||||
deckSyncGrid->addWidget(&autoSyncCheckBox, 1, 0, 1, 2);
|
deckSyncGrid->addWidget(&autoSyncCheckBox, 1, 0, 1, 2);
|
||||||
deckSyncGrid->addWidget(&syncIntervalLabel, 2, 0);
|
deckSyncGrid->addWidget(&syncIntervalLabel, 2, 0);
|
||||||
deckSyncGrid->addWidget(&syncIntervalEdit, 2, 1);
|
deckSyncGrid->addWidget(&syncIntervalEdit, 2, 1);
|
||||||
|
deckSyncGrid->addWidget(&historyLimitLabel, 3, 0);
|
||||||
|
deckSyncGrid->addWidget(&historyLimitEdit, 3, 1);
|
||||||
deckSyncGroupBox = new QGroupBox;
|
deckSyncGroupBox = new QGroupBox;
|
||||||
deckSyncGroupBox->setLayout(deckSyncGrid);
|
deckSyncGroupBox->setLayout(deckSyncGrid);
|
||||||
|
|
||||||
|
|
@ -692,5 +700,6 @@ void GeneralSettingsPage::retranslateUi()
|
||||||
autoSyncCheckBox.setText(tr("Sync decks automatically"));
|
autoSyncCheckBox.setText(tr("Sync decks automatically"));
|
||||||
syncIntervalLabel.setText(tr("Check every:"));
|
syncIntervalLabel.setText(tr("Check every:"));
|
||||||
syncIntervalEdit.setSuffix(" " + tr("min"));
|
syncIntervalEdit.setSuffix(" " + tr("min"));
|
||||||
|
historyLimitLabel.setText(tr("Kept versions per deck:"));
|
||||||
resetAllPathsButton->setText(tr("Reset all paths"));
|
resetAllPathsButton->setText(tr("Reset all paths"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ private:
|
||||||
QCheckBox autoSyncCheckBox;
|
QCheckBox autoSyncCheckBox;
|
||||||
QLabel syncIntervalLabel;
|
QLabel syncIntervalLabel;
|
||||||
QSpinBox syncIntervalEdit;
|
QSpinBox syncIntervalEdit;
|
||||||
|
QLabel historyLimitLabel;
|
||||||
|
QSpinBox historyLimitEdit;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include "../cards/additional_info/deck_color_identity.h"
|
#include "../cards/additional_info/deck_color_identity.h"
|
||||||
#include "../deck_share/deck_share_utils.h"
|
#include "../deck_share/deck_share_utils.h"
|
||||||
#include "../deck_share/share_bar_widget.h"
|
#include "../deck_share/share_bar_widget.h"
|
||||||
|
#include "../dialogs/dlg_deck_history.h"
|
||||||
#include "../dialogs/dlg_sync_conflict.h"
|
#include "../dialogs/dlg_sync_conflict.h"
|
||||||
#include "../dialogs/dlg_sync_onboarding.h"
|
#include "../dialogs/dlg_sync_onboarding.h"
|
||||||
#include "../interface/widgets/server/remote/remote_decklist_tree_widget.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));
|
aSyncNow->setIcon(qApp->style()->standardIcon(QStyle::SP_BrowserReload));
|
||||||
connect(aSyncNow, &QAction::triggered, this, &TabDeckStorage::actSyncNow);
|
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
|
// Add actions to toolbars
|
||||||
leftToolBar->addAction(aOpenLocalDeck);
|
leftToolBar->addAction(aOpenLocalDeck);
|
||||||
leftToolBar->addAction(aRenameLocal);
|
leftToolBar->addAction(aRenameLocal);
|
||||||
|
|
@ -253,6 +258,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||||
rightToolBar->addAction(aOpenRemoteDeck);
|
rightToolBar->addAction(aOpenRemoteDeck);
|
||||||
rightToolBar->addAction(aDownload);
|
rightToolBar->addAction(aDownload);
|
||||||
rightToolBar->addAction(aSyncNow);
|
rightToolBar->addAction(aSyncNow);
|
||||||
|
rightToolBar->addAction(aDeckHistory);
|
||||||
rightToolBar->addAction(aShareDecks);
|
rightToolBar->addAction(aShareDecks);
|
||||||
rightToolBar->addAction(aPublishDeck);
|
rightToolBar->addAction(aPublishDeck);
|
||||||
rightToolBar->addAction(aNewFolder);
|
rightToolBar->addAction(aNewFolder);
|
||||||
|
|
@ -314,6 +320,7 @@ void TabDeckStorage::retranslateUi()
|
||||||
aPublishDeck->setText(tr("Publish/unpublish deck"));
|
aPublishDeck->setText(tr("Publish/unpublish deck"));
|
||||||
aOpenDecksFolder->setText(tr("Open decks folder"));
|
aOpenDecksFolder->setText(tr("Open decks folder"));
|
||||||
aSyncNow->setText(tr("Sync now"));
|
aSyncNow->setText(tr("Sync now"));
|
||||||
|
aDeckHistory->setText(tr("Version history"));
|
||||||
renderSyncStatus();
|
renderSyncStatus();
|
||||||
shareBar->retranslateUi();
|
shareBar->retranslateUi();
|
||||||
if (shareBar->isVisible()) {
|
if (shareBar->isVisible()) {
|
||||||
|
|
@ -369,6 +376,7 @@ void TabDeckStorage::setRemoteEnabled(bool enabled)
|
||||||
aPublishDeck->setEnabled(enabled);
|
aPublishDeck->setEnabled(enabled);
|
||||||
aNewFolder->setEnabled(enabled);
|
aNewFolder->setEnabled(enabled);
|
||||||
aDeleteRemoteDeck->setEnabled(enabled);
|
aDeleteRemoteDeck->setEnabled(enabled);
|
||||||
|
aDeckHistory->setEnabled(enabled);
|
||||||
aSyncNow->setEnabled(enabled && !syncManager->isSyncing());
|
aSyncNow->setEnabled(enabled && !syncManager->isSyncing());
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
|
@ -654,6 +662,58 @@ void TabDeckStorage::renderSyncStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabDeckStorage::actDeckHistory()
|
||||||
|
{
|
||||||
|
QList<RemoteDeckList_TreeModel::FileNode *> files;
|
||||||
|
for (const auto curRight : serverDirView->getCurrentSelection()) {
|
||||||
|
if (auto *file = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(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)
|
void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight)
|
||||||
{
|
{
|
||||||
if (dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getNode(curRight))) {
|
if (dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getNode(curRight))) {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ private:
|
||||||
QAction *aOpenDecksFolder;
|
QAction *aOpenDecksFolder;
|
||||||
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck;
|
QAction *aOpenRemoteDeck, *aDownload, *aShareDecks, *aPublishDeck, *aNewFolder, *aDeleteRemoteDeck;
|
||||||
QAction *aSyncNow;
|
QAction *aSyncNow;
|
||||||
|
QAction *aDeckHistory;
|
||||||
QLabel *syncStatusLabel;
|
QLabel *syncStatusLabel;
|
||||||
DeckSyncManager *syncManager;
|
DeckSyncManager *syncManager;
|
||||||
QString syncUsername;
|
QString syncUsername;
|
||||||
|
|
@ -130,6 +131,9 @@ private slots:
|
||||||
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
|
||||||
|
|
||||||
void actSyncNow();
|
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 onFirstSyncDetected(int localCount, int serverCount);
|
||||||
void onConflictRequest(const DeckSyncConflict &conflict);
|
void onConflictRequest(const DeckSyncConflict &conflict);
|
||||||
void onSyncStatusMessage(const QString &message);
|
void onSyncStatusMessage(const QString &message);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ set(PROTO_FILES
|
||||||
command_deck_del_dir.proto
|
command_deck_del_dir.proto
|
||||||
command_deck_download.proto
|
command_deck_download.proto
|
||||||
command_deck_download_public.proto
|
command_deck_download_public.proto
|
||||||
|
command_deck_history_list.proto
|
||||||
command_deck_list.proto
|
command_deck_list.proto
|
||||||
command_deck_list_other_user.proto
|
command_deck_list_other_user.proto
|
||||||
command_deck_new_dir.proto
|
command_deck_new_dir.proto
|
||||||
|
|
@ -147,6 +148,7 @@ set(PROTO_FILES
|
||||||
response_ban_history.proto
|
response_ban_history.proto
|
||||||
response_card_art_rule_entry.proto
|
response_card_art_rule_entry.proto
|
||||||
response_deck_download.proto
|
response_deck_download.proto
|
||||||
|
response_deck_history_list.proto
|
||||||
response_deck_list.proto
|
response_deck_list.proto
|
||||||
response_deck_share_create.proto
|
response_deck_share_create.proto
|
||||||
response_deck_share_download.proto
|
response_deck_share_download.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];
|
||||||
|
}
|
||||||
|
|
@ -86,6 +86,7 @@ message Response {
|
||||||
DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download
|
DECK_SHARE_DOWNLOAD = 1105; // Response for shared deck download
|
||||||
DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares
|
DECK_SHARE_LIST_MINE = 1106; // Response listing the caller's own shares
|
||||||
DECK_SYNC_STATUS = 1107; // Response with changed decks since a timestamp
|
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
|
CARD_ART_RULE_LIST = 1200; // Response containing a list of card art rules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,7 @@ message SessionCommand {
|
||||||
DECK_SHARE_REMOVE = 1033;
|
DECK_SHARE_REMOVE = 1033;
|
||||||
SUBMIT_PASSWORD_VERIFIER = 1034;
|
SUBMIT_PASSWORD_VERIFIER = 1034;
|
||||||
DECK_SYNC_CHECK = 1035;
|
DECK_SYNC_CHECK = 1035;
|
||||||
|
DECK_HISTORY_LIST = 1036;
|
||||||
REPLAY_LIST = 1100;
|
REPLAY_LIST = 1100;
|
||||||
REPLAY_DOWNLOAD = 1101;
|
REPLAY_DOWNLOAD = 1101;
|
||||||
REPLAY_MODIFY_MATCH = 1102;
|
REPLAY_MODIFY_MATCH = 1102;
|
||||||
|
|
|
||||||
|
|
@ -42,3 +42,14 @@ void DeckSyncSettings::setSyncIntervalMinutes(int _syncIntervalMinutes)
|
||||||
setValue(_syncIntervalMinutes, "syncIntervalMinutes");
|
setValue(_syncIntervalMinutes, "syncIntervalMinutes");
|
||||||
emit syncIntervalMinutesChanged(_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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,14 @@ public:
|
||||||
void setAutoSync(bool _autoSync);
|
void setAutoSync(bool _autoSync);
|
||||||
[[nodiscard]] int getSyncIntervalMinutes() const;
|
[[nodiscard]] int getSyncIntervalMinutes() const;
|
||||||
void setSyncIntervalMinutes(int _syncIntervalMinutes);
|
void setSyncIntervalMinutes(int _syncIntervalMinutes);
|
||||||
|
[[nodiscard]] int getHistoryLimit() const;
|
||||||
|
void setHistoryLimit(int _historyLimit);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void conflictDefaultChanged(int conflictDefault);
|
void conflictDefaultChanged(int conflictDefault);
|
||||||
void autoSyncChanged(bool autoSync);
|
void autoSyncChanged(bool autoSync);
|
||||||
void syncIntervalMinutesChanged(int syncIntervalMinutes);
|
void syncIntervalMinutesChanged(int syncIntervalMinutes);
|
||||||
|
void historyLimitChanged(int historyLimit);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DeckSyncSettings(const DeckSyncSettings & /*other*/);
|
DeckSyncSettings(const DeckSyncSettings & /*other*/);
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@
|
||||||
#include <libcockatrice/protocol/pb/command_deck_del_dir.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_del_dir.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/command_deck_history_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_list_other_user.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_list_other_user.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
||||||
|
|
@ -130,6 +131,7 @@
|
||||||
#include <libcockatrice/protocol/pb/response_card_art_rule_entry.pb.h>
|
#include <libcockatrice/protocol/pb/response_card_art_rule_entry.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_clear_blacklisted_banners.pb.h>
|
#include <libcockatrice/protocol/pb/response_clear_blacklisted_banners.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
||||||
|
#include <libcockatrice/protocol/pb/response_deck_history_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
||||||
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
||||||
|
|
@ -375,6 +377,8 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
|
||||||
return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc);
|
return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc);
|
||||||
case SessionCommand::DECK_SYNC_CHECK:
|
case SessionCommand::DECK_SYNC_CHECK:
|
||||||
return cmdDeckSyncCheck(cmd.GetExtension(Command_DeckSyncCheck::ext), rc);
|
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:
|
case SessionCommand::REPLAY_LIST:
|
||||||
return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc);
|
return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc);
|
||||||
case SessionCommand::REPLAY_DOWNLOAD:
|
case SessionCommand::REPLAY_DOWNLOAD:
|
||||||
|
|
@ -953,6 +957,50 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckSyncCheck(const Com
|
||||||
return Response::RespOk;
|
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<int>(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,
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd,
|
||||||
ResponseContainer &rc)
|
ResponseContainer &rc)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ class Command_DeckShareList;
|
||||||
class Command_DeckShareListMine;
|
class Command_DeckShareListMine;
|
||||||
class Command_DeckShareRemove;
|
class Command_DeckShareRemove;
|
||||||
class Command_DeckShareDownload;
|
class Command_DeckShareDownload;
|
||||||
|
class Command_DeckHistoryList;
|
||||||
class Command_DeckSyncCheck;
|
class Command_DeckSyncCheck;
|
||||||
class Command_ReplayList;
|
class Command_ReplayList;
|
||||||
class Command_ReplayDownload;
|
class Command_ReplayDownload;
|
||||||
|
|
@ -112,6 +113,7 @@ class Command_ViewLogHistory;
|
||||||
class Command_WarnUser;
|
class Command_WarnUser;
|
||||||
class QObject;
|
class QObject;
|
||||||
class ResponseContainer;
|
class ResponseContainer;
|
||||||
|
class Response_DeckHistoryList;
|
||||||
class Response_DeckSyncStatus;
|
class Response_DeckSyncStatus;
|
||||||
|
|
||||||
class AbstractServerSocketInterface : public Server_ProtocolHandler
|
class AbstractServerSocketInterface : public Server_ProtocolHandler
|
||||||
|
|
@ -167,6 +169,7 @@ private:
|
||||||
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc);
|
||||||
bool syncCheckHelper(int userId, qint64 sinceSecs, Response_DeckSyncStatus *status);
|
bool syncCheckHelper(int userId, qint64 sinceSecs, Response_DeckSyncStatus *status);
|
||||||
Response::ResponseCode cmdDeckSyncCheck(const Command_DeckSyncCheck &cmd, ResponseContainer &rc);
|
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 cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckShareCreate(const Command_DeckShareCreate &cmd, ResponseContainer &rc);
|
||||||
Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc);
|
Response::ResponseCode cmdDeckShareList(const Command_DeckShareList &cmd, ResponseContainer &rc);
|
||||||
|
|
|
||||||
|
|
@ -633,6 +633,18 @@ TEST_F(SettingsDefaultsTest, DeckSync_SyncIntervalMinutes_DefaultAndBounds)
|
||||||
ASSERT_EQ(s.getSyncIntervalMinutes(), 60);
|
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 ---
|
// --- NetworkSettings ---
|
||||||
|
|
||||||
TEST_F(SettingsDefaultsTest, Network_ClientID_Default)
|
TEST_F(SettingsDefaultsTest, Network_ClientID_Default)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue