feat(ui): add ImmichURLInput (#27105)

feat(ui): implement shared URL input configuration and update input fields
pull/28479/head
Lauritz Tieste 2026-05-18 16:58:57 +02:00 committed by GitHub
parent 7993619ed2
commit 3a3469a5f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 42 deletions

View File

@ -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(),
),
),

View File

@ -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<EndpointInput> {
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,
),

View File

@ -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<String?> _showEditDialog(BuildContext context, String title, String hintText, String initialValue) {
Future<String?> _showEditDialog(
BuildContext context,
String title,
String hintText,
String initialValue, {
bool isUrlField = false,
}) {
final controller = TextEditingController(text: initialValue);
return showDialog<String>(
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) {

View File

@ -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';

View File

@ -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<String>? autofillHints;
final Widget? suffixIcon;
final bool obscureText;
final bool autoCorrect;
final bool autocorrect;
final SmartDashesType? smartDashesType;
final SmartQuotesType? smartQuotesType;
final List<TextInputFormatter>? 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<ImmichTextInput> {
}
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<ImmichTextInput> {
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<ImmichTextInput> {
),
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,
);
}
}

View File

@ -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<TextInputFormatter> _formatters = List.unmodifiable([
FilteringTextInputFormatter.deny(RegExp(r'\s')),
]);
}