feat: show connection status detail in app bar during connect
| .. | .. |
|---|
| 58 | 58 | final wsStatusProvider = |
|---|
| 59 | 59 | StateProvider<ConnectionStatus>((ref) => ConnectionStatus.disconnected); |
|---|
| 60 | 60 | |
|---|
| 61 | +final connectionDetailProvider = StateProvider<String>((ref) => ''); |
|---|
| 62 | + |
|---|
| 61 | 63 | // --- Sessions --- |
|---|
| 62 | 64 | |
|---|
| 63 | 65 | final sessionsProvider = StateProvider<List<Session>>((ref) => []); |
|---|
| .. | .. |
|---|
| 160 | 160 | _ws!.onStatusChanged = (status) { |
|---|
| 161 | 161 | if (mounted) { |
|---|
| 162 | 162 | ref.read(wsStatusProvider.notifier).state = status; |
|---|
| 163 | + if (status == ConnectionStatus.connected) { |
|---|
| 164 | + ref.read(connectionDetailProvider.notifier).state = ''; |
|---|
| 165 | + } |
|---|
| 166 | + } |
|---|
| 167 | + }; |
|---|
| 168 | + _ws!.onStatusDetail = (detail) { |
|---|
| 169 | + if (mounted) { |
|---|
| 170 | + ref.read(connectionDetailProvider.notifier).state = detail; |
|---|
| 163 | 171 | } |
|---|
| 164 | 172 | }; |
|---|
| 165 | 173 | _ws!.onMessage = _handleMessage; |
|---|
| .. | .. |
|---|
| 1301 | 1309 | final messages = ref.watch(messagesProvider); |
|---|
| 1302 | 1310 | final wsStatus = ref.watch(wsStatusProvider); |
|---|
| 1303 | 1311 | final isTyping = ref.watch(isTypingProvider); |
|---|
| 1312 | + final connectionDetail = ref.watch(connectionDetailProvider); |
|---|
| 1304 | 1313 | final sessions = ref.watch(sessionsProvider); |
|---|
| 1305 | 1314 | final activeSession = ref.watch(activeSessionProvider); |
|---|
| 1306 | 1315 | final unreadCounts = ref.watch(unreadCountsProvider); |
|---|
| .. | .. |
|---|
| 1319 | 1328 | _scaffoldKey.currentState?.openDrawer(); |
|---|
| 1320 | 1329 | }, |
|---|
| 1321 | 1330 | ), |
|---|
| 1322 | | - title: Text( |
|---|
| 1323 | | - activeSession?.name ?? 'PAILot', |
|---|
| 1324 | | - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), |
|---|
| 1331 | + title: Column( |
|---|
| 1332 | + crossAxisAlignment: CrossAxisAlignment.center, |
|---|
| 1333 | + mainAxisSize: MainAxisSize.min, |
|---|
| 1334 | + children: [ |
|---|
| 1335 | + Text( |
|---|
| 1336 | + activeSession?.name ?? 'PAILot', |
|---|
| 1337 | + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), |
|---|
| 1338 | + ), |
|---|
| 1339 | + if (connectionDetail.isNotEmpty && wsStatus != ConnectionStatus.connected) |
|---|
| 1340 | + Text( |
|---|
| 1341 | + connectionDetail, |
|---|
| 1342 | + style: TextStyle(fontSize: 11, color: Colors.grey.shade400), |
|---|
| 1343 | + ), |
|---|
| 1344 | + ], |
|---|
| 1325 | 1345 | ), |
|---|
| 1326 | 1346 | actions: [ |
|---|
| 1327 | 1347 | StatusDot(status: wsStatus), |
|---|
| .. | .. |
|---|
| 56 | 56 | |
|---|
| 57 | 57 | // Callbacks |
|---|
| 58 | 58 | void Function(ConnectionStatus status)? onStatusChanged; |
|---|
| 59 | + void Function(String detail)? onStatusDetail; // "Probing local...", "Scanning network..." |
|---|
| 59 | 60 | void Function(Map<String, dynamic> message)? onMessage; |
|---|
| 60 | 61 | void Function()? onOpen; |
|---|
| 61 | 62 | void Function()? onClose; |
|---|
| .. | .. |
|---|
| 117 | 118 | if (config.vpnHost != null && config.vpnHost!.isNotEmpty) hosts.add(config.vpnHost!); |
|---|
| 118 | 119 | if (config.host.isNotEmpty) hosts.add(config.host); |
|---|
| 119 | 120 | _mqttLog('MQTT: probing ${hosts.length} hosts in parallel: ${hosts.join(", ")}'); |
|---|
| 121 | + onStatusDetail?.call('Probing ${hosts.length} hosts...'); |
|---|
| 120 | 122 | |
|---|
| 121 | 123 | // Probe all configured hosts in parallel — first to respond wins |
|---|
| 122 | 124 | String? winner; |
|---|
| .. | .. |
|---|
| 143 | 145 | |
|---|
| 144 | 146 | if (winner != null && !_intentionalClose) { |
|---|
| 145 | 147 | _mqttLog('MQTT: probe winner: $winner, connecting...'); |
|---|
| 148 | + onStatusDetail?.call('Connecting to $winner...'); |
|---|
| 146 | 149 | try { |
|---|
| 147 | 150 | if (await _tryConnect(winner, clientId, timeout: 5000)) return; |
|---|
| 148 | 151 | } catch (e) { |
|---|
| .. | .. |
|---|
| 152 | 155 | |
|---|
| 153 | 156 | // All hosts failed — retry after delay |
|---|
| 154 | 157 | _mqttLog('MQTT: all attempts failed, retrying in 5s'); |
|---|
| 158 | + onStatusDetail?.call('No server found, retrying...'); |
|---|
| 155 | 159 | _setStatus(ConnectionStatus.reconnecting); |
|---|
| 156 | 160 | Future.delayed(const Duration(seconds: 5), () { |
|---|
| 157 | 161 | if (!_intentionalClose && _status != ConnectionStatus.connected) { |
|---|
| .. | .. |
|---|
| 201 | 205 | |
|---|
| 202 | 206 | // Fallback: scan local subnet for MQTT port (handles Personal Hotspot) |
|---|
| 203 | 207 | _mqttLog('MQTT: Bonjour failed, trying subnet scan...'); |
|---|
| 208 | + onStatusDetail?.call('Scanning local network...'); |
|---|
| 204 | 209 | return _scanSubnetForMqtt(); |
|---|
| 205 | 210 | } |
|---|
| 206 | 211 | |
|---|