diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index e5505a3288..f3abd2a03d 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -397,16 +397,16 @@ class LoginForm extends HookConsumerWidget { mainAxisSize: MainAxisSize.max, children: [ ImmichForm( + onSubmit: getServerAuthSettings, submitText: 'next'.t(context: context), submitIcon: Icons.arrow_forward_rounded, - onSubmit: getServerAuthSettings, - child: ImmichURLInput( + builder: (_, form) => ImmichURLInput( controller: serverEndpointController, label: 'login_form_endpoint_url'.t(context: context), hintText: 'login_form_endpoint_hint'.t(context: context), validator: _validateUrl, keyboardAction: .next, - onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), + onSubmit: (_) => form.submit(), ), ), ImmichTextButton( @@ -434,10 +434,10 @@ class LoginForm extends HookConsumerWidget { ), if (isPasswordLoginEnable.value) ImmichForm( + onSubmit: login, submitText: 'login'.t(context: context), submitIcon: Icons.login_rounded, - onSubmit: login, - child: Column( + builder: (context, form) => Column( spacing: ImmichSpacing.md, children: [ ImmichTextInput( @@ -448,7 +448,7 @@ class LoginForm extends HookConsumerWidget { keyboardAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, autofillHints: const [AutofillHints.email], - onSubmit: (_, _) => passwordFocusNode.requestFocus(), + onSubmit: (_) => passwordFocusNode.requestFocus(), ), ImmichPasswordInput( controller: passwordController, @@ -456,17 +456,17 @@ class LoginForm extends HookConsumerWidget { label: 'password'.t(context: context), hintText: 'login_form_password_hint'.t(context: context), keyboardAction: TextInputAction.go, - onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), + onSubmit: (_) => form.submit(), ), ], ), ), if (isOauthEnable.value) ImmichForm( + onSubmit: oAuthLogin, submitText: oAuthButtonLabel.value, submitIcon: Icons.pin_outlined, - onSubmit: oAuthLogin, - child: isPasswordLoginEnable.value + builder: (context, _) => isPasswordLoginEnable.value ? Padding( padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 12.0), child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black, height: 5), diff --git a/mobile/packages/ui/lib/src/components/form.dart b/mobile/packages/ui/lib/src/components/form.dart index 9e8c161806..4d0344ae6b 100644 --- a/mobile/packages/ui/lib/src/components/form.dart +++ b/mobile/packages/ui/lib/src/components/form.dart @@ -4,95 +4,95 @@ import 'package:flutter/material.dart'; import 'package:immich_ui/immich_ui.dart'; import 'package:immich_ui/src/internal.dart'; -class ImmichForm extends StatefulWidget { - final String? submitText; - final IconData? submitIcon; - final FutureOr Function()? onSubmit; - final Widget child; +class ImmichFormController extends ChangeNotifier { + ImmichFormController({this.onSubmit}); - const ImmichForm({ - super.key, - this.submitText, - this.submitIcon, - required this.onSubmit, - required this.child, - }); + FutureOr Function()? onSubmit; + final formKey = GlobalKey(); - @override - State createState() => ImmichFormState(); - - static ImmichFormState of(BuildContext context) { - final scope = context.dependOnInheritedWidgetOfExactType<_ImmichFormScope>(); - if (scope == null) { - throw FlutterError( - 'ImmichForm.of() called with a context that does not contain an ImmichForm.\n' - 'No ImmichForm ancestor could be found starting from the context that was passed to ' - 'ImmichForm.of(). This usually happens when the context provided is ' - 'from a widget above the ImmichForm.\n' - 'The context used was:\n' - '$context', - ); - } - return scope._formState; - } -} - -class ImmichFormState extends State { - final _formKey = GlobalKey(); bool _isLoading = false; + bool get isLoading => _isLoading; - FutureOr submit() async { - final isValid = _formKey.currentState?.validate() ?? false; - if (!isValid) { + Future submit() async { + if (_isLoading) { + return; + } + if (!(formKey.currentState?.validate() ?? false)) { return; } - setState(() { - _isLoading = true; - }); - + _isLoading = true; + notifyListeners(); try { - await widget.onSubmit?.call(); + await onSubmit?.call(); } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); - } + _isLoading = false; + notifyListeners(); } } +} + +class ImmichForm extends StatefulWidget { + final FutureOr Function()? onSubmit; + final Widget Function(BuildContext context, ImmichFormController form) builder; + final String? submitText; + final IconData? submitIcon; + + const ImmichForm({ + super.key, + this.onSubmit, + this.submitText, + this.submitIcon, + required this.builder, + }); + + @override + State createState() => _ImmichFormState(); +} + +class _ImmichFormState extends State { + late final ImmichFormController _controller; + + @override + void initState() { + super.initState(); + _controller = ImmichFormController(onSubmit: widget.onSubmit); + } + + @override + void didUpdateWidget(ImmichForm oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.onSubmit = widget.onSubmit; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final submitText = widget.submitText ?? context.translations.submit; - return _ImmichFormScope( - formState: this, - child: Form( - key: _formKey, - child: Column( - spacing: ImmichSpacing.md, - children: [ - widget.child, - ImmichTextButton( + return Form( + key: _controller.formKey, + child: Column( + spacing: ImmichSpacing.md, + children: [ + widget.builder(context, _controller), + ListenableBuilder( + listenable: _controller, + builder: (context, _) => ImmichTextButton( labelText: submitText, icon: widget.submitIcon, variant: ImmichVariant.filled, - loading: _isLoading, - onPressed: submit, - disabled: widget.onSubmit == null, + loading: _controller.isLoading, + onPressed: _controller.submit, + disabled: _controller.onSubmit == null, ), - ], - ), + ), + ], ), ); } } - -class _ImmichFormScope extends InheritedWidget { - const _ImmichFormScope({required super.child, required ImmichFormState formState}) : _formState = formState; - - final ImmichFormState _formState; - - @override - bool updateShouldNotify(_ImmichFormScope oldWidget) => oldWidget._formState != _formState; -} diff --git a/mobile/packages/ui/lib/src/components/password_input.dart b/mobile/packages/ui/lib/src/components/password_input.dart index bd5a149354..70808472df 100644 --- a/mobile/packages/ui/lib/src/components/password_input.dart +++ b/mobile/packages/ui/lib/src/components/password_input.dart @@ -8,7 +8,7 @@ class ImmichPasswordInput extends StatefulWidget { final TextEditingController? controller; final FocusNode? focusNode; final String? Function(String?)? validator; - final void Function(BuildContext, String)? onSubmit; + final void Function(String value)? onSubmit; final TextInputAction? keyboardAction; const ImmichPasswordInput({ diff --git a/mobile/packages/ui/lib/src/components/text_input.dart b/mobile/packages/ui/lib/src/components/text_input.dart index 627af15d34..71d3248ac1 100644 --- a/mobile/packages/ui/lib/src/components/text_input.dart +++ b/mobile/packages/ui/lib/src/components/text_input.dart @@ -7,7 +7,7 @@ class ImmichTextInput extends StatefulWidget { final TextEditingController? controller; final FocusNode? focusNode; final String? Function(String?)? validator; - final void Function(BuildContext, String)? onSubmit; + final void Function(String value)? onSubmit; final TextInputType keyboardType; final TextInputAction? keyboardAction; final List? autofillHints; @@ -29,7 +29,7 @@ class ImmichTextInput extends StatefulWidget { this.hintText, this.validator, this.onSubmit, - this.keyboardType = TextInputType.text, + this.keyboardType = .text, this.keyboardAction, this.autofillHints, this.suffixIcon, @@ -49,7 +49,6 @@ class ImmichTextInput extends StatefulWidget { class _ImmichTextInputState extends State { late final FocusNode _focusNode; - String? _error; @override void initState() { @@ -65,45 +64,20 @@ class _ImmichTextInputState extends State { super.dispose(); } - String? _validateInput(String? value) { - final error = widget.validator?.call(value); - if (error != _error) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() => _error = error); - } - }); - } - return null; - } - - bool get _hasError => _error != null && _error!.isNotEmpty; - @override Widget build(BuildContext context) { - final themeData = Theme.of(context); - return TextFormField( controller: widget.controller, focusNode: _focusNode, enabled: widget.enabled, autofocus: widget.autofocus, autovalidateMode: widget.autovalidateMode, - decoration: InputDecoration( - hintText: widget.hintText, - labelText: widget.label, - labelStyle: themeData.inputDecorationTheme.labelStyle?.copyWith( - color: _hasError ? themeData.colorScheme.error : null, - ), - errorText: _error, - suffixIcon: widget.suffixIcon, - ), + decoration: InputDecoration(hintText: widget.hintText, labelText: widget.label, suffixIcon: widget.suffixIcon), obscureText: widget.obscureText, - validator: _validateInput, + validator: widget.validator, textInputAction: widget.keyboardAction, - onTap: () => setState(() => _error = null), onTapOutside: (_) => _focusNode.unfocus(), - onFieldSubmitted: (value) => widget.onSubmit?.call(context, value), + onFieldSubmitted: (value) => widget.onSubmit?.call(value), keyboardType: widget.keyboardType, autofillHints: widget.autofillHints, autocorrect: widget.autocorrect, diff --git a/mobile/packages/ui/lib/src/theme.dart b/mobile/packages/ui/lib/src/theme.dart index 387723b8ce..891b41ee91 100644 --- a/mobile/packages/ui/lib/src/theme.dart +++ b/mobile/packages/ui/lib/src/theme.dart @@ -15,23 +15,21 @@ class ImmichThemeProvider extends StatelessWidget { brightness: colorScheme.brightness, inputDecorationTheme: InputDecorationTheme( floatingLabelBehavior: FloatingLabelBehavior.always, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.primary), - borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.primary), - borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.error), - borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.error), - borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), - ), - labelStyle: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w600), + border: WidgetStateInputBorder.resolveWith((states) { + final color = states.contains(WidgetState.error) + ? colorScheme.error + : states.contains(WidgetState.focused) + ? colorScheme.primary + : colorScheme.outline; + return OutlineInputBorder( + borderSide: BorderSide(color: color), + borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), + ); + }), + labelStyle: WidgetStateTextStyle.resolveWith((states) { + final color = states.contains(WidgetState.error) ? colorScheme.error : colorScheme.primary; + return TextStyle(color: color, fontWeight: FontWeight.w600); + }), hintStyle: const TextStyle(fontSize: ImmichTextSize.body), errorStyle: TextStyle(color: colorScheme.error, fontWeight: FontWeight.w600), ), diff --git a/mobile/packages/ui/showcase/lib/pages/components/form_page.dart b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart index 14567031de..f4480026b3 100644 --- a/mobile/packages/ui/showcase/lib/pages/components/form_page.dart +++ b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart @@ -39,7 +39,7 @@ class _FormPageState extends State { _result = 'Form submitted!'; }); }, - child: Column( + builder: (context, form) => Column( spacing: 10, children: [ ImmichTextInput( @@ -54,6 +54,7 @@ class _FormPageState extends State { controller: _passwordController, validator: (value) => value?.isEmpty ?? true ? 'Required' : null, + onSubmit: (_) => form.submit(), ), ], ),