import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../../connection/connection.dart'; import '../../connection/connection_registry.dart'; import '../../exceptions/media_server_exceptions.dart'; import '../../focus/focusable_button.dart'; import '../../focus/focusable_text_field.dart'; import '../../i18n/strings.g.dart'; import '../../mixins/controller_disposer_mixin.dart'; import '../../services/jellyfin_endpoint_discovery.dart'; import '../../utils/app_logger.dart'; import '../../widgets/app_icon.dart'; import '../../widgets/focused_scroll_scaffold.dart'; import '../../widgets/loading_indicator_box.dart'; import 'async_form_state_mixin.dart'; class EditJellyfinConnectionScreen extends StatefulWidget { final JellyfinConnection connection; const EditJellyfinConnectionScreen({super.key, required this.connection}); @override State createState() => _EditJellyfinConnectionScreenState(); } class _EditJellyfinConnectionScreenState extends State with AsyncFormStateMixin, ControllerDisposerMixin { late final _urlsController = createTextEditingController(text: widget.connection.baseUrls.join('\n')); final _urlsFocus = FocusNode(debugLabel: 'EditJellyfin:Urls'); final _saveFocus = FocusNode(debugLabel: 'EditJellyfin:Save'); final _formKey = GlobalKey(); @override void dispose() { _urlsFocus.dispose(); _saveFocus.dispose(); super.dispose(); } Future _save() async { if (!(_formKey.currentState?.validate() ?? false)) return; await runAsync( () async { final input = JellyfinEndpointDiscovery.buildUserInputCandidates( _enteredUrls(), dialect: widget.connection.dialect, ); final endpoint = await JellyfinEndpointDiscovery(dialect: widget.connection.dialect).raceEndpoints( input.probeBaseUrls, preferredUrl: widget.connection.baseUrl, expectedMachineId: widget.connection.serverMachineId, baseUrlsToPersist: input.explicitBaseUrls, baseUrlValidationGroups: input.validationBaseUrlGroups, ); final updated = widget.connection.copyWith( baseUrl: endpoint.activeBaseUrl, baseUrls: endpoint.baseUrls, serverName: endpoint.serverInfo.serverName, ); if (!mounted) return; await context.read().upsert(updated); if (!mounted) return; Navigator.of(context).pop(true); }, errorMapper: (e) { if (e is MediaServerUrlException) return e.display ?? e.message; appLogger.e('Edit ${widget.connection.dialect.productName} connection failed', error: e); return t.addServer.couldNotReachServer(error: e.toString()); }, ); } List _enteredUrls() => JellyfinEndpointDiscovery.parseUserEnteredUrls(_urlsController.text); @override Widget build(BuildContext context) { final theme = Theme.of(context); return FocusedScrollScaffold( title: Text(t.connections.editMediaBrowserTitle(product: widget.connection.dialect.productName)), slivers: [ SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverToBoxAdapter( child: Form( key: _formKey, child: Column( crossAxisAlignment: .stretch, children: [ Text( t.connections.editMediaBrowserIntro(serverName: widget.connection.serverName), style: theme.textTheme.bodyMedium, ), const SizedBox(height: 16), FocusableTextFormField( controller: _urlsController, focusNode: _urlsFocus, tvTextInputPresentation: TvTextInputPresentation.flutterOverlay, autofocus: true, keyboardType: TextInputType.url, minLines: 1, maxLines: 5, autocorrect: false, enableSuggestions: false, enabled: !busy, onNavigateDown: () => _saveFocus.requestFocus(), decoration: InputDecoration( labelText: t.addServer.serverUrls, prefixIcon: const AppIcon(Symbols.link_rounded, fill: 1), ), validator: (_) => _enteredUrls().isEmpty ? t.addServer.required : null, ), const SizedBox(height: 16), FocusableButton( focusNode: _saveFocus, useBackgroundFocus: true, onPressed: busy ? null : _save, child: FilledButton.icon( onPressed: busy ? null : _save, icon: busy ? const LoadingIndicatorBox() : const AppIcon(Symbols.save_rounded, fill: 1), label: Text(t.common.save), ), ), ...buildInlineError(theme), ], ), ), ), ), ], ); } }