From d2b428c05ba55f12da42902ef626f4c93715d533 Mon Sep 17 00:00:00 2001 From: Anton Stubenbord Date: Fri, 2 Jun 2023 16:37:03 +0200 Subject: [PATCH] feat: Add suggestions to server login page --- lib/core/config/hive/hive_config.dart | 1 + .../form_builder_type_ahead.dart | 13 +- .../login/cubit/authentication_cubit.dart | 6 +- lib/features/login/view/login_page.dart | 23 ++- .../server_address_form_field.dart | 144 ++++++++++++++---- .../login_pages/server_connection_page.dart | 2 +- lib/main.dart | 1 + 7 files changed, 146 insertions(+), 44 deletions(-) diff --git a/lib/core/config/hive/hive_config.dart b/lib/core/config/hive/hive_config.dart index dc231af..c0d8f7b 100644 --- a/lib/core/config/hive/hive_config.dart +++ b/lib/core/config/hive/hive_config.dart @@ -20,6 +20,7 @@ class HiveBoxes { static const localUserAccount = 'localUserAccount'; static const localUserAppState = 'localUserAppState'; static const localUserSettings = 'localUserSettings'; + static const hosts = 'hosts'; } class HiveTypeIds { diff --git a/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart b/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart index bbd8480..18b9f50 100644 --- a/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart +++ b/lib/core/widgets/form_builder_fields/form_builder_type_ahead.dart @@ -334,8 +334,7 @@ class FormBuilderTypeAhead extends FormBuilderField { // TODO HACK to satisfy strictness suggestionsCallback: suggestionsCallback, itemBuilder: itemBuilder, - transitionBuilder: (context, suggestionsBox, controller) => - suggestionsBox, + transitionBuilder: (context, suggestionsBox, controller) => suggestionsBox, onSuggestionSelected: (T suggestion) { state.didChange(suggestion); onSuggestionSelected?.call(suggestion); @@ -357,8 +356,7 @@ class FormBuilderTypeAhead extends FormBuilderField { keepSuggestionsOnLoading: keepSuggestionsOnLoading, autoFlipDirection: autoFlipDirection, suggestionsBoxController: suggestionsBoxController, - keepSuggestionsOnSuggestionSelected: - keepSuggestionsOnSuggestionSelected, + keepSuggestionsOnSuggestionSelected: keepSuggestionsOnSuggestionSelected, hideKeyboard: hideKeyboard, scrollController: scrollController, ); @@ -369,15 +367,14 @@ class FormBuilderTypeAhead extends FormBuilderField { FormBuilderTypeAheadState createState() => FormBuilderTypeAheadState(); } -class FormBuilderTypeAheadState - extends FormBuilderFieldState, T> { +class FormBuilderTypeAheadState extends FormBuilderFieldState, T> { late TextEditingController _typeAheadController; @override void initState() { super.initState(); - _typeAheadController = widget.controller ?? - TextEditingController(text: _getTextString(initialValue)); + _typeAheadController = + widget.controller ?? TextEditingController(text: _getTextString(initialValue)); // _typeAheadController.addListener(_handleControllerChanged); } diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index 2afdd1a..1cdb368 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -244,7 +244,7 @@ class AuthenticationCubit extends Cubit { final userStateBox = Hive.box(HiveBoxes.localUserAppState); if (userAccountBox.containsKey(localUserId)) { - throw Exception("User with id $localUserId already exists!"); + throw Exception("User already exists!"); } final apiVersion = await _getApiVersion(sessionManager.client); @@ -282,6 +282,10 @@ class AuthenticationCubit extends Cubit { ), ); }); + final hostsBox = Hive.box(HiveBoxes.hosts); + if (!hostsBox.values.contains(serverUrl)) { + await hostsBox.add(serverUrl); + } return serverUser.id; } diff --git a/lib/features/login/view/login_page.dart b/lib/features/login/view/login_page.dart index 3e63b36..229bdb1 100644 --- a/lib/features/login/view/login_page.dart +++ b/lib/features/login/view/login_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; @@ -14,12 +16,13 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart'; import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; +import 'package:paperless_mobile/helpers/message_helpers.dart'; import 'widgets/login_pages/server_login_page.dart'; import 'widgets/never_scrollable_scroll_behavior.dart'; class LoginPage extends StatefulWidget { - final void Function( + final FutureOr Function( BuildContext context, String username, String password, @@ -131,13 +134,17 @@ class _LoginPageState extends State { ); } final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials; - widget.onSubmit( - context, - credentials.username!, - credentials.password!, - form[ServerAddressFormField.fkServerAddress], - clientCert, - ); + try { + await widget.onSubmit( + context, + credentials.username!, + credentials.password!, + form[ServerAddressFormField.fkServerAddress], + clientCert, + ); + } on Exception catch (error) { + showGenericError(context, error); + } } } } diff --git a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart index 9d0b5a0..6b0d3eb 100644 --- a/lib/features/login/view/widgets/form_fields/server_address_form_field.dart +++ b/lib/features/login/view/widgets/form_fields/server_address_form_field.dart @@ -1,16 +1,18 @@ - import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; class ServerAddressFormField extends StatefulWidget { static const String fkServerAddress = "serverAddress"; - final void Function(String? address) onDone; + final void Function(String? address) onSubmit; const ServerAddressFormField({ Key? key, - required this.onDone, + required this.onSubmit, }) : super(key: key); @override @@ -24,21 +26,18 @@ class _ServerAddressFormFieldState extends State { void initState() { super.initState(); _textEditingController.addListener(() { - if (_textEditingController.text.isNotEmpty) { - setState(() { - _canClear = true; - }); - } + setState(() { + _canClear = _textEditingController.text.isNotEmpty; + }); }); } + final _focusNode = FocusNode(); final _textEditingController = TextEditingController(); @override Widget build(BuildContext context) { - return FormBuilderTextField( - key: const ValueKey('login-server-address'), - controller: _textEditingController, + return FormBuilderField( name: ServerAddressFormField.fkServerAddress, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { @@ -50,20 +49,60 @@ class _ServerAddressFormFieldState extends State { } return null; }, - decoration: InputDecoration( - hintText: "http://192.168.1.50:8000", - labelText: S.of(context)!.serverAddress, - suffixIcon: _canClear - ? IconButton( - icon: const Icon(Icons.clear), - color: Theme.of(context).iconTheme.color, - onPressed: () { - _textEditingController.clear(); - }, - ) - : null, - ), - onSubmitted: (_) => _formatInput(), + builder: (field) { + return RawAutocomplete( + focusNode: _focusNode, + textEditingController: _textEditingController, + optionsViewBuilder: (context, onSelected, options) { + return _AutocompleteOptions( + onSelected: onSelected, + options: options, + maxOptionsHeight: 200.0, + ); + }, + key: const ValueKey('login-server-address'), + optionsBuilder: (textEditingValue) { + return Hive.box(HiveBoxes.hosts) + .values + .where((element) => element.contains(textEditingValue.text)); + }, + onSelected: (option) => _formatInput(), + fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + decoration: InputDecoration( + hintText: "http://192.168.1.50:8000", + labelText: S.of(context)!.serverAddress, + suffixIcon: _canClear + ? IconButton( + icon: const Icon(Icons.clear), + color: Theme.of(context).iconTheme.color, + onPressed: () { + textEditingController.clear(); + field.didChange(textEditingController.text); + widget.onSubmit(textEditingController.text); + }, + ) + : null, + ), + autofocus: true, + onSubmitted: (_) { + onFieldSubmitted(); + _formatInput(); + }, + keyboardType: TextInputType.url, + onChanged: (value) { + field.didChange(value); + }, + onEditingComplete: () { + field.didChange(_textEditingController.text); + _focusNode.unfocus(); + }, + ); + }, + ); + }, ); } @@ -71,6 +110,59 @@ class _ServerAddressFormFieldState extends State { String address = _textEditingController.text.trim(); address = address.replaceAll(RegExp(r'^\/+|\/+$'), ''); _textEditingController.text = address; - widget.onDone(address); + widget.onSubmit(address); + } +} + +/// Taken from [Autocomplete] +class _AutocompleteOptions extends StatelessWidget { + const _AutocompleteOptions({ + required this.onSelected, + required this.options, + required this.maxOptionsHeight, + }); + + final AutocompleteOnSelected onSelected; + + final Iterable options; + final double maxOptionsHeight; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4.0, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxOptionsHeight), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final option = options.elementAt(index); + return InkWell( + onTap: () { + onSelected(option); + }, + child: Builder(builder: (BuildContext context) { + final bool highlight = AutocompleteHighlightedOption.of(context) == index; + if (highlight) { + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { + Scrollable.ensureVisible(context, alignment: 0.5); + }); + } + return Container( + color: highlight ? Theme.of(context).focusColor : null, + padding: const EdgeInsets.all(16.0), + child: Text(option), + ); + }), + ); + }, + ), + ), + ), + ); } } diff --git a/lib/features/login/view/widgets/login_pages/server_connection_page.dart b/lib/features/login/view/widgets/login_pages/server_connection_page.dart index eaf6b66..3662f65 100644 --- a/lib/features/login/view/widgets/login_pages/server_connection_page.dart +++ b/lib/features/login/view/widgets/login_pages/server_connection_page.dart @@ -48,7 +48,7 @@ class _ServerConnectionPageState extends State { child: Column( children: [ ServerAddressFormField( - onDone: (address) { + onSubmit: (address) { _updateReachability(address); }, ).padded(), diff --git a/lib/main.dart b/lib/main.dart index e6399eb..5dc43f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -64,6 +64,7 @@ Future _initHive() async { // await getApplicationDocumentsDirectory().then((value) => value.deleteSync(recursive: true)); await Hive.openBox(HiveBoxes.localUserAccount); await Hive.openBox(HiveBoxes.localUserAppState); + await Hive.openBox(HiveBoxes.hosts); final globalSettingsBox = await Hive.openBox(HiveBoxes.globalSettings); if (!globalSettingsBox.hasValue) {