mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 08:08:14 -06:00
Added server address validation, success message, localization
This commit is contained in:
@@ -83,8 +83,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
-4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
||||
isLabelVisible: appliedFiltersCount > 0,
|
||||
count: state.filter.appliedFiltersCount,
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
backgroundColor: Colors.red,
|
||||
textColor: Colors.white,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
@@ -115,7 +115,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
expand: false,
|
||||
snap: true,
|
||||
initialChildSize: .9,
|
||||
maxChildSize: .9,
|
||||
snapSizes: const [.9, 1],
|
||||
builder: (context, controller) => LabelsBlocProvider(
|
||||
child: DocumentFilterPanel(
|
||||
initialFilter: context.read<DocumentsCubit>().state.filter,
|
||||
|
||||
@@ -70,65 +70,74 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
controller: widget.scrollController,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
S.of(context).documentFilterTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddedOnly(
|
||||
top: 16.0,
|
||||
left: 16.0,
|
||||
bottom: 24,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
S.of(context).documentFilterSearchLabel,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
).paddedOnly(left: 8.0),
|
||||
_buildQueryFormField().padded(),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
S.of(context).documentFilterAdvancedLabel,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
).padded(),
|
||||
FormBuilderExtendedDateRangePicker(
|
||||
name: DocumentModel.createdKey,
|
||||
initialValue: widget.initialFilter.created,
|
||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||
).padded(),
|
||||
FormBuilderExtendedDateRangePicker(
|
||||
name: DocumentModel.addedKey,
|
||||
initialValue: widget.initialFilter.added,
|
||||
labelText: S.of(context).documentAddedPropertyLabel,
|
||||
).padded(),
|
||||
_buildCorrespondentFormField().padded(),
|
||||
_buildDocumentTypeFormField().padded(),
|
||||
_buildStoragePathFormField().padded(),
|
||||
_buildTagsFormField().padded(),
|
||||
],
|
||||
).paddedOnly(bottom: 16),
|
||||
child: _buildFormList(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ListView _buildFormList(BuildContext context) {
|
||||
return ListView(
|
||||
controller: widget.scrollController,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: _buildDragHandle(context),
|
||||
),
|
||||
Text(
|
||||
S.of(context).documentFilterTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddedOnly(
|
||||
top: 16.0,
|
||||
left: 16.0,
|
||||
bottom: 24,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
S.of(context).documentFilterSearchLabel,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
).paddedOnly(left: 8.0),
|
||||
_buildQueryFormField().padded(),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
S.of(context).documentFilterAdvancedLabel,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
).padded(),
|
||||
FormBuilderExtendedDateRangePicker(
|
||||
name: DocumentModel.createdKey,
|
||||
initialValue: widget.initialFilter.created,
|
||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||
).padded(),
|
||||
FormBuilderExtendedDateRangePicker(
|
||||
name: DocumentModel.addedKey,
|
||||
initialValue: widget.initialFilter.added,
|
||||
labelText: S.of(context).documentAddedPropertyLabel,
|
||||
).padded(),
|
||||
_buildCorrespondentFormField().padded(),
|
||||
_buildDocumentTypeFormField().padded(),
|
||||
_buildStoragePathFormField().padded(),
|
||||
_buildTagsFormField().padded(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildDragHandle(BuildContext context) {
|
||||
return Container(
|
||||
// According to m3 spec
|
||||
width: 32,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
|
||||
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
|
||||
builder: (context, state) {
|
||||
|
||||
76
lib/features/home/view/widget/verify_identity_page.dart
Normal file
76
lib/features/home/view/widget/verify_identity_page.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VerifyIdentityPage extends StatelessWidget {
|
||||
const VerifyIdentityPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
title: Text(S.of(context).verifyIdentityPageTitle),
|
||||
),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).verifyIdentityPageDescriptionText)
|
||||
.paddedSymmetrically(horizontal: 16),
|
||||
const Icon(
|
||||
Icons.fingerprint,
|
||||
size: 96,
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
runAlignment: WrapAlignment.spaceBetween,
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => _logout(context),
|
||||
child: Text(
|
||||
S.of(context).verifyIdentityPageLogoutButtonLabel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context
|
||||
.read<AuthenticationCubit>()
|
||||
.restoreSessionState(context
|
||||
.read<ApplicationSettingsCubit>()
|
||||
.state
|
||||
.isLocalAuthenticationEnabled),
|
||||
child: Text(S
|
||||
.of(context)
|
||||
.verifyIdentityPageVerifyIdentityButtonLabel),
|
||||
),
|
||||
],
|
||||
).padded(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _logout(BuildContext context) {
|
||||
context.read<AuthenticationCubit>().logout();
|
||||
context.read<LabelRepository<Tag>>().clear();
|
||||
context.read<LabelRepository<Correspondent>>().clear();
|
||||
context.read<LabelRepository<DocumentType>>().clear();
|
||||
context.read<LabelRepository<StoragePath>>().clear();
|
||||
context.read<SavedViewRepository>().clear();
|
||||
HydratedBloc.storage.clear();
|
||||
}
|
||||
}
|
||||
@@ -17,26 +17,49 @@ class ServerAddressFormField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
static const _ipv4Regex = r"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}";
|
||||
static const _ipv6Regex =
|
||||
r"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))";
|
||||
static final _urlRegex = RegExp(
|
||||
r"^(https?:\/\/)(([\da-z\.-]+)\.([a-z\.]{2,6})|(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})|((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))))(:\d{1,5})?([\/\w \.-]*)*\/?$");
|
||||
final TextEditingController _textEditingController = TextEditingController();
|
||||
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.undefined;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderTextField(
|
||||
key: const ValueKey('login-server-address'),
|
||||
controller: _textEditingController,
|
||||
name: ServerAddressFormField.fkServerAddress,
|
||||
validator: FormBuilderValidators.required(
|
||||
errorText: S.of(context).loginPageServerUrlValidatorMessageText,
|
||||
validator: FormBuilderValidators.compose(
|
||||
[
|
||||
FormBuilderValidators.required(
|
||||
errorText:
|
||||
S.of(context).loginPageServerUrlValidatorMessageRequiredText,
|
||||
),
|
||||
FormBuilderValidators.match(
|
||||
_urlRegex.pattern,
|
||||
errorText: S
|
||||
.of(context)
|
||||
.loginPageServerUrlValidatorMessageInvalidAddressText,
|
||||
),
|
||||
],
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(r".*/$"),
|
||||
FilteringTextInputFormatter.deny(r"\s"),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: _buildIsReachableIcon(),
|
||||
hintText: "http://192.168.1.50:8000",
|
||||
labelText: S.of(context).loginPageServerUrlFieldLabel,
|
||||
),
|
||||
onChanged: _updateIsAddressReachableStatus,
|
||||
onSubmitted: (value) {
|
||||
if (value == null) return;
|
||||
// Remove trailing slash if it is a valid address.
|
||||
final address = value.trim();
|
||||
_textEditingController.text = address;
|
||||
if (_urlRegex.hasMatch(address) && address.endsWith("/")) {
|
||||
_textEditingController.text = address.replaceAll(RegExp(r'\/$'), '');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +83,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
}
|
||||
|
||||
void _updateIsAddressReachableStatus(String? address) async {
|
||||
if (address == null || address.isEmpty) {
|
||||
if (address == null || !_urlRegex.hasMatch(address)) {
|
||||
setState(() {
|
||||
_reachabilityStatus = ReachabilityStatus.undefined;
|
||||
});
|
||||
@@ -70,12 +93,12 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
setState(() => _reachabilityStatus = ReachabilityStatus.testing);
|
||||
final isReachable = await context
|
||||
.read<ConnectivityStatusService>()
|
||||
.isServerReachable(address);
|
||||
if (isReachable) {
|
||||
setState(() => _reachabilityStatus = ReachabilityStatus.reachable);
|
||||
} else {
|
||||
setState(() => _reachabilityStatus = ReachabilityStatus.notReachable);
|
||||
}
|
||||
.isServerReachable(address.trim());
|
||||
setState(
|
||||
() => _reachabilityStatus = isReachable
|
||||
? ReachabilityStatus.reachable
|
||||
: ReachabilityStatus.notReachable,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user