From a00d64fc26da942473eab4f6f58c1defbc4cd577 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:32:59 +0530 Subject: [PATCH] feat(transport): Improve socket transport mode selection and localization --- lib/core/models/backend_config.dart | 53 +++++ lib/core/providers/app_providers.dart | 85 +++++++- lib/core/services/api_service.dart | 39 ++++ lib/core/services/settings_service.dart | 39 +++- lib/core/services/socket_service.dart | 17 +- .../profile/views/app_customization_page.dart | 186 ++++++++++++++++++ lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 8 +- lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 12 +- lib/l10n/app_localizations_de.dart | 6 +- lib/l10n/app_localizations_en.dart | 6 +- lib/l10n/app_localizations_fr.dart | 6 +- lib/l10n/app_localizations_it.dart | 6 +- lib/l10n/app_nl.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_zh.arb | 4 +- 19 files changed, 441 insertions(+), 50 deletions(-) create mode 100644 lib/core/models/backend_config.dart diff --git a/lib/core/models/backend_config.dart b/lib/core/models/backend_config.dart new file mode 100644 index 0000000..3c04a21 --- /dev/null +++ b/lib/core/models/backend_config.dart @@ -0,0 +1,53 @@ +import 'package:flutter/foundation.dart'; + +/// Subset of the backend `/api/config` response the app cares about. +@immutable +class BackendConfig { + const BackendConfig({this.enableWebsocket}); + + /// Mirrors `features.enable_websocket` from OpenWebUI. + final bool? enableWebsocket; + + /// Returns a copy with updated fields. + BackendConfig copyWith({bool? enableWebsocket}) { + return BackendConfig( + enableWebsocket: enableWebsocket ?? this.enableWebsocket, + ); + } + + /// Whether the backend only allows WebSocket transport. + bool get websocketOnly => enableWebsocket == true; + + /// Whether the backend only allows HTTP polling transport. + bool get pollingOnly => enableWebsocket == false; + + /// Whether the backend permits choosing WebSocket-only mode. + bool get supportsWebsocketOnly => !pollingOnly; + + /// Whether the backend permits choosing polling fallback. + bool get supportsPolling => !websocketOnly; + + /// Returns the enforced transport mode derived from backend policy. + String? get enforcedTransportMode { + if (websocketOnly) return 'ws'; + if (pollingOnly) return 'polling'; + return null; + } + + Map toJson() { + return {'enable_websocket': enableWebsocket}; + } + + static BackendConfig fromJson(Map json) { + bool? enableWebsocket; + final features = json['features']; + if (features is Map) { + final value = features['enable_websocket']; + if (value is bool) { + enableWebsocket = value; + } + } + + return BackendConfig(enableWebsocket: enableWebsocket); + } +} diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 8ba3794..6687d5a 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -15,6 +15,7 @@ import '../models/user.dart'; import '../models/model.dart'; import '../models/conversation.dart'; import '../models/chat_message.dart'; +import '../models/backend_config.dart'; import '../models/folder.dart'; import '../models/user_settings.dart'; import '../models/file_info.dart'; @@ -172,6 +173,84 @@ final serverConnectionStateProvider = Provider((ref) { ); }); +final backendConfigProvider = FutureProvider((ref) async { + final api = ref.watch(apiServiceProvider); + if (api == null) { + return null; + } + + final server = await ref.watch(activeServerProvider.future); + if (server == null) { + return null; + } + + try { + final config = await api.getBackendConfig(); + if (config != null) { + final forcedMode = config.enforcedTransportMode; + if (forcedMode != null) { + final settings = ref.read(appSettingsProvider); + if (settings.socketTransportMode != forcedMode) { + Future.microtask(() { + ref + .read(appSettingsProvider.notifier) + .setSocketTransportMode(forcedMode); + }); + } + } + } + return config; + } catch (_) { + return null; + } +}); + +class SocketTransportAvailability { + const SocketTransportAvailability({ + required this.allowPolling, + required this.allowWebsocketOnly, + }); + + final bool allowPolling; + final bool allowWebsocketOnly; +} + +final socketTransportOptionsProvider = Provider(( + ref, +) { + final backendConfigAsync = ref.watch(backendConfigProvider); + final config = backendConfigAsync.maybeWhen( + data: (value) => value, + orElse: () => null, + ); + + if (config == null) { + return const SocketTransportAvailability( + allowPolling: true, + allowWebsocketOnly: true, + ); + } + + if (config.websocketOnly) { + return const SocketTransportAvailability( + allowPolling: false, + allowWebsocketOnly: true, + ); + } + + if (config.pollingOnly) { + return const SocketTransportAvailability( + allowPolling: true, + allowWebsocketOnly: false, + ); + } + + return const SocketTransportAvailability( + allowPolling: true, + allowWebsocketOnly: true, + ); +}); + // API Service provider with unified auth integration final apiServiceProvider = Provider((ref) { // If reviewer mode is enabled, skip creating ApiService @@ -242,6 +321,8 @@ class SocketServiceManager extends _$SocketServiceManager { appSettingsProvider.select((settings) => settings.socketTransportMode), ); final websocketOnly = transportMode == 'ws'; + final transportAvailability = ref.watch(socketTransportOptionsProvider); + final allowWebsocketUpgrade = transportAvailability.allowWebsocketOnly; // Don't watch authTokenProvider3 here to avoid rebuilding on token changes // Token updates are handled via the subscription below @@ -250,13 +331,15 @@ class SocketServiceManager extends _$SocketServiceManager { final requiresNewService = _service == null || _service!.serverConfig.id != server.id || - _service!.websocketOnly != websocketOnly; + _service!.websocketOnly != websocketOnly || + _service!.allowWebsocketUpgrade != allowWebsocketUpgrade; if (requiresNewService) { _disposeService(); _service = SocketService( serverConfig: server, authToken: token, websocketOnly: websocketOnly, + allowWebsocketUpgrade: allowWebsocketUpgrade, ); _scheduleConnect(_service!); } else { diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index a92a254..f43faa4 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; // import 'package:http_parser/http_parser.dart'; // Removed legacy websocket/socket.io imports import 'package:uuid/uuid.dart'; +import '../models/backend_config.dart'; import '../models/server_config.dart'; import '../models/user.dart'; import '../models/model.dart'; @@ -253,6 +254,44 @@ class ApiService { return result; } + Future getBackendConfig() async { + try { + final response = await _dio.get('/api/config'); + final data = response.data; + Map? jsonMap; + if (data is Map) { + jsonMap = data; + } else if (data is String && data.isNotEmpty) { + final decoded = json.decode(data); + if (decoded is Map) { + jsonMap = decoded; + } + } + if (jsonMap == null) { + return null; + } + return BackendConfig.fromJson(jsonMap); + } on DioException catch (e, stackTrace) { + _traceApi('Backend config request failed: $e'); + DebugLogger.error( + 'backend-config-error', + scope: 'api/config', + error: e, + stackTrace: stackTrace, + ); + rethrow; + } catch (e, stackTrace) { + _traceApi('Backend config decode error: $e'); + DebugLogger.error( + 'backend-config-decode', + scope: 'api/config', + error: e, + stackTrace: stackTrace, + ); + rethrow; + } + } + // Authentication Future> login(String username, String password) async { try { diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index b40b533..39a08ba 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -26,7 +26,7 @@ class SettingsService { static const String _voiceAutoSendKey = PreferenceKeys.voiceAutoSendFinal; // Realtime transport preference static const String _socketTransportModeKey = - PreferenceKeys.socketTransportMode; // 'auto' or 'ws' + PreferenceKeys.socketTransportMode; // 'polling' or 'ws' // Quick pill visibility selections (max 2) static const String _quickPillsKey = PreferenceKeys .quickPills; // StringList of identifiers e.g. ['web','image','tools'] @@ -256,15 +256,27 @@ class SettingsService { return _preferencesBox().put(_voiceAutoSendKey, value); } - /// Transport mode: 'auto' (polling+websocket) or 'ws' (websocket only) + /// Transport mode: 'polling' (HTTP polling + WebSocket upgrade) or 'ws' static Future getSocketTransportMode() { - final value = _preferencesBox().get(_socketTransportModeKey) as String?; - return Future.value(value ?? 'ws'); + final raw = _preferencesBox().get(_socketTransportModeKey) as String?; + if (raw == null) { + return Future.value('ws'); + } + if (raw == 'auto') { + return Future.value('polling'); + } + if (raw != 'polling' && raw != 'ws') { + return Future.value('ws'); + } + return Future.value(raw); } static Future setSocketTransportMode(String mode) { - if (mode != 'auto' && mode != 'ws') { - mode = 'auto'; + if (mode == 'auto') { + mode = 'polling'; + } + if (mode != 'polling' && mode != 'ws') { + mode = 'polling'; } return _preferencesBox().put(_socketTransportModeKey, mode); } @@ -344,7 +356,7 @@ class AppSettings { final String? voiceLocaleId; final bool voiceHoldToTalk; final bool voiceAutoSendFinal; - final String socketTransportMode; // 'auto' or 'ws' + final String socketTransportMode; // 'polling' or 'ws' final List quickPills; // e.g., ['web','image'] final bool sendOnEnter; final String? ttsVoice; @@ -566,8 +578,17 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { } Future setSocketTransportMode(String mode) async { - state = state.copyWith(socketTransportMode: mode); - await SettingsService.setSocketTransportMode(mode); + var sanitized = mode; + if (sanitized == 'auto') { + sanitized = 'polling'; + } + if (sanitized != 'polling' && sanitized != 'ws') { + sanitized = 'polling'; + } + if (state.socketTransportMode != sanitized) { + state = state.copyWith(socketTransportMode: sanitized); + } + await SettingsService.setSocketTransportMode(sanitized); } Future setQuickPills(List pills) async { diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index 4ef30da..fe88c19 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -22,6 +22,7 @@ typedef SocketChannelEventHandler = class SocketService with WidgetsBindingObserver { final ServerConfig serverConfig; final bool websocketOnly; + final bool allowWebsocketUpgrade; io.Socket? _socket; String? _authToken; bool _isAppForeground = true; @@ -34,6 +35,7 @@ class SocketService with WidgetsBindingObserver { required this.serverConfig, String? authToken, this.websocketOnly = false, + this.allowWebsocketUpgrade = true, }) : _authToken = authToken { final binding = WidgetsBinding.instance; final lifecycle = binding.lifecycleState; @@ -72,11 +74,18 @@ class SocketService with WidgetsBindingObserver { } catch (_) {} final path = '/ws/socket.io'; + final usePollingOnly = !websocketOnly && !allowWebsocketUpgrade; + final transports = websocketOnly + ? const ['websocket'] + : usePollingOnly + ? const ['polling'] + : const ['polling', 'websocket']; + final builder = io.OptionBuilder() - // Transport selection - WebSocket only, no polling fallback - .setTransports(['websocket']) - .setRememberUpgrade(false) - .setUpgrade(false) + // Transport selection switches between WebSocket-only and polling fallback + .setTransports(transports) + .setRememberUpgrade(!websocketOnly && allowWebsocketUpgrade) + .setUpgrade(!websocketOnly && allowWebsocketUpgrade) // Tune reconnect/backoff and timeouts .setReconnectionAttempts(0) // 0/Infinity semantics: unlimited attempts .setReconnectionDelay(1000) diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index bc5e01d..bc00a29 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -392,6 +392,18 @@ class AppCustomizationPage extends ConsumerWidget { ) { final theme = context.conduitTheme; final l10n = AppLocalizations.of(context)!; + final transportAvailability = ref.watch(socketTransportOptionsProvider); + var activeTransportMode = settings.socketTransportMode; + if (!transportAvailability.allowPolling && + activeTransportMode == 'polling') { + activeTransportMode = 'ws'; + } else if (!transportAvailability.allowWebsocketOnly && + activeTransportMode == 'ws') { + activeTransportMode = 'polling'; + } + final transportLabel = activeTransportMode == 'polling' + ? l10n.transportModePolling + : l10n.transportModeWs; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -402,6 +414,38 @@ class AppCustomizationPage extends ConsumerWidget { TextStyle(color: theme.sidebarForeground, fontSize: 18), ), const SizedBox(height: Spacing.sm), + _CustomizationTile( + leading: _buildIconBadge( + context, + UiUtils.platformIcon( + ios: CupertinoIcons.arrow_2_circlepath, + android: Icons.sync, + ), + color: theme.buttonPrimary, + ), + title: l10n.transportMode, + subtitle: transportLabel, + trailing: + transportAvailability.allowPolling && + transportAvailability.allowWebsocketOnly + ? _buildValueBadge(context, transportLabel) + : null, + onTap: + transportAvailability.allowPolling && + transportAvailability.allowWebsocketOnly + ? () => _showTransportModeSheet( + context, + ref, + settings, + allowPolling: transportAvailability.allowPolling, + allowWebsocketOnly: transportAvailability.allowWebsocketOnly, + ) + : null, + showChevron: + transportAvailability.allowPolling && + transportAvailability.allowWebsocketOnly, + ), + const SizedBox(height: Spacing.sm), _CustomizationTile( leading: _buildIconBadge( context, @@ -1217,6 +1261,148 @@ class AppCustomizationPage extends ConsumerWidget { } } + Future _showTransportModeSheet( + BuildContext context, + WidgetRef ref, + AppSettings settings, { + required bool allowPolling, + required bool allowWebsocketOnly, + }) async { + final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; + var current = settings.socketTransportMode; + + final options = <({String value, String title, String subtitle})>[]; + if (allowPolling) { + options.add(( + value: 'polling', + title: l10n.transportModePolling, + subtitle: l10n.transportModePollingInfo, + )); + } + if (allowWebsocketOnly) { + options.add(( + value: 'ws', + title: l10n.transportModeWs, + subtitle: l10n.transportModeWsInfo, + )); + } + + if (options.isEmpty) { + return; + } + + if (!options.any((option) => option.value == current)) { + current = options.first.value; + } + + await showModalBottomSheet( + context: context, + backgroundColor: theme.sidebarBackground, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.modal), + ), + ), + builder: (sheetContext) { + return SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.lg, + vertical: Spacing.md, + ), + child: Row( + children: [ + Expanded( + child: Text( + l10n.transportMode, + style: + theme.headingSmall?.copyWith( + color: theme.sidebarForeground, + ) ?? + TextStyle( + color: theme.sidebarForeground, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + icon: Icon(Icons.close, color: theme.iconPrimary), + onPressed: () => Navigator.of(sheetContext).pop(), + ), + ], + ), + ), + const Divider(height: 1), + for (var i = 0; i < options.length; i++) ...[ + () { + final option = options[i]; + final selected = current == option.value; + return ListTile( + leading: Icon( + selected ? Icons.check_circle : Icons.circle_outlined, + color: selected + ? theme.buttonPrimary + : theme.iconSecondary, + ), + title: Text(option.title), + subtitle: Text(option.subtitle), + onTap: () { + if (!selected) { + ref + .read(appSettingsProvider.notifier) + .setSocketTransportMode(option.value); + } + Navigator.of(sheetContext).pop(); + }, + ); + }(), + if (i != options.length - 1) const Divider(height: 1), + ], + const SizedBox(height: Spacing.lg), + ], + ), + ); + }, + ); + } + + Widget _buildValueBadge(BuildContext context, String label) { + final theme = context.conduitTheme; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + decoration: BoxDecoration( + color: theme.buttonPrimary.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(AppBorderRadius.small), + border: Border.all( + color: theme.buttonPrimary.withValues(alpha: 0.25), + width: BorderWidth.thin, + ), + ), + child: Text( + label, + style: + theme.bodySmall?.copyWith( + color: theme.buttonPrimary, + fontWeight: FontWeight.w600, + ) ?? + TextStyle( + color: theme.buttonPrimary, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ); + } + Widget _buildIconBadge( BuildContext context, IconData icon, { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 31f9bb3..55fe232 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -334,9 +334,9 @@ "transportMode": "Transportmodus", "transportModeDescription": "Wähle, wie die App für Echtzeit-Updates verbindet.", "mode": "Modus", - "transportModeAuto": "Auto (Polling + WebSocket)", + "transportModePolling": "Polling-Fallback", "transportModeWs": "Nur WebSocket", - "transportModeAutoInfo": "Robuster in restriktiven Netzwerken. Wechselt nach Möglichkeit zu WebSocket.", + "transportModePollingInfo": "Fällt auf HTTP-Polling zurück, wenn WebSockets blockiert sind. Wechselt nach Möglichkeit zu WebSocket.", "transportModeWsInfo": "Geringerer Overhead, kann jedoch hinter strikten Proxys/Firewalls fehlschlagen.", "websocketConnectionError": "Echtzeit-Verbindung konnte nicht hergestellt werden. Bitte überprüfen Sie Ihr Netzwerk und die Serverkonfiguration.", "websocketReconnectFailed": "Echtzeit-Verbindung fehlgeschlagen. Streaming funktioniert möglicherweise nicht ordnungsgemäß." diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 24f8b03..5a9c825 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -694,12 +694,12 @@ "@transportModeDescription": {"description": "Helper text explaining the transport setting."}, "mode": "Mode", "@mode": {"description": "Form field label for transport mode dropdown."}, - "transportModeAuto": "Auto (Polling + WebSocket)", - "@transportModeAuto": {"description": "Dropdown option label for automatic transport selection."}, + "transportModePolling": "Polling fallback", + "@transportModePolling": {"description": "Dropdown option label for HTTP polling fallback transport."}, "transportModeWs": "WebSocket only", "@transportModeWs": {"description": "Dropdown option label for WebSocket-only transport."}, - "transportModeAutoInfo": "More robust on restrictive networks. Upgrades to WebSocket when possible.", - "@transportModeAutoInfo": {"description": "Footnote text for the Auto transport mode."}, + "transportModePollingInfo": "Falls back to HTTP polling when WebSocket is blocked. Upgrades to WebSocket when possible.", + "@transportModePollingInfo": {"description": "Footnote text for the polling fallback transport mode."}, "transportModeWsInfo": "Lower overhead, but may fail behind strict proxies/firewalls.", "@transportModeWsInfo": {"description": "Footnote text for the WebSocket-only transport mode."}, "websocketConnectionError": "Unable to establish real-time connection. Please check your network and server configuration.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e57e3a9..5bcf573 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -327,9 +327,9 @@ "transportMode": "Modo de transporte", "transportModeDescription": "Elige cómo se conecta la aplicación para actualizaciones en tiempo real.", "mode": "Modo", - "transportModeAuto": "Automático (Polling + WebSocket)", + "transportModePolling": "Polling de respaldo", "transportModeWs": "Solo WebSocket", - "transportModeAutoInfo": "Más robusto en redes restrictivas. Se actualiza a WebSocket cuando es posible.", + "transportModePollingInfo": "Recurrirá a HTTP polling si WebSocket está bloqueado. Se actualizará a WebSocket cuando sea posible.", "transportModeWsInfo": "Menor sobrecarga, pero puede fallar detrás de proxies/firewalls estrictos.", "websocketConnectionError": "No se puede establecer la conexión en tiempo real. Por favor, verifica tu red y la configuración del servidor.", "websocketReconnectFailed": "Fallo en la conexión en tiempo real. El streaming podría no funcionar correctamente." diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1646a2f..cd5a0cb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -334,9 +334,9 @@ "transportMode": "Mode de transport", "transportModeDescription": "Choisissez comment l'app se connecte pour les mises à jour en temps réel.", "mode": "Mode", - "transportModeAuto": "Auto (Polling + WebSocket)", + "transportModePolling": "Polling de secours", "transportModeWs": "WebSocket uniquement", - "transportModeAutoInfo": "Plus robuste sur les réseaux restrictifs. Passe à WebSocket lorsque possible.", + "transportModePollingInfo": "Bascule sur HTTP polling lorsque WebSocket est bloqué. Repasse à WebSocket dès que possible.", "transportModeWsInfo": "Moins de surcharge, mais peut échouer derrière des proxys/firewalls stricts.", "websocketConnectionError": "Impossible d'établir une connexion en temps réel. Veuillez vérifier votre réseau et la configuration du serveur.", "websocketReconnectFailed": "Échec de la connexion en temps réel. Le streaming pourrait ne pas fonctionner correctement." diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 72bc56e..1b2f9cd 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -334,9 +334,9 @@ "transportMode": "Modalità di trasporto", "transportModeDescription": "Scegli come l'app si connette per gli aggiornamenti in tempo reale.", "mode": "Modalità", - "transportModeAuto": "Auto (Polling + WebSocket)", + "transportModePolling": "Polling di fallback", "transportModeWs": "Solo WebSocket", - "transportModeAutoInfo": "Più robusto nelle reti restrittive. Passa a WebSocket quando possibile.", + "transportModePollingInfo": "Quando WebSocket è bloccato passa a HTTP polling. Torna a WebSocket appena possibile.", "transportModeWsInfo": "Minore overhead, ma può fallire dietro proxy/firewall restrittivi.", "websocketConnectionError": "Impossibile stabilire una connessione in tempo reale. Si prega di controllare la rete e la configurazione del server.", "websocketReconnectFailed": "Connessione in tempo reale fallita. Lo streaming potrebbe non funzionare correttamente." diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 46f432b..e97c797 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1826,11 +1826,11 @@ abstract class AppLocalizations { /// **'Mode'** String get mode; - /// Dropdown option label for automatic transport selection. + /// Dropdown option label for HTTP polling fallback transport. /// /// In en, this message translates to: - /// **'Auto (Polling + WebSocket)'** - String get transportModeAuto; + /// **'Polling fallback'** + String get transportModePolling; /// Dropdown option label for WebSocket-only transport. /// @@ -1838,11 +1838,11 @@ abstract class AppLocalizations { /// **'WebSocket only'** String get transportModeWs; - /// Footnote text for the Auto transport mode. + /// Footnote text for the polling fallback transport mode. /// /// In en, this message translates to: - /// **'More robust on restrictive networks. Upgrades to WebSocket when possible.'** - String get transportModeAutoInfo; + /// **'Falls back to HTTP polling when WebSocket is blocked. Upgrades to WebSocket when possible.'** + String get transportModePollingInfo; /// Footnote text for the WebSocket-only transport mode. /// diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 5f6f17c..19578db 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -953,14 +953,14 @@ class AppLocalizationsDe extends AppLocalizations { String get mode => 'Modus'; @override - String get transportModeAuto => 'Auto (Polling + WebSocket)'; + String get transportModePolling => 'Polling-Fallback'; @override String get transportModeWs => 'Nur WebSocket'; @override - String get transportModeAutoInfo => - 'Robuster in restriktiven Netzwerken. Wechselt nach Möglichkeit zu WebSocket.'; + String get transportModePollingInfo => + 'Fällt auf HTTP-Polling zurück, wenn WebSockets blockiert sind. Wechselt nach Möglichkeit zu WebSocket.'; @override String get transportModeWsInfo => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 56308b5..bb3189d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -945,14 +945,14 @@ class AppLocalizationsEn extends AppLocalizations { String get mode => 'Mode'; @override - String get transportModeAuto => 'Auto (Polling + WebSocket)'; + String get transportModePolling => 'Polling fallback'; @override String get transportModeWs => 'WebSocket only'; @override - String get transportModeAutoInfo => - 'More robust on restrictive networks. Upgrades to WebSocket when possible.'; + String get transportModePollingInfo => + 'Falls back to HTTP polling when WebSocket is blocked. Upgrades to WebSocket when possible.'; @override String get transportModeWsInfo => diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f5f6084..b132f89 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -959,14 +959,14 @@ class AppLocalizationsFr extends AppLocalizations { String get mode => 'Mode'; @override - String get transportModeAuto => 'Auto (Polling + WebSocket)'; + String get transportModePolling => 'Polling de secours'; @override String get transportModeWs => 'WebSocket uniquement'; @override - String get transportModeAutoInfo => - 'Plus robuste sur les réseaux restrictifs. Passe à WebSocket lorsque possible.'; + String get transportModePollingInfo => + 'Bascule sur HTTP polling lorsque WebSocket est bloqué. Repasse à WebSocket dès que possible.'; @override String get transportModeWsInfo => diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 2693b20..896850a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -948,14 +948,14 @@ class AppLocalizationsIt extends AppLocalizations { String get mode => 'Modalità'; @override - String get transportModeAuto => 'Auto (Polling + WebSocket)'; + String get transportModePolling => 'Polling di fallback'; @override String get transportModeWs => 'Solo WebSocket'; @override - String get transportModeAutoInfo => - 'Più robusto nelle reti restrittive. Passa a WebSocket quando possibile.'; + String get transportModePollingInfo => + 'Quando WebSocket è bloccato passa a HTTP polling. Torna a WebSocket appena possibile.'; @override String get transportModeWsInfo => diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index c21b3f8..984c5ac 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -327,9 +327,9 @@ "transportMode": "Transportmodus", "transportModeDescription": "Kies hoe de app verbindt voor realtime updates.", "mode": "Modus", - "transportModeAuto": "Automatisch (Polling + WebSocket)", + "transportModePolling": "Polling-fallback", "transportModeWs": "Alleen WebSocket", - "transportModeAutoInfo": "Robuuster op beperkende netwerken. Upgrade naar WebSocket indien mogelijk.", + "transportModePollingInfo": "Valt terug op HTTP-polling wanneer WebSocket geblokkeerd is. Upgrade naar WebSocket zodra dat kan.", "transportModeWsInfo": "Lagere overhead, maar kan mislukken achter strikte proxies/firewalls.", "websocketConnectionError": "Kan geen realtime verbinding maken. Controleer uw netwerk en serverconfiguratie.", "websocketReconnectFailed": "Realtime verbinding mislukt. Streaming werkt mogelijk niet goed." diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b258c19..68f1f41 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -327,9 +327,9 @@ "transportMode": "Режим транспорта", "transportModeDescription": "Выберите, как приложение подключается для обновлений в реальном времени.", "mode": "Режим", - "transportModeAuto": "Авто (опрос + WebSocket)", + "transportModePolling": "Опрос (резерв)", "transportModeWs": "Только WebSocket", - "transportModeAutoInfo": "Более надежен в ограничительных сетях. Переходит на WebSocket, когда это возможно.", + "transportModePollingInfo": "Переходит на HTTP-опрос, если WebSocket заблокирован. Возвращается к WebSocket, когда это возможно.", "transportModeWsInfo": "Меньше накладных расходов, но может не работать за строгими прокси/брандмауэрами.", "websocketConnectionError": "Не удалось установить соединение в реальном времени. Пожалуйста, проверьте сеть и конфигурацию сервера.", "websocketReconnectFailed": "Сбой соединения в реальном времени. Потоковая передача может работать неправильно." diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c1d054d..a615ced 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -327,9 +327,9 @@ "transportMode": "传输模式", "transportModeDescription": "选择应用如何连接以进行实时更新。", "mode": "模式", - "transportModeAuto": "自动(轮询 + WebSocket)", + "transportModePolling": "轮询回退", "transportModeWs": "仅 WebSocket", - "transportModeAutoInfo": "在限制性网络上更稳健。在可能的情况下升级到 WebSocket。", + "transportModePollingInfo": "当 WebSocket 被阻止时改用 HTTP 轮询,在条件允许时切换回 WebSocket。", "transportModeWsInfo": "开销较低,但可能在严格的代理/防火墙后失败。", "websocketConnectionError": "无法建立实时连接。请检查您的网络和服务器配置。", "websocketReconnectFailed": "实时连接失败。流式传输可能无法正常工作。"