diff --git a/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp b/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp index 80ef22687..34b71a452 100644 --- a/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp +++ b/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp @@ -321,6 +321,7 @@ void DeckSyncManager::startSync(bool _autoRun) sessionDefault = -1; firstSyncRun = false; pendingServerTime = -1; + serverChangeSkipped = false; emit syncStarted(); emit statusMessage(tr("Scanning local decks...")); @@ -426,7 +427,6 @@ void DeckSyncManager::onCheckFinished(const Response &response, const CommandCon return; } - pendingServerTime = status.has_server_time() ? static_cast(status.server_time()) : -1; unchangedCount = localFiles.size(); emit statusMessage(tr("Decks are already in sync.")); finishSync(); @@ -641,6 +641,10 @@ void DeckSyncManager::classify(const QMap &serverFiles) const QString backupPath = absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch()); if (!QFile::copy(absolutePath, backupPath) || !QFile::remove(absolutePath)) { + // The server row is gone but the local file could not be + // removed. Keep the baseline so the next check still reports + // the delete instead of hiding it behind a newer sync time. + serverChangeSkipped = true; ++skippedCount; continue; } @@ -1249,6 +1253,7 @@ void DeckSyncManager::onDownloadFinished(const Response &response, return; } if (response.response_code() != Response::RespOk) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1275,6 +1280,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, { DeckList deck; if (!deck.loadFromString_Native(deckString)) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1283,6 +1289,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, const QString cleanDeckPath = QDir::cleanPath(deckPath); const QString absolutePath = QDir::cleanPath(deckPath + "/" + relativePath); if (absolutePath != cleanDeckPath && !absolutePath.startsWith(cleanDeckPath + "/")) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1291,6 +1298,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, const QString backupPath = absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch()); if (!QFile::copy(absolutePath, backupPath)) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1298,6 +1306,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, } QDir().mkpath(QFileInfo(absolutePath).absolutePath()); if (!DeckLoader::saveToFile(deck, absolutePath, DeckFileFormat::Cockatrice)) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1406,7 +1415,10 @@ void DeckSyncManager::finishSync() { syncing = false; DeckSyncSidecar sidecar(sidecarPath); - if (sidecar.load()) { + if (sidecar.load() && !serverChangeSkipped) { + // Advance the baseline only when every server change was applied. If one + // was skipped, keep the previous baseline so the next check reports it + // again instead of hiding it behind a newer sync time. sidecar.lastSyncSecs = pendingServerTime >= 0 ? pendingServerTime : QDateTime::currentDateTime().toSecsSinceEpoch(); sidecar.save(); diff --git a/cockatrice/src/interface/deck_sync/deck_sync_manager.h b/cockatrice/src/interface/deck_sync/deck_sync_manager.h index e3d748996..220f28039 100644 --- a/cockatrice/src/interface/deck_sync/deck_sync_manager.h +++ b/cockatrice/src/interface/deck_sync/deck_sync_manager.h @@ -255,6 +255,7 @@ private: int removedCount = 0; int unchangedCount = 0; int skippedCount = 0; + bool serverChangeSkipped = false; ///< A server change could not be applied; keep the sync baseline. int conflictCount = 0; bool autoRun = false; }; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp index 23bf663d5..32504fa75 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp @@ -37,7 +37,8 @@ void DlgSyncOnboarding::retranslateUi() setWindowTitle(tr("Sync decks to this server")); detailLabel->setText( tr("You have %1 local decks and %2 decks stored on this server. This choice sets how decks sync with this " - "server from now on; you can reset it to two-way sync from the deck storage tab.") + "server from now on; you can reset it to two-way sync from the deck storage tab. Only two-way sync keeps " + "checking for changes automatically.") .arg(localCount) .arg(serverCount)); mergeRadio->setText(tr("Merge: keep everything, sync both ways (recommended)")); diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 25a183c12..e38d72fef 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -283,6 +283,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, connect(&SettingsCache::instance().deckSync(), &DeckSyncSettings::syncIntervalMinutesChanged, syncManager, &DeckSyncManager::setSyncIntervalMinutes); connect(syncManager, &DeckSyncManager::statusMessage, this, &TabDeckStorage::onSyncStatusMessage); + connect(syncManager, &DeckSyncManager::syncStarted, this, &TabDeckStorage::onSyncStarted); connect(syncManager, &DeckSyncManager::syncFinished, this, &TabDeckStorage::onSyncFinished); connect(syncManager, &DeckSyncManager::firstSyncDetected, this, &TabDeckStorage::onFirstSyncDetected); connect(syncManager, &DeckSyncManager::conflictRequest, this, &TabDeckStorage::onConflictRequest); @@ -656,9 +657,19 @@ void TabDeckStorage::onSyncStatusMessage(const QString &message) renderSyncStatus(); } +void TabDeckStorage::onSyncStarted() +{ + // An auto run starts from the watcher or the interval timer, so the button + // has to follow the manager: a click during a run would be dropped by + // syncNow() and leave the user pressing a button that looks live. + aSyncNow->setEnabled(false); + aSyncReset->setEnabled(false); +} + void TabDeckStorage::onSyncFinished(const SyncSummary &summary, const QString &error) { aSyncNow->setEnabled(remoteEnabled); + aSyncReset->setEnabled(remoteEnabled); if (!error.isEmpty()) { syncStatusType = SyncStatusType::Failed; syncStatusDetail = error; diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h index 9889e9a51..20fdab0fb 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h @@ -135,6 +135,7 @@ private slots: void onFirstSyncDetected(int localCount, int serverCount); void onConflictRequest(const DeckSyncConflict &conflict); void onSyncStatusMessage(const QString &message); + void onSyncStarted(); void onSyncFinished(const SyncSummary &summary, const QString &error); public: