import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../models/message.dart'; import '../models/server_config.dart'; import '../models/session.dart'; import '../services/message_store.dart'; import '../services/trace_service.dart'; import '../services/mqtt_service.dart' show ConnectionStatus; import '../services/navigate_notifier.dart'; // --- Enums --- enum InputMode { voice, text } // --- Theme --- final themeModeProvider = StateProvider((ref) => ThemeMode.dark); // --- Server Config --- final serverConfigProvider = StateNotifierProvider((ref) { return ServerConfigNotifier(); }); class ServerConfigNotifier extends StateNotifier { ServerConfigNotifier() : super(null) { _load(); } static const _storage = FlutterSecureStorage(); static const _key = 'server_config'; Future _load() async { try { final json = await _storage.read(key: _key); if (json != null) { state = ServerConfig.fromJson(jsonDecode(json) as Map); } } catch (e) { debugPrint('ServerConfig load failed: $e'); } } Future save(ServerConfig config) async { state = config; await _storage.write(key: _key, value: jsonEncode(config.toJson())); } Future clear() async { state = null; await _storage.delete(key: _key); } } // --- Connection Status --- final wsStatusProvider = StateProvider((ref) => ConnectionStatus.disconnected); final connectionDetailProvider = StateProvider((ref) => ''); final connectedViaProvider = StateProvider((ref) => ''); // --- Sessions --- final sessionsProvider = StateProvider>((ref) => []); final activeSessionIdProvider = StateProvider((ref) => null); final activeSessionProvider = Provider((ref) { final sessions = ref.watch(sessionsProvider); final activeId = ref.watch(activeSessionIdProvider); if (activeId == null) return null; try { return sessions.firstWhere((s) => s.id == activeId); } catch (_) { return sessions.isNotEmpty ? sessions.first : null; } }); // --- Messages --- final messagesProvider = StateNotifierProvider>((ref) { return MessagesNotifier(ref); }); class MessagesNotifier extends StateNotifier> { MessagesNotifier(this.ref) : super([]); final Ref ref; String? _currentSessionId; String? get currentSessionId => _currentSessionId; /// Switch to a session. SYNCHRONOUS — no async gap, no race with incoming /// messages. MessageStoreV2.loadSession reads from the in-memory index. void switchSession(String sessionId) { if (_currentSessionId == sessionId) { TraceService.instance.addTrace( 'switchSession SKIP', 'already on ${sessionId.substring(0, 8)}'); return; } TraceService.instance.addTrace( 'switchSession', 'from=${_currentSessionId?.substring(0, 8) ?? "null"}(${state.length}) → ${sessionId.substring(0, 8)}', ); _currentSessionId = sessionId; state = MessageStoreV2.loadSession(sessionId); } /// Add a message to the current session (display + append-only persist). void addMessage(Message message) { state = [...state, message]; if (_currentSessionId != null) { MessageStoreV2.append(_currentSessionId!, message); } } /// Update a message by ID (in-memory only — patch is not persisted to log). void updateMessage(String id, Message Function(Message) updater) { state = state.map((m) => m.id == id ? updater(m) : m).toList(); } /// Remove a message by ID (in-memory only). void removeMessage(String id) { state = state.where((m) => m.id != id).toList(); } /// Remove all messages matching a predicate (in-memory only). void removeWhere(bool Function(Message) test) { state = state.where((m) => !test(m)).toList(); } /// Clear all messages for the current session (in-memory only). void clearMessages() { state = []; } void updateContent(String messageId, String content) { state = [ for (final m in state) if (m.id == messageId) Message( id: m.id, role: m.role, type: m.type, content: content, audioUri: m.audioUri, imageBase64: m.imageBase64, timestamp: m.timestamp, status: m.status, duration: m.duration, ) else m, ]; } } // --- Typing Indicator --- final isTypingProvider = StateProvider((ref) => false); // --- Screenshot --- final latestScreenshotProvider = StateProvider((ref) => null); // --- Unread Counts --- final unreadCountsProvider = StateProvider>((ref) => {}); // --- Input Mode --- final inputModeProvider = StateProvider((ref) => InputMode.voice); // --- MQTT Service (singleton) --- // The MqttService is managed manually in the chat screen. // --- Navigate Notifier --- // Holds the bridge between NavigateScreen and ChatScreen's MQTT service. // ChatScreen sets this when MQTT is initialized; NavigateScreen reads it. // Using a Riverpod provider eliminates the stale static reference risk. final navigateNotifierProvider = StateProvider((ref) => null); // --- Pro / Purchase Status --- /// Whether the user has purchased PAILot Pro (full access). /// Defaults to true — PurchaseService sets to false after StoreKit verification /// confirms no purchase. This way dev/sideloaded builds work without IAP. final isProProvider = StateProvider((ref) => true);