mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 20:07:51 -06:00
feat: Add suggestions to server login page
This commit is contained in:
@@ -20,6 +20,7 @@ class HiveBoxes {
|
|||||||
static const localUserAccount = 'localUserAccount';
|
static const localUserAccount = 'localUserAccount';
|
||||||
static const localUserAppState = 'localUserAppState';
|
static const localUserAppState = 'localUserAppState';
|
||||||
static const localUserSettings = 'localUserSettings';
|
static const localUserSettings = 'localUserSettings';
|
||||||
|
static const hosts = 'hosts';
|
||||||
}
|
}
|
||||||
|
|
||||||
class HiveTypeIds {
|
class HiveTypeIds {
|
||||||
|
|||||||
@@ -334,8 +334,7 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
|||||||
// TODO HACK to satisfy strictness
|
// TODO HACK to satisfy strictness
|
||||||
suggestionsCallback: suggestionsCallback,
|
suggestionsCallback: suggestionsCallback,
|
||||||
itemBuilder: itemBuilder,
|
itemBuilder: itemBuilder,
|
||||||
transitionBuilder: (context, suggestionsBox, controller) =>
|
transitionBuilder: (context, suggestionsBox, controller) => suggestionsBox,
|
||||||
suggestionsBox,
|
|
||||||
onSuggestionSelected: (T suggestion) {
|
onSuggestionSelected: (T suggestion) {
|
||||||
state.didChange(suggestion);
|
state.didChange(suggestion);
|
||||||
onSuggestionSelected?.call(suggestion);
|
onSuggestionSelected?.call(suggestion);
|
||||||
@@ -357,8 +356,7 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
|||||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
||||||
autoFlipDirection: autoFlipDirection,
|
autoFlipDirection: autoFlipDirection,
|
||||||
suggestionsBoxController: suggestionsBoxController,
|
suggestionsBoxController: suggestionsBoxController,
|
||||||
keepSuggestionsOnSuggestionSelected:
|
keepSuggestionsOnSuggestionSelected: keepSuggestionsOnSuggestionSelected,
|
||||||
keepSuggestionsOnSuggestionSelected,
|
|
||||||
hideKeyboard: hideKeyboard,
|
hideKeyboard: hideKeyboard,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
);
|
);
|
||||||
@@ -369,15 +367,14 @@ class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
|||||||
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
|
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class FormBuilderTypeAheadState<T>
|
class FormBuilderTypeAheadState<T> extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
|
||||||
extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
|
|
||||||
late TextEditingController _typeAheadController;
|
late TextEditingController _typeAheadController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_typeAheadController = widget.controller ??
|
_typeAheadController =
|
||||||
TextEditingController(text: _getTextString(initialValue));
|
widget.controller ?? TextEditingController(text: _getTextString(initialValue));
|
||||||
// _typeAheadController.addListener(_handleControllerChanged);
|
// _typeAheadController.addListener(_handleControllerChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
final userStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
final userStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||||
|
|
||||||
if (userAccountBox.containsKey(localUserId)) {
|
if (userAccountBox.containsKey(localUserId)) {
|
||||||
throw Exception("User with id $localUserId already exists!");
|
throw Exception("User already exists!");
|
||||||
}
|
}
|
||||||
final apiVersion = await _getApiVersion(sessionManager.client);
|
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;
|
return serverUser.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.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/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/features/users/view/widgets/user_account_list_tile.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.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/login_pages/server_login_page.dart';
|
||||||
import 'widgets/never_scrollable_scroll_behavior.dart';
|
import 'widgets/never_scrollable_scroll_behavior.dart';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
final void Function(
|
final FutureOr<void> Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
String username,
|
String username,
|
||||||
String password,
|
String password,
|
||||||
@@ -131,13 +134,17 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
|
||||||
widget.onSubmit(
|
try {
|
||||||
context,
|
await widget.onSubmit(
|
||||||
credentials.username!,
|
context,
|
||||||
credentials.password!,
|
credentials.username!,
|
||||||
form[ServerAddressFormField.fkServerAddress],
|
credentials.password!,
|
||||||
clientCert,
|
form[ServerAddressFormField.fkServerAddress],
|
||||||
);
|
clientCert,
|
||||||
|
);
|
||||||
|
} on Exception catch (error) {
|
||||||
|
showGenericError(context, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.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';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ServerAddressFormField extends StatefulWidget {
|
class ServerAddressFormField extends StatefulWidget {
|
||||||
static const String fkServerAddress = "serverAddress";
|
static const String fkServerAddress = "serverAddress";
|
||||||
|
|
||||||
final void Function(String? address) onDone;
|
final void Function(String? address) onSubmit;
|
||||||
const ServerAddressFormField({
|
const ServerAddressFormField({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onDone,
|
required this.onSubmit,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -24,21 +26,18 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_textEditingController.addListener(() {
|
_textEditingController.addListener(() {
|
||||||
if (_textEditingController.text.isNotEmpty) {
|
setState(() {
|
||||||
setState(() {
|
_canClear = _textEditingController.text.isNotEmpty;
|
||||||
_canClear = true;
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _focusNode = FocusNode();
|
||||||
final _textEditingController = TextEditingController();
|
final _textEditingController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderTextField(
|
return FormBuilderField<String>(
|
||||||
key: const ValueKey('login-server-address'),
|
|
||||||
controller: _textEditingController,
|
|
||||||
name: ServerAddressFormField.fkServerAddress,
|
name: ServerAddressFormField.fkServerAddress,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -50,20 +49,60 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
builder: (field) {
|
||||||
hintText: "http://192.168.1.50:8000",
|
return RawAutocomplete<String>(
|
||||||
labelText: S.of(context)!.serverAddress,
|
focusNode: _focusNode,
|
||||||
suffixIcon: _canClear
|
textEditingController: _textEditingController,
|
||||||
? IconButton(
|
optionsViewBuilder: (context, onSelected, options) {
|
||||||
icon: const Icon(Icons.clear),
|
return _AutocompleteOptions(
|
||||||
color: Theme.of(context).iconTheme.color,
|
onSelected: onSelected,
|
||||||
onPressed: () {
|
options: options,
|
||||||
_textEditingController.clear();
|
maxOptionsHeight: 200.0,
|
||||||
},
|
);
|
||||||
)
|
},
|
||||||
: null,
|
key: const ValueKey('login-server-address'),
|
||||||
),
|
optionsBuilder: (textEditingValue) {
|
||||||
onSubmitted: (_) => _formatInput(),
|
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();
|
String address = _textEditingController.text.trim();
|
||||||
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
|
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
|
||||||
_textEditingController.text = address;
|
_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),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ServerAddressFormField(
|
ServerAddressFormField(
|
||||||
onDone: (address) {
|
onSubmit: (address) {
|
||||||
_updateReachability(address);
|
_updateReachability(address);
|
||||||
},
|
},
|
||||||
).padded(),
|
).padded(),
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ Future<void> _initHive() async {
|
|||||||
// await getApplicationDocumentsDirectory().then((value) => value.deleteSync(recursive: true));
|
// await getApplicationDocumentsDirectory().then((value) => value.deleteSync(recursive: true));
|
||||||
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
|
await Hive.openBox<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||||
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
|
await Hive.openBox<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||||
|
await Hive.openBox<String>(HiveBoxes.hosts);
|
||||||
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
final globalSettingsBox = await Hive.openBox<GlobalSettings>(HiveBoxes.globalSettings);
|
||||||
|
|
||||||
if (!globalSettingsBox.hasValue) {
|
if (!globalSettingsBox.hasValue) {
|
||||||
|
|||||||
Reference in New Issue
Block a user