diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 57f9f17..ac856b9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -45,7 +45,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - record_ios (1.0.0): + - record_ios (1.1.0): - Flutter - SDWebImage (5.21.1): - SDWebImage/Core (= 5.21.1) @@ -143,7 +143,7 @@ SPEC CHECKSUMS: image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b + record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 SDWebImage: f29024626962457f3470184232766516dee8dfea share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 7c30c1e..12a2252 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -196,11 +196,16 @@ final socketServiceProvider = Provider((ref) { final activeServer = ref.watch(activeServerProvider); final token = ref.watch(authTokenProvider3); + final transportMode = ref.watch(appSettingsProvider).socketTransportMode; // 'auto' or 'ws' return activeServer.maybeWhen( data: (server) { if (server == null) return null; - final s = SocketService(serverConfig: server, authToken: token); + final s = SocketService( + serverConfig: server, + authToken: token, + websocketOnly: transportMode == 'ws', + ); // best-effort connect; errors handled internally // ignore unawaited_futures s.connect(); diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index 96f2c36..233c588 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/app_providers.dart'; import '../../features/auth/providers/unified_auth_providers.dart'; import '../services/navigation_service.dart'; +import '../models/conversation.dart'; +import '../services/background_streaming_handler.dart'; import '../../features/onboarding/views/onboarding_sheet.dart'; import '../../shared/theme/theme_extensions.dart'; import '../utils/debug_logger.dart'; @@ -28,6 +30,12 @@ final appStartupFlowProvider = Provider((ref) { ref.watch(socketServiceProvider); } + // Ensure resume-triggered foreground refresh is active + ref.watch(foregroundRefreshProvider); + + // Keep Socket.IO connection alive in background within platform limits + ref.watch(socketPersistenceProvider); + // When auth state becomes authenticated, run additional background work ref.listen(authNavigationStateProvider, (prev, next) { @@ -67,6 +75,128 @@ final appStartupFlowProvider = Provider((ref) { }); }); +/// Listens to app lifecycle and refreshes server state when app returns to foreground. +/// +/// Rationale: Socket.IO does not replay historical events. If the app was suspended, +/// we may miss updates. On resume, invalidate conversations to reconcile state. +final foregroundRefreshProvider = Provider((ref) { + final observer = _ForegroundRefreshObserver(ref); + WidgetsBinding.instance.addObserver(observer); + ref.onDispose(() => WidgetsBinding.instance.removeObserver(observer)); +}); + +class _ForegroundRefreshObserver extends WidgetsBindingObserver { + final Ref _ref; + _ForegroundRefreshObserver(this._ref); + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // Schedule to avoid side-effects during build frames + Future.microtask(() { + try { + _ref.invalidate(conversationsProvider); + } catch (_) {} + }); + } + } +} + +/// Attempts to keep the realtime socket connection alive while the app is +/// backgrounded, similar to how PersistentStreamingService works for streams. +/// +/// Notes: +/// - iOS: limited to short background task windows; we send periodic keepAlive. +/// - Android: uses existing foreground service notification. +final socketPersistenceProvider = Provider((ref) { + final observer = _SocketPersistenceObserver(ref); + WidgetsBinding.instance.addObserver(observer); + // React to active conversation changes while backgrounded + final sub = ref.listen( + activeConversationProvider, + (prev, next) => observer.onActiveConversationChanged(), + ); + ref.onDispose(() => WidgetsBinding.instance.removeObserver(observer)); + ref.onDispose(sub.close); +}); + +class _SocketPersistenceObserver extends WidgetsBindingObserver { + final Ref _ref; + _SocketPersistenceObserver(this._ref); + + static const String _socketId = 'socket-keepalive'; + Timer? _heartbeat; + bool _bgActive = false; + bool _isBackgrounded = false; + + bool _shouldKeepAlive() { + final authed = + _ref.read(authNavigationStateProvider) == + AuthNavigationState.authenticated; + final hasConversation = _ref.read(activeConversationProvider) != null; + return authed && hasConversation; + } + + void _startBackground() { + if (_bgActive) return; + if (!_shouldKeepAlive()) return; + try { + BackgroundStreamingHandler.instance + .startBackgroundExecution([_socketId]); + // Periodic keep-alive (primarily useful on iOS) + _heartbeat?.cancel(); + _heartbeat = + Timer.periodic(const Duration(seconds: 30), (_) async { + try { + await BackgroundStreamingHandler.instance.keepAlive(); + } catch (_) {} + }); + _bgActive = true; + } catch (_) {} + } + + void _stopBackground() { + if (!_bgActive) return; + try { + BackgroundStreamingHandler.instance + .stopBackgroundExecution([_socketId]); + } catch (_) {} + _heartbeat?.cancel(); + _heartbeat = null; + _bgActive = false; + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.paused: + case AppLifecycleState.inactive: + _isBackgrounded = true; + _startBackground(); + break; + case AppLifecycleState.resumed: + _isBackgrounded = false; + _stopBackground(); + break; + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + _isBackgrounded = false; + _stopBackground(); + break; + } + } + + // Called when active conversation changes; only acts during background + void onActiveConversationChanged() { + if (!_isBackgrounded) return; + if (_shouldKeepAlive()) { + _startBackground(); + } else { + _stopBackground(); + } + } +} + Future _maybeShowOnboarding(Ref ref) async { try { final storage = ref.read(optimizedStorageServiceProvider); diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 258f964..f06f76a 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -2907,7 +2907,8 @@ class ApiService { bool containsDone(String s) => s.contains('
= 3) { + if (content.isNotEmpty && stableCount >= 3) { break; } @@ -3086,6 +3088,66 @@ class ApiService { } } + // Final backfill: one last attempt to fetch the latest content + // in case the server wrote the final message after our last poll. + try { + if (!streamController.isClosed) { + final resp = await _dio.get('/api/v1/chats/$chatId'); + final data = resp.data as Map; + String content = ''; + Map? chatObj = (data['chat'] is Map) + ? data['chat'] as Map + : null; + if (chatObj != null && chatObj['messages'] is List) { + final List messagesList = chatObj['messages'] as List; + final target = messagesList.firstWhere( + (m) => (m is Map && (m['id']?.toString() == messageId)), + orElse: () => null, + ); + if (target != null) { + final rawContent = (target as Map)['content']; + if (rawContent is String) { + content = rawContent; + } else if (rawContent is List) { + final textItem = rawContent.firstWhere( + (i) => i is Map && i['type'] == 'text', + orElse: () => null, + ); + if (textItem != null) { + content = (textItem as Map)['text']?.toString() ?? ''; + } + } + } + } + if (content.isEmpty && chatObj != null) { + final history = chatObj['history']; + if (history is Map && history['messages'] is Map) { + final Map messagesMap = + (history['messages'] as Map).cast(); + final msg = messagesMap[messageId]; + if (msg is Map) { + final rawContent = msg['content']; + if (rawContent is String) { + content = rawContent; + } else if (rawContent is List) { + final textItem = rawContent.firstWhere( + (i) => i is Map && i['type'] == 'text', + orElse: () => null, + ); + if (textItem != null) { + content = (textItem as Map)['text']?.toString() ?? ''; + } + } + } + } + } + if (content.isNotEmpty && content != last) { + streamController.add('\n'); + streamController.add(content); + } + } + } catch (_) {} + if (!streamController.isClosed) { streamController.close(); } diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index aeda7c5..36747ae 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -19,6 +19,8 @@ class SettingsService { static const String _voiceLocaleKey = 'voice_locale_id'; static const String _voiceHoldToTalkKey = 'voice_hold_to_talk'; static const String _voiceAutoSendKey = 'voice_auto_send_final'; + // Realtime transport preference + static const String _socketTransportModeKey = 'socket_transport_mode'; // 'auto' or 'ws' /// Get reduced motion preference static Future getReduceMotion() async { @@ -133,6 +135,7 @@ class SettingsService { voiceLocaleId: await getVoiceLocaleId(), voiceHoldToTalk: await getVoiceHoldToTalk(), voiceAutoSendFinal: await getVoiceAutoSendFinal(), + socketTransportMode: await getSocketTransportMode(), ); } @@ -150,6 +153,7 @@ class SettingsService { setVoiceLocaleId(settings.voiceLocaleId), setVoiceHoldToTalk(settings.voiceHoldToTalk), setVoiceAutoSendFinal(settings.voiceAutoSendFinal), + setSocketTransportMode(settings.socketTransportMode), ]); } @@ -188,6 +192,18 @@ class SettingsService { await prefs.setBool(_voiceAutoSendKey, value); } + /// Transport mode: 'auto' (polling+websocket) or 'ws' (websocket only) + static Future getSocketTransportMode() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_socketTransportModeKey) ?? 'auto'; + } + + static Future setSocketTransportMode(String mode) async { + final prefs = await SharedPreferences.getInstance(); + if (mode != 'auto' && mode != 'ws') mode = 'auto'; + await prefs.setString(_socketTransportModeKey, mode); + } + /// Get effective animation duration considering all settings static Duration getEffectiveAnimationDuration( BuildContext context, @@ -241,6 +257,7 @@ class AppSettings { final String? voiceLocaleId; final bool voiceHoldToTalk; final bool voiceAutoSendFinal; + final String socketTransportMode; // 'auto' or 'ws' const AppSettings({ this.reduceMotion = false, @@ -254,6 +271,7 @@ class AppSettings { this.voiceLocaleId, this.voiceHoldToTalk = false, this.voiceAutoSendFinal = false, + this.socketTransportMode = 'auto', }); AppSettings copyWith({ @@ -268,6 +286,7 @@ class AppSettings { Object? voiceLocaleId = const _DefaultValue(), bool? voiceHoldToTalk, bool? voiceAutoSendFinal, + String? socketTransportMode, }) { return AppSettings( reduceMotion: reduceMotion ?? this.reduceMotion, @@ -281,6 +300,7 @@ class AppSettings { voiceLocaleId: voiceLocaleId is _DefaultValue ? this.voiceLocaleId : voiceLocaleId as String?, voiceHoldToTalk: voiceHoldToTalk ?? this.voiceHoldToTalk, voiceAutoSendFinal: voiceAutoSendFinal ?? this.voiceAutoSendFinal, + socketTransportMode: socketTransportMode ?? this.socketTransportMode, ); } @@ -299,6 +319,7 @@ class AppSettings { other.voiceLocaleId == voiceLocaleId && other.voiceHoldToTalk == voiceHoldToTalk && other.voiceAutoSendFinal == voiceAutoSendFinal; + // socketTransportMode intentionally not included in == to avoid frequent rebuilds } @override @@ -315,6 +336,7 @@ class AppSettings { voiceLocaleId, voiceHoldToTalk, voiceAutoSendFinal, + socketTransportMode, ); } } @@ -390,6 +412,11 @@ class AppSettingsNotifier extends StateNotifier { await SettingsService.setVoiceAutoSendFinal(value); } + Future setSocketTransportMode(String mode) async { + state = state.copyWith(socketTransportMode: mode); + await SettingsService.setSocketTransportMode(mode); + } + Future resetToDefaults() async { const defaultSettings = AppSettings(); await SettingsService.saveSettings(defaultSettings); diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index 7c79ef6..a413f6f 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -5,9 +5,14 @@ import '../models/server_config.dart'; class SocketService { final ServerConfig serverConfig; final String? authToken; + final bool websocketOnly; io.Socket? _socket; - SocketService({required this.serverConfig, required this.authToken}); + SocketService({ + required this.serverConfig, + required this.authToken, + this.websocketOnly = false, + }); String? get sessionId => _socket?.id; io.Socket? get socket => _socket; @@ -24,20 +29,28 @@ class SocketService { final base = serverConfig.url.replaceFirst(RegExp(r'/+$'), ''); final path = '/ws/socket.io'; - _socket = io.io( - base, - io.OptionBuilder() - .setTransports(['websocket']) - .setPath(path) - .setExtraHeaders( - authToken != null && authToken!.isNotEmpty - ? { - 'Authorization': 'Bearer $authToken', - } - : {}, - ) - .build(), - ); + final builder = io.OptionBuilder() + // Transport selection + .setTransports( + websocketOnly ? ['websocket'] : ['polling', 'websocket'], + ) + .setRememberUpgrade(!websocketOnly) + .setUpgrade(!websocketOnly) + // Tune reconnect/backoff and timeouts + .setReconnectionAttempts(0) // 0/Infinity semantics: unlimited attempts + .setReconnectionDelay(1000) + .setReconnectionDelayMax(5000) + .setRandomizationFactor(0.5) + .setTimeout(20000) + .setPath(path); + + if (authToken != null && authToken!.isNotEmpty) { + builder + .setAuth({'token': authToken}) + .setExtraHeaders({'Authorization': 'Bearer $authToken'}); + } + + _socket = io.io(base, builder.build()); _socket!.on('connect', (_) { debugPrint('Socket connected: ${_socket!.id}'); @@ -52,6 +65,24 @@ class SocketService { debugPrint('Socket connect_error: $err'); }); + _socket!.on('reconnect_attempt', (attempt) { + debugPrint('Socket reconnect_attempt: $attempt'); + }); + + _socket!.on('reconnect', (attempt) { + debugPrint('Socket reconnected after $attempt attempts'); + if (authToken != null && authToken!.isNotEmpty) { + // Best-effort rejoin + _socket!.emit('user-join', { + 'auth': {'token': authToken} + }); + } + }); + + _socket!.on('reconnect_failed', (_) { + debugPrint('Socket reconnect_failed'); + }); + _socket!.on('disconnect', (reason) { debugPrint('Socket disconnected: $reason'); }); diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index 40b871e..2d3197b 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -55,10 +55,27 @@ class ChatMessagesNotifier extends StateNotifier> { // locally streamed assistant content with an outdated server copy. if (previous?.updatedAt != next?.updatedAt) { final serverMessages = next?.messages ?? const []; - // Only replace local messages if the server has strictly more messages - // (i.e., includes new content we don't have yet). + // Primary rule: adopt server messages when there are strictly more of them. if (serverMessages.length > state.length) { state = serverMessages; + return; + } + + // Secondary rule: if counts are equal but the last assistant message grew, + // adopt the server copy to recover from missed socket events. + if (serverMessages.isNotEmpty && state.isNotEmpty) { + final serverLast = serverMessages.last; + final localLast = state.last; + final serverText = serverLast.content.trim(); + final localText = localLast.content.trim(); + final sameLastId = serverLast.id == localLast.id; + final isAssistant = serverLast.role == 'assistant'; + final serverHasMore = serverText.isNotEmpty && serverText.length > localText.length; + final localEmptyButServerHas = localText.isEmpty && serverText.isNotEmpty; + if (sameLastId && isAssistant && (serverHasMore || localEmptyButServerHas)) { + state = serverMessages; + return; + } } } return; diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index f9ea69c..3d74396 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1408,16 +1408,21 @@ class _ChatPageState extends ConsumerState { try { final full = await api.getConversation(active.id); ref - .read(activeConversationProvider.notifier) - .state = - full; + .read(activeConversationProvider.notifier) + .state = full; } catch (e) { - debugPrint( - 'DEBUG: Failed to refresh conversation: $e', - ); - // Could show a snackbar here if needed + debugPrint('DEBUG: Failed to refresh conversation: $e'); } } + + // Also refresh the conversations list to reconcile missed events + // and keep timestamps/order in sync with the server. + try { + ref.invalidate(conversationsProvider); + // Best-effort await to stabilize UI; ignore errors. + await ref.read(conversationsProvider.future); + } catch (_) {} + // Add small delay for better UX feedback await Future.delayed(const Duration(milliseconds: 300)); }, diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index 0851241..02fd0d8 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -41,7 +41,7 @@ class AppCustomizationPage extends ConsumerWidget { ), centerTitle: true, ), - body: Padding( + body: SingleChildScrollView( padding: const EdgeInsets.all(Spacing.pagePadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -102,6 +102,110 @@ class AppCustomizationPage extends ConsumerWidget { ], ), ), + + const SizedBox(height: Spacing.lg), + Text( + 'Realtime', + style: context.conduitTheme.headingSmall?.copyWith( + color: context.conduitTheme.textPrimary, + ), + ), + const SizedBox(height: Spacing.md), + ConduitCard( + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.listItemPadding, + vertical: Spacing.sm, + ), + leading: Container( + padding: const EdgeInsets.all(Spacing.sm), + decoration: BoxDecoration( + color: context.conduitTheme.buttonPrimary + .withValues(alpha: Alpha.highlight), + borderRadius: + BorderRadius.circular(AppBorderRadius.small), + ), + child: Icon( + Platform.isIOS + ? CupertinoIcons.waveform + : Icons.sync_alt, + color: context.conduitTheme.buttonPrimary, + size: IconSize.medium, + ), + ), + title: Text( + 'Transport mode', + style: context.conduitTheme.bodyLarge?.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + 'Choose how the app connects for realtime updates.', + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.listItemPadding, + 0, + Spacing.listItemPadding, + Spacing.md, + ), + child: DropdownButtonFormField( + initialValue: settings.socketTransportMode, + onChanged: (v) async { + if (v == null) return; + await ref + .read(appSettingsProvider.notifier) + .setSocketTransportMode(v); + }, + items: const [ + DropdownMenuItem( + value: 'auto', + child: Text('Auto (Polling + WebSocket)'), + ), + DropdownMenuItem( + value: 'ws', + child: Text('WebSocket only'), + ), + ], + decoration: const InputDecoration( + labelText: 'Mode', + border: OutlineInputBorder(), + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.listItemPadding, + 0, + Spacing.listItemPadding, + Spacing.md, + ), + child: Text( + settings.socketTransportMode == 'auto' + ? 'More robust on restrictive networks. Upgrades to WebSocket when possible.' + : 'Lower overhead, but may fail behind strict proxies/firewalls.', + style: context.conduitTheme.caption?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ), + ], + ), + ), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index f12db3e..e7d3157 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: analyzer - sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" url: "https://pub.dev" source: hosted - version: "7.6.0" + version: "7.7.1" ansicolor: dependency: transitive description: @@ -61,18 +61,18 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "3.1.0" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -85,26 +85,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "3.0.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.7.1" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b" url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "9.3.1" built_collection: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62" + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d url: "https://pub.dev" source: hosted - version: "8.11.0" + version: "8.12.0" cached_network_image: dependency: "direct main" description: @@ -245,10 +245,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.dev" source: hosted - version: "5.8.0+1" + version: "5.9.0" dio_web_adapter: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "13ba4e627ef24503a465d1d61b32596ce10eb6b8903678d362a528f9939b4aa8" + sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22 url: "https://pub.dev" source: hosted - version: "10.2.1" + version: "10.3.2" file_selector_linux: dependency: transitive description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" url: "https://pub.dev" source: hosted - version: "0.9.4+3" + version: "0.9.4+4" file_selector_platform_interface: dependency: transitive description: @@ -383,10 +383,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 url: "https://pub.dev" source: hosted - version: "2.0.28" + version: "2.0.30" flutter_riverpod: dependency: "direct main" description: @@ -465,10 +465,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22" + sha256: da32f8ba8cfcd4ec71d9decc8cbf28bd2c31b5283d9887eb51eb4a0659d8110c url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" freezed_annotation: dependency: "direct main" description: @@ -521,10 +521,10 @@ packages: dependency: transitive description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" http_multi_server: dependency: transitive description: @@ -553,66 +553,66 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" + sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" url: "https://pub.dev" source: hosted - version: "0.8.12+24" + version: "0.8.13+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e url: "https://pub.dev" source: hosted - version: "0.8.12+2" + version: "0.8.13" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" url: "https://pub.dev" source: hosted - version: "0.2.1+2" + version: "0.2.2" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 url: "https://pub.dev" source: hosted - version: "0.2.1+2" + version: "0.2.2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" url: "https://pub.dev" source: hosted - version: "2.10.1" + version: "2.11.0" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.2" intl: dependency: "direct main" description: @@ -649,10 +649,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe" url: "https://pub.dev" source: hosted - version: "6.9.5" + version: "6.11.1" leak_tracker: dependency: transitive description: @@ -761,18 +761,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "8.3.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" path: dependency: "direct main" description: @@ -793,18 +793,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" url: "https://pub.dev" source: hosted - version: "2.2.17" + version: "2.2.18" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -833,10 +833,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -889,66 +889,66 @@ packages: dependency: "direct main" description: name: record - sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817 + sha256: "9dbc6ff3e784612f90a9b001373c45ff76b7a08abd2bd9fdf72c242320c8911c" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.1" record_android: dependency: transitive description: name: record_android - sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b" + sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.4.1" record_ios: dependency: transitive description: name: record_ios - sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb" + sha256: "13e241ed9cbc220534a40ae6b66222e21288db364d96dd66fb762ebd3cb77c71" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.2" record_linux: dependency: transitive description: name: record_linux - sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95" + sha256: "235b1f1fb84e810f8149cc0c2c731d7d697f8d1c333b32cb820c449bf7bb72d8" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.1" record_macos: dependency: transitive description: name: record_macos - sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd" + sha256: "2849068bb59072f300ad63ed146e543d66afaef8263edba4de4834fc7c8d4d35" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" record_platform_interface: dependency: transitive description: name: record_platform_interface - sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89 + sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" record_web: dependency: transitive description: name: record_web - sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb + sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56" url: "https://pub.dev" source: hosted - version: "1.1.9" + version: "1.2.0" record_windows: dependency: transitive description: name: record_windows - sha256: "85a22fc97f6d73ecd67c8ba5f2f472b74ef1d906f795b7970f771a0914167e99" + sha256: "223258060a1d25c62bae18282c16783f28581ec19401d17e56b5205b9f039d78" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" riverpod: dependency: transitive description: @@ -1033,10 +1033,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.12" shared_preferences_foundation: dependency: transitive description: @@ -1102,34 +1102,34 @@ packages: dependency: "direct main" description: name: socket_io_client - sha256: ede469f3e4c55e8528b4e023bdedbc20832e8811ab9b61679d1ba3ed5f01f23b + sha256: c8471c2c6843cf308a5532ff653f2bcdb7fa9ae79d84d1179920578a06624f0d url: "https://pub.dev" source: hosted - version: "2.0.3+1" + version: "3.1.2" socket_io_common: dependency: transitive description: name: socket_io_common - sha256: "2ab92f8ff3ebbd4b353bf4a98bee45cc157e3255464b2f90f66e09c4472047eb" + sha256: "162fbaecbf4bf9a9372a62a341b3550b51dcef2f02f3e5830a297fd48203d45b" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.1.1" source_gen: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.1.0" source_helper: dependency: transitive description: name: source_helper - sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.dev" source: hosted - version: "1.3.6" + version: "1.3.8" source_span: dependency: transitive description: @@ -1158,10 +1158,10 @@ packages: dependency: transitive description: name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2+2" sqflite_common: dependency: transitive description: @@ -1310,18 +1310,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" + sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" url: "https://pub.dev" source: hosted - version: "6.3.17" + version: "6.3.18" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.4" url_launcher_linux: dependency: transitive description: @@ -1334,10 +1334,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" url_launcher_platform_interface: dependency: transitive description: @@ -1390,10 +1390,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" wakelock_plus: dependency: "direct main" description: @@ -1414,10 +1414,10 @@ packages: dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" web: dependency: transitive description: @@ -1462,10 +1462,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: "direct main" description: @@ -1475,5 +1475,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index e15b12a..d030e52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: flutter_riverpod: ^2.6.1 # Network & API - dio: ^5.5.0 + dio: ^5.9.0 http_parser: ^4.0.2 @@ -28,7 +28,7 @@ dependencies: markdown_widget: ^2.3.2+8 flutter_highlight: ^0.7.0 cached_network_image: ^3.3.1 - socket_io_client: ^2.0.3 + socket_io_client: ^3.1.2 yaml: ^3.1.2 @@ -37,17 +37,17 @@ dependencies: flutter_animate: ^4.5.0 # Platform Features - record: ^6.0.0 + record: ^6.1.1 stts: ^1.2.5 - image_picker: ^1.1.2 - file_picker: ^10.2.1 + image_picker: ^1.2.0 + file_picker: ^10.3.2 path_provider: ^2.1.4 # Utilities path: ^1.9.0 uuid: ^4.5.0 crypto: ^3.0.3 - package_info_plus: ^8.0.2 + package_info_plus: ^8.3.1 url_launcher: ^6.3.0 intl: ^0.20.2 @@ -65,9 +65,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - build_runner: ^2.4.11 - freezed: ^3.0.0 - json_serializable: ^6.8.0 + build_runner: ^2.7.1 + freezed: ^3.2.0 + json_serializable: ^6.11.1 flutter_native_splash: ^2.4.6 dependency_overrides: