import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'stream_recovery.dart'; final realtimeControllerProvider = ChangeNotifierProvider(( ref, ) { // ChangeNotifierProvider automatically disposes the notifier; no need // for ref.onDispose here — adding it causes a double-dispose assertion. return RealtimeController(); }); /// Per-channel realtime controller for UI skeleton indicators. /// /// Individual streams handle their own recovery via [StreamRecoveryWrapper]. /// This controller aggregates channel-level status so the UI can show /// per-channel skeleton shimmers (e.g. only the tasks list shimmers when /// the `tasks` channel is recovering, not the whole app). /// /// Coordinates: /// - Per-channel recovering state for pinpoint skeleton indicators /// - Auth token refreshes for realtime connections class RealtimeController extends ChangeNotifier { bool _disposed = false; /// Channels currently in a recovering/polling/stale state. final Set _recoveringChannels = {}; RealtimeController(); // ── Per-channel status ───────────────────────────────────────────────── /// Whether a specific channel is currently recovering. bool isChannelRecovering(String channel) => _recoveringChannels.contains(channel); /// Global flag: true if **any** channel is recovering. Useful for global /// indicators (e.g. dashboard) where per-channel granularity isn't needed. bool get isAnyStreamRecovering => _recoveringChannels.isNotEmpty; /// The set of channels currently recovering, for UI display. Set get recoveringChannels => Set.unmodifiable(_recoveringChannels); /// Mark a channel as recovering. Called by [StreamRecoveryWrapper] via its /// [ChannelStatusCallback]. void markChannelRecovering(String channel) { if (_disposed) return; if (_recoveringChannels.add(channel)) { notifyListeners(); } } /// Mark a channel as recovered. Called when realtime reconnects /// successfully. void markChannelRecovered(String channel) { if (_disposed) return; if (_recoveringChannels.remove(channel)) { notifyListeners(); } } /// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged]. /// /// Routes [StreamConnectionStatus] to the appropriate mark method: /// - `connected` — live stream; no indicator needed. /// - `polling` — REST fallback; data is fresh, no skeleton needed. /// - `offline` — device has no internet; Brick cache is active. The offline /// banner already communicates this state, so the reconnection skeleton /// should NOT show — it would be misleading while real data is on screen. /// - Everything else (`recovering`, `stale`, `failed`) — show skeleton. void handleChannelStatus(String channel, StreamConnectionStatus status) { if (status == StreamConnectionStatus.connected || status == StreamConnectionStatus.polling || status == StreamConnectionStatus.offline) { markChannelRecovered(channel); } else { markChannelRecovering(channel); } } // ── Legacy compat ───────────────────────────────────────────────────── /// @deprecated Use [markChannelRecovering] instead. void markStreamRecovering() { // Kept for backward compatibility; maps to a synthetic channel. markChannelRecovering('_global'); } /// @deprecated Use [markChannelRecovered] instead. void markStreamRecovered() { markChannelRecovered('_global'); } @override void dispose() { _disposed = true; super.dispose(); } }