From 3a3469a5f9a59e9836718576e91ace69207d2b1b Mon Sep 17 00:00:00 2001 From: Lauritz Tieste <84938977+Lauritz-Tieste@users.noreply.github.com> Date: Mon, 18 May 2026 16:58:57 +0200 Subject: [PATCH] feat(ui): add ImmichURLInput (#27105) feat(ui): implement shared URL input configuration and update input fields --- .../lib/widgets/forms/login/login_form.dart | 7 +-- .../networking_settings/endpoint_input.dart | 26 +++-------- .../local_network_preference.dart | 18 +++++--- mobile/packages/ui/lib/immich_ui.dart | 1 + .../ui/lib/src/components/text_input.dart | 44 ++++++++++++++----- .../ui/lib/src/components/url_input.dart | 30 +++++++++++++ 6 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 mobile/packages/ui/lib/src/components/url_input.dart diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index d53cf1d1d2..e5505a3288 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -400,15 +400,12 @@ class LoginForm extends HookConsumerWidget { submitText: 'next'.t(context: context), submitIcon: Icons.arrow_forward_rounded, onSubmit: getServerAuthSettings, - child: ImmichTextInput( + child: ImmichURLInput( controller: serverEndpointController, label: 'login_form_endpoint_url'.t(context: context), hintText: 'login_form_endpoint_hint'.t(context: context), validator: _validateUrl, - keyboardAction: TextInputAction.next, - keyboardType: TextInputType.url, - autofillHints: const [AutofillHints.url], - autoCorrect: false, + keyboardAction: .next, onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), ), ), diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index 735971e0c2..e8310caed4 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; +import 'package:immich_ui/immich_ui.dart'; class EndpointInput extends StatefulHookConsumerWidget { const EndpointInput({ @@ -111,28 +111,12 @@ class EndpointInputState extends ConsumerState { status: auxCheckStatus, enabled: widget.enabled, ), - subtitle: TextFormField( + subtitle: ImmichURLInput( enabled: widget.enabled, - onTapOutside: (_) => focusNode.unfocus(), - autovalidateMode: AutovalidateMode.onUserInteraction, + autovalidateMode: .onUserInteraction, validator: validateUrl, - keyboardType: TextInputType.url, - style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 14), - decoration: InputDecoration( - hintText: 'http(s)://immich.domain.com', - contentPadding: const EdgeInsets.all(16), - filled: true, - fillColor: context.colorScheme.surfaceContainer, - border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(16))), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.red[300]!), - borderRadius: const BorderRadius.all(Radius.circular(16)), - ), - disabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: context.isDarkTheme ? Colors.grey[900]! : Colors.grey[300]!), - borderRadius: const BorderRadius.all(Radius.circular(16)), - ), - ), + keyboardAction: .next, + hintText: 'http(s)://immich.domain.com', controller: controller, focusNode: focusNode, ), diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index c89c8e149e..1d2b5eea0f 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -8,24 +8,29 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; +import 'package:immich_ui/immich_ui.dart'; class LocalNetworkPreference extends HookConsumerWidget { const LocalNetworkPreference({super.key, required this.enabled}); final bool enabled; - Future _showEditDialog(BuildContext context, String title, String hintText, String initialValue) { + Future _showEditDialog( + BuildContext context, + String title, + String hintText, + String initialValue, { + bool isUrlField = false, + }) { final controller = TextEditingController(text: initialValue); return showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), - content: TextField( - controller: controller, - autofocus: true, - decoration: InputDecoration(border: const OutlineInputBorder(), hintText: hintText), - ), + content: isUrlField + ? ImmichURLInput(controller: controller, autofocus: true, keyboardAction: .done, hintText: hintText) + : ImmichTextInput(controller: controller, autofocus: true, keyboardAction: .done, hintText: hintText), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -81,6 +86,7 @@ class LocalNetworkPreference extends HookConsumerWidget { "server_endpoint".tr(), "http://local-ip:2283", localEndpointText.value, + isUrlField: true, ); if (localEndpoint != null) { diff --git a/mobile/packages/ui/lib/immich_ui.dart b/mobile/packages/ui/lib/immich_ui.dart index c9e510a162..6158b4f0e1 100644 --- a/mobile/packages/ui/lib/immich_ui.dart +++ b/mobile/packages/ui/lib/immich_ui.dart @@ -5,6 +5,7 @@ export 'src/components/icon_button.dart'; export 'src/components/password_input.dart'; export 'src/components/text_button.dart'; export 'src/components/text_input.dart'; +export 'src/components/url_input.dart'; export 'src/constants.dart'; export 'src/theme.dart'; export 'src/translation.dart'; diff --git a/mobile/packages/ui/lib/src/components/text_input.dart b/mobile/packages/ui/lib/src/components/text_input.dart index 1b3fb91f51..627af15d34 100644 --- a/mobile/packages/ui/lib/src/components/text_input.dart +++ b/mobile/packages/ui/lib/src/components/text_input.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class ImmichTextInput extends StatefulWidget { - final String label; + final String? label; final String? hintText; final TextEditingController? controller; final FocusNode? focusNode; @@ -12,13 +13,19 @@ class ImmichTextInput extends StatefulWidget { final List? autofillHints; final Widget? suffixIcon; final bool obscureText; - final bool autoCorrect; + final bool autocorrect; + final SmartDashesType? smartDashesType; + final SmartQuotesType? smartQuotesType; + final List? inputFormatters; + final bool enabled; + final bool autofocus; + final AutovalidateMode? autovalidateMode; const ImmichTextInput({ super.key, this.controller, this.focusNode, - required this.label, + this.label, this.hintText, this.validator, this.onSubmit, @@ -27,7 +34,13 @@ class ImmichTextInput extends StatefulWidget { this.autofillHints, this.suffixIcon, this.obscureText = false, - this.autoCorrect = true, + this.autocorrect = true, + this.smartDashesType, + this.smartQuotesType, + this.inputFormatters, + this.enabled = true, + this.autofocus = false, + this.autovalidateMode, }); @override @@ -53,9 +66,14 @@ class _ImmichTextInputState extends State { } String? _validateInput(String? value) { - setState(() { - _error = widget.validator?.call(value); - }); + final error = widget.validator?.call(value); + if (error != _error) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() => _error = error); + } + }); + } return null; } @@ -68,6 +86,9 @@ class _ImmichTextInputState extends State { return TextFormField( controller: widget.controller, focusNode: _focusNode, + enabled: widget.enabled, + autofocus: widget.autofocus, + autovalidateMode: widget.autovalidateMode, decoration: InputDecoration( hintText: widget.hintText, labelText: widget.label, @@ -79,13 +100,16 @@ class _ImmichTextInputState extends State { ), obscureText: widget.obscureText, validator: _validateInput, - keyboardType: widget.keyboardType, textInputAction: widget.keyboardAction, - autocorrect: widget.autoCorrect, - autofillHints: widget.autofillHints, onTap: () => setState(() => _error = null), onTapOutside: (_) => _focusNode.unfocus(), onFieldSubmitted: (value) => widget.onSubmit?.call(context, value), + keyboardType: widget.keyboardType, + autofillHints: widget.autofillHints, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + inputFormatters: widget.inputFormatters, ); } } diff --git a/mobile/packages/ui/lib/src/components/url_input.dart b/mobile/packages/ui/lib/src/components/url_input.dart new file mode 100644 index 0000000000..b23136a122 --- /dev/null +++ b/mobile/packages/ui/lib/src/components/url_input.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; +import 'package:immich_ui/src/components/text_input.dart'; + +class ImmichURLInput extends ImmichTextInput { + ImmichURLInput({ + super.key, + super.controller, + super.focusNode, + super.label, + super.hintText, + super.validator, + super.onSubmit, + super.keyboardAction, + super.suffixIcon, + super.enabled, + super.autofocus, + super.autovalidateMode, + }) : super( + keyboardType: .url, + autofillHints: const [AutofillHints.url], + autocorrect: false, + smartDashesType: .disabled, + smartQuotesType: .disabled, + inputFormatters: _formatters, + ); + + static final List _formatters = List.unmodifiable([ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ]); +}