feat(sync): wire TUI account orchestration, SyncProvider integration, and Ctrl+S keybinding
- MicrosoftAccountSyncProvider: implements SyncProvider interface with connect_account(), sync_account(), disconnect_account(), provider_id() - TuiApp: inherits SyncObserver, implements thread-safe sync progress rendering, background sync thread, and Action::sync dispatch - CLI: add "account disconnect <id>" and "account list" commands - Main wiring: initialize sync infrastructure when connected accounts exist and a valid client ID is configured - Screen: add sync_stage and sync_is_error fields to ScreenState, map Ctrl+S to Action::sync keybinding - Fix ScreenState aggregate initializers in all construction sites - Tests: new microsoft_sync_provider_tests.cpp, all 22 tests pass
This commit is contained in:
74
include/nocal/sync/microsoft_sync_provider.hpp
Normal file
74
include/nocal/sync/microsoft_sync_provider.hpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include "nocal/sync/oauth.hpp"
|
||||
#include "nocal/sync/oauth_tokens.hpp"
|
||||
#include "nocal/sync/provider.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace nocal::storage {
|
||||
class SyncCache;
|
||||
} // namespace nocal::storage
|
||||
|
||||
namespace nocal::sync {
|
||||
|
||||
class MicrosoftGraphSync;
|
||||
class OAuthTokenBroker;
|
||||
class OAuthSecretStore;
|
||||
class HttpTransport;
|
||||
|
||||
class MicrosoftAccountSyncProvider final : public SyncProvider {
|
||||
public:
|
||||
// All references must outlive this object.
|
||||
MicrosoftAccountSyncProvider(OAuthTokenBroker& token_broker,
|
||||
OAuthSecretStore& secret_store,
|
||||
storage::SyncCache& cache,
|
||||
HttpTransport& transport,
|
||||
const OAuthClientConfig& oauth_config,
|
||||
std::function<std::int64_t()> clock,
|
||||
BrowserLauncher& browser,
|
||||
AuthorizationCallbackReceiver& callback_receiver,
|
||||
EntropySource& entropy);
|
||||
|
||||
~MicrosoftAccountSyncProvider() override;
|
||||
|
||||
MicrosoftAccountSyncProvider(const MicrosoftAccountSyncProvider&) = delete;
|
||||
MicrosoftAccountSyncProvider& operator=(const MicrosoftAccountSyncProvider&) = delete;
|
||||
|
||||
// --- SyncProvider interface ---
|
||||
[[nodiscard]] std::string_view provider_id() const noexcept override;
|
||||
ConnectedAccount connect_account() override;
|
||||
void sync_account(std::string_view account_id, SyncObserver& observer) override;
|
||||
void disconnect_account(std::string_view account_id) override;
|
||||
|
||||
private:
|
||||
OAuthTokenBroker& token_broker_;
|
||||
OAuthSecretStore& secret_store_;
|
||||
storage::SyncCache& cache_;
|
||||
HttpTransport& transport_;
|
||||
OAuthClientConfig oauth_config_;
|
||||
std::function<std::int64_t()> clock_;
|
||||
BrowserLauncher& browser_;
|
||||
AuthorizationCallbackReceiver& callback_receiver_;
|
||||
EntropySource& entropy_;
|
||||
std::unique_ptr<MicrosoftGraphSync> graph_;
|
||||
|
||||
// Helper: run full PKCE connect flow and report progress.
|
||||
ConnectedAccount do_connect(SyncObserver& observer);
|
||||
|
||||
// Helper: get valid tokens, throwing AccountDisconnected on invalid credentials.
|
||||
OAuthTokens get_tokens(std::string_view account_id);
|
||||
|
||||
// Helper: sync one calendar (primary uses delta, secondaries use calendarView).
|
||||
void sync_one_calendar(std::string_view account_id,
|
||||
std::string_view calendar_id,
|
||||
std::string_view calendar_name,
|
||||
bool is_primary,
|
||||
SyncObserver& observer);
|
||||
};
|
||||
|
||||
} // namespace nocal::sync
|
||||
@@ -82,6 +82,8 @@ struct ScreenState {
|
||||
std::size_t search_result_index{0};
|
||||
std::string notification;
|
||||
bool notification_is_error{false};
|
||||
std::string sync_stage;
|
||||
bool sync_is_error{false};
|
||||
bool show_calendar_picker{false};
|
||||
std::size_t calendar_index{0};
|
||||
bool show_agenda{false};
|
||||
@@ -136,6 +138,7 @@ enum class Action {
|
||||
search,
|
||||
agenda,
|
||||
calendars,
|
||||
sync,
|
||||
quit,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "nocal/domain/event.hpp"
|
||||
#include "nocal/sync/provider.hpp"
|
||||
#include "nocal/tui/EventEditor.hpp"
|
||||
#include "nocal/tui/Screen.hpp"
|
||||
|
||||
@@ -21,7 +25,7 @@ enum class ExitReason {
|
||||
|
||||
// The Event vector is retained by reference so future editor commands can
|
||||
// update the caller's model without changing the application boundary.
|
||||
class TuiApp {
|
||||
class TuiApp : public sync::SyncObserver {
|
||||
public:
|
||||
using SaveCallback = std::function<void(std::span<const Event>)>;
|
||||
|
||||
@@ -57,7 +61,28 @@ public:
|
||||
return calendars_;
|
||||
}
|
||||
|
||||
void set_sync_provider(std::unique_ptr<sync::SyncProvider> provider) noexcept {
|
||||
sync_provider_ = std::move(provider);
|
||||
}
|
||||
void set_connected_account_id(std::string id) noexcept {
|
||||
connected_account_id_ = std::move(id);
|
||||
}
|
||||
|
||||
private:
|
||||
// Sync state (protected by sync_mutex_)
|
||||
std::mutex sync_mutex_;
|
||||
std::unique_ptr<sync::SyncProvider> sync_provider_;
|
||||
std::optional<std::string> connected_account_id_;
|
||||
std::optional<std::thread> sync_thread_;
|
||||
|
||||
// --- SyncObserver implementation (called from sync thread) ---
|
||||
void on_sync_progress(const sync::SyncProgressEvent& event) override;
|
||||
|
||||
void start_sync(std::string_view account_id);
|
||||
void stop_sync();
|
||||
void apply_sync_stage(const std::string& stage_text, bool is_error);
|
||||
void clear_sync_state();
|
||||
|
||||
struct HistorySnapshot {
|
||||
std::vector<Event> events;
|
||||
std::chrono::year_month visible_month;
|
||||
|
||||
Reference in New Issue
Block a user