import 'dart:convert'; 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/websocket_service.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 (_) {} } 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); // --- 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 new session and load its messages. Future switchSession(String sessionId) async { // Save current session before switching if (_currentSessionId != null && state.isNotEmpty) { MessageStore.save(_currentSessionId!, state); } _currentSessionId = sessionId; final messages = await MessageStore.loadAll(sessionId); state = messages; } /// Add a message to the current session. void addMessage(Message message) { state = [...state, message]; if (_currentSessionId != null) { MessageStore.save(_currentSessionId!, state); } } /// Update a message by ID. void updateMessage(String id, Message Function(Message) updater) { state = state.map((m) => m.id == id ? updater(m) : m).toList(); if (_currentSessionId != null) { MessageStore.save(_currentSessionId!, state); } } /// Remove a message by ID. void removeMessage(String id) { state = state.where((m) => m.id != id).toList(); if (_currentSessionId != null) { MessageStore.save(_currentSessionId!, state); } } /// Remove all messages matching a predicate. void removeWhere(bool Function(Message) test) { state = state.where((m) => !test(m)).toList(); if (_currentSessionId != null) { MessageStore.save(_currentSessionId!, state); } } /// Clear all messages for the current session. void clearMessages() { state = []; if (_currentSessionId != null) { MessageStore.save(_currentSessionId!, 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, ]; if (_currentSessionId != null) { MessageStore.save(_currentSessionId!, state); } } /// Load more (older) messages for pagination. Future loadMore() async { if (_currentSessionId == null) return; final older = await MessageStore.load( _currentSessionId!, offset: state.length, limit: 50, ); if (older.isNotEmpty) { state = [...older, ...state]; } } } // --- 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); // --- WebSocket Service (singleton) --- final webSocketServiceProvider = Provider((ref) { // This is managed manually in the chat screen return null; });