refactor: immich form and text input (#28479)
refacotr: immich form Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>pull/28482/head
parent
0544d22902
commit
40925f0a06
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<void> 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<void> Function()? onSubmit;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
State<ImmichForm> 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<ImmichForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
FutureOr<void> submit() async {
|
||||
final isValid = _formKey.currentState?.validate() ?? false;
|
||||
if (!isValid) {
|
||||
Future<void> 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<void> 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<ImmichForm> createState() => _ImmichFormState();
|
||||
}
|
||||
|
||||
class _ImmichFormState extends State<ImmichForm> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<String>? 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<ImmichTextInput> {
|
||||
late final FocusNode _focusNode;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -65,45 +64,20 @@ class _ImmichTextInputState extends State<ImmichTextInput> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class _FormPageState extends State<FormPage> {
|
|||
_result = 'Form submitted!';
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
builder: (context, form) => Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
ImmichTextInput(
|
||||
|
|
@ -54,6 +54,7 @@ class _FormPageState extends State<FormPage> {
|
|||
controller: _passwordController,
|
||||
validator: (value) =>
|
||||
value?.isEmpty ?? true ? 'Required' : null,
|
||||
onSubmit: (_) => form.submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue