diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 03affd02..78e1f0ad 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -357,6 +357,10 @@ class SettingsService extends BaseSharedPreferencesService { static const customDownloadPath = NullableStringPref('custom_download_path'); static final customRelayUrl = NullableStringPref('custom_relay_url', transform: _trimEmptyAsNull); static const recentRooms = NullableStringPref('watch_together_recent_rooms'); + static final companionRemoteLastHostAddress = NullableStringPref( + 'companion_remote_last_host_address', + transform: _trimEmptyAsNull, + ); static final maxVolume = IntPref('max_volume', defaultValue: 100, transform: (v) => v.clamp(100, 300)); static final subtitlePosition = IntPref('subtitle_position', defaultValue: 100, transform: (v) => v.clamp(0, 100)); @@ -736,6 +740,7 @@ class SettingsService extends BaseSharedPreferencesService { selectedExternalPlayer, customExternalPlayers, customRelayUrl, + companionRemoteLastHostAddress, ]; Future resetAllSettings() async { diff --git a/lib/widgets/companion_remote/discovery_view.dart b/lib/widgets/companion_remote/discovery_view.dart index 393ef442..72fcba2a 100644 --- a/lib/widgets/companion_remote/discovery_view.dart +++ b/lib/widgets/companion_remote/discovery_view.dart @@ -16,6 +16,7 @@ import '../../profiles/active_profile_provider.dart'; import '../../profiles/plex_home_service.dart'; import '../../profiles/profile_connection_registry.dart'; import '../../providers/companion_remote_provider.dart'; +import '../../services/settings_service.dart'; import '../../utils/app_logger.dart'; import '../loading_indicator_box.dart'; @@ -48,9 +49,20 @@ class _DiscoveryViewState extends State with ControllerDisposerMi void initState() { super.initState(); _provider = context.read(); + unawaited(_loadSavedManualHostAddress()); _initCryptoAndDiscover(); } + Future _loadSavedManualHostAddress() async { + final settings = SettingsService.instanceOrNull ?? await SettingsService.getInstance(); + final hostAddress = settings.read(SettingsService.companionRemoteLastHostAddress); + if (!mounted || hostAddress == null || _hostAddressController.text.isNotEmpty) return; + setState(() { + _hostAddressController.text = hostAddress; + _showManualEntry = true; + }); + } + Future _initCryptoAndDiscover() async { final connections = context.read(); final activeProfile = context.read(); @@ -140,6 +152,26 @@ class _DiscoveryViewState extends State with ControllerDisposerMi } } + Future _saveManualHostAddress(String hostAddress) async { + try { + final settings = SettingsService.instanceOrNull ?? await SettingsService.getInstance(); + await settings.write(SettingsService.companionRemoteLastHostAddress, hostAddress); + } catch (e) { + appLogger.w('Failed to save companion remote host address', error: e); + } + } + + void _submitManualHost() { + if (!_formKey.currentState!.validate()) return; + final hostAddress = _hostAddressController.text.trim(); + unawaited( + _connect(() async { + await _provider.connectToManualHost(hostAddress); + await _saveManualHostAddress(hostAddress); + }), + ); + } + String _parseErrorMessage(String error) { if (error.contains('timeout') || error.contains('Timed out')) { return t.companionRemote.pairing.connectionTimedOut; @@ -340,10 +372,11 @@ class _DiscoveryViewState extends State with ControllerDisposerMi prefixIcon: const Icon(Icons.computer), ), validator: (value) { - if (value == null || value.isEmpty) { + final hostAddress = value?.trim() ?? ''; + if (hostAddress.isEmpty) { return t.companionRemote.pairing.validationHostRequired; } - if (value.split(':').length != 2) { + if (hostAddress.split(':').length != 2) { return t.companionRemote.pairing.validationHostFormat; } return null; @@ -356,19 +389,9 @@ class _DiscoveryViewState extends State with ControllerDisposerMi FocusableButton( focusNode: _connectFocusNode, onNavigateUp: _hostAddressFocusNode.requestFocus, - onPressed: _isConnecting - ? null - : () { - if (!_formKey.currentState!.validate()) return; - _connect(() => _provider.connectToManualHost(_hostAddressController.text.trim())); - }, + onPressed: _isConnecting ? null : _submitManualHost, child: FilledButton.icon( - onPressed: _isConnecting - ? null - : () { - if (!_formKey.currentState!.validate()) return; - _connect(() => _provider.connectToManualHost(_hostAddressController.text.trim())); - }, + onPressed: _isConnecting ? null : _submitManualHost, icon: _isConnecting ? const LoadingIndicatorBox(size: 16) : const Icon(Icons.link), label: Text(_isConnecting ? t.companionRemote.pairing.connecting : t.common.connect), ), diff --git a/test/services/settings_service_test.dart b/test/services/settings_service_test.dart index 23915c78..45c393f2 100644 --- a/test/services/settings_service_test.dart +++ b/test/services/settings_service_test.dart @@ -74,6 +74,29 @@ void main() { }); }); + group('SettingsService companion remote prefs', () { + test('last manual host address trims whitespace and drops blanks', () async { + final settings = await SettingsService.getInstance(); + + await settings.write(SettingsService.companionRemoteLastHostAddress, ' 192.168.1.10:48632 '); + expect(settings.read(SettingsService.companionRemoteLastHostAddress), '192.168.1.10:48632'); + + await settings.write(SettingsService.companionRemoteLastHostAddress, ' '); + expect(settings.read(SettingsService.companionRemoteLastHostAddress), isNull); + }); + + test('resetAllSettings clears the last manual host address', () async { + final settings = await SettingsService.getInstance(); + + await settings.write(SettingsService.companionRemoteLastHostAddress, '192.168.1.10:48632'); + expect(settings.read(SettingsService.companionRemoteLastHostAddress), isNotNull); + + await settings.resetAllSettings(); + + expect(settings.read(SettingsService.companionRemoteLastHostAddress), isNull); + }); + }); + group('SettingsService listenables', () { test('refreshListenables updates active prefs outside the resettable surface', () async { final settings = await SettingsService.getInstance();