feat: enhanced sockets, tuned retries and polling fallback

This commit is contained in:
cogwheel0
2025-09-07 11:13:05 +05:30
parent 3decf9d46b
commit a16fb86e27
11 changed files with 519 additions and 138 deletions

View File

@@ -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

View File

@@ -196,11 +196,16 @@ final socketServiceProvider = Provider<SocketService?>((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();

View File

@@ -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<void>((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<AuthNavigationState>(authNavigationStateProvider,
(prev, next) {
@@ -67,6 +75,128 @@ final appStartupFlowProvider = Provider<void>((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<void>((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<void>((ref) {
final observer = _SocketPersistenceObserver(ref);
WidgetsBinding.instance.addObserver(observer);
// React to active conversation changes while backgrounded
final sub = ref.listen<Conversation?>(
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<void> _maybeShowOnboarding(Ref ref) async {
try {
final storage = ref.read(optimizedStorageServiceProvider);

View File

@@ -2907,7 +2907,8 @@ class ApiService {
bool containsDone(String s) =>
s.contains('<details type="tool_calls"') && s.contains('done="true"');
while (DateTime.now().difference(started).inSeconds < 60) {
// Allow longer time for large completions (e.g., long stories)
while (DateTime.now().difference(started).inSeconds < 180) {
try {
// Small delay between polls
await Future.delayed(const Duration(milliseconds: 900));
@@ -3069,14 +3070,15 @@ class ApiService {
break;
}
// If content hasn't changed for a few polls, assume completion
// If content hasn't changed for a few polls, assume completion,
// but do not early-exit while content is still empty.
final prev = last;
if (content == prev) {
if (content == prev && content.isNotEmpty) {
stableCount++;
} else {
} else if (content != prev) {
stableCount = 0;
}
if (stableCount >= 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, dynamic>;
String content = '';
Map<String, dynamic>? chatObj = (data['chat'] is Map<String, dynamic>)
? data['chat'] as Map<String, dynamic>
: 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<String, dynamic> messagesMap =
(history['messages'] as Map).cast<String, dynamic>();
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();
}

View File

@@ -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<bool> 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<String> getSocketTransportMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_socketTransportModeKey) ?? 'auto';
}
static Future<void> 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<AppSettings> {
await SettingsService.setVoiceAutoSendFinal(value);
}
Future<void> setSocketTransportMode(String mode) async {
state = state.copyWith(socketTransportMode: mode);
await SettingsService.setSocketTransportMode(mode);
}
Future<void> resetToDefaults() async {
const defaultSettings = AppSettings();
await SettingsService.saveSettings(defaultSettings);

View File

@@ -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',
}
: {},
final builder = io.OptionBuilder()
// Transport selection
.setTransports(
websocketOnly ? ['websocket'] : ['polling', 'websocket'],
)
.build(),
);
.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');
});

View File

@@ -55,10 +55,27 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
// 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;

View File

@@ -1409,15 +1409,20 @@ class _ChatPageState extends ConsumerState<ChatPage> {
final full = await api.getConversation(active.id);
ref
.read(activeConversationProvider.notifier)
.state =
full;
.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));
},

View File

@@ -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<String>(
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,
),
),
),
],
),
),
],
),
),

View File

@@ -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"

View File

@@ -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: