feat: Add suggestions to server login page

This commit is contained in:
Anton Stubenbord
2023-06-02 16:37:03 +02:00
parent 31447087e1
commit d2b428c05b
7 changed files with 146 additions and 44 deletions

View File

@@ -20,6 +20,7 @@ class HiveBoxes {
static const localUserAccount = 'localUserAccount';
static const localUserAppState = 'localUserAppState';
static const localUserSettings = 'localUserSettings';
static const hosts = 'hosts';
}
class HiveTypeIds {

View File

@@ -334,8 +334,7 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
// 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<T> extends FormBuilderField<T> {
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
autoFlipDirection: autoFlipDirection,
suggestionsBoxController: suggestionsBoxController,
keepSuggestionsOnSuggestionSelected:
keepSuggestionsOnSuggestionSelected,
keepSuggestionsOnSuggestionSelected: keepSuggestionsOnSuggestionSelected,
hideKeyboard: hideKeyboard,
scrollController: scrollController,
);
@@ -369,15 +367,14 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
}
class FormBuilderTypeAheadState<T>
extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
class FormBuilderTypeAheadState<T> extends FormBuilderFieldState<FormBuilderTypeAhead<T>, 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);
}

View File

@@ -244,7 +244,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final userStateBox = Hive.box<LocalUserAppState>(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<AuthenticationState> {
),
);
});
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
if (!hostsBox.values.contains(serverUrl)) {
await hostsBox.add(serverUrl);
}
return serverUser.id;
}

View File

@@ -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<void> Function(
BuildContext context,
String username,
String password,
@@ -131,13 +134,17 @@ class _LoginPageState extends State<LoginPage> {
);
}
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);
}
}
}
}

View File

@@ -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<ServerAddressFormField> {
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<String>(
name: ServerAddressFormField.fkServerAddress,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
@@ -50,20 +49,60 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
}
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<String>(
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<String>(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<ServerAddressFormField> {
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<String> onSelected;
final Iterable<String> 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),
);
}),
);
},
),
),
),
);
}
}

View File

@@ -48,7 +48,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
child: Column(
children: [
ServerAddressFormField(
onDone: (address) {
onSubmit: (address) {
_updateReachability(address);
},
).padded(),

View File

@@ -64,6 +64,7 @@ Future<void> _initHive() async {
// await getApplicationDocumentsDirectory().then((value) => value.deleteSync(recursive: true));
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
await Hive.openBox<String>(HiveBoxes.hosts);
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
if (!globalSettingsBox.hasValue) {