mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 10:08:00 -06:00
WIP - Redesigned login flow
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:introduction_screen/introduction_screen.dart';
|
||||
import 'package:paperless_mobile/core/global/asset_images.dart';
|
||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/biometric_authentication_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart';
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ConfigurationDoneIntroSlide extends StatelessWidget {
|
||||
const ConfigurationDoneIntroSlide({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
//TODO: INTL
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
"All set up!",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Icon(
|
||||
Icons.emoji_emotions_outlined,
|
||||
size: 64,
|
||||
),
|
||||
Text(
|
||||
"You've successfully configured Paperless Mobile! Press 'GO' to get started managing your documents.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -449,7 +449,7 @@ class _DetailsItem extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
content,
|
||||
],
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
part 'document_upload_state.dart';
|
||||
|
||||
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
final LocalVault _localVault;
|
||||
final PaperlessDocumentsApi _documentApi;
|
||||
|
||||
final LabelRepository<Tag> _tagRepository;
|
||||
@@ -29,7 +28,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
_tagRepository = tagRepository,
|
||||
_correspondentRepository = correspondentRepository,
|
||||
_documentTypeRepository = documentTypeRepository,
|
||||
_localVault = localVault,
|
||||
super(
|
||||
const DocumentUploadState(
|
||||
tags: {},
|
||||
@@ -37,13 +35,13 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
documentTypes: {},
|
||||
),
|
||||
) {
|
||||
_subs.add(_tagRepository.labels.listen(
|
||||
_subs.add(_tagRepository.values.listen(
|
||||
(tags) => emit(state.copyWith(tags: tags)),
|
||||
));
|
||||
_subs.add(_correspondentRepository.labels.listen(
|
||||
_subs.add(_correspondentRepository.values.listen(
|
||||
(correspondents) => emit(state.copyWith(correspondents: correspondents)),
|
||||
));
|
||||
_subs.add(_documentTypeRepository.labels.listen(
|
||||
_subs.add(_documentTypeRepository.values.listen(
|
||||
(documentTypes) => emit(state.copyWith(documentTypes: documentTypes)),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:developer' as dev;
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
@@ -14,7 +12,6 @@ import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
|
||||
enum DateRangeSelection { before, after }
|
||||
|
||||
|
||||
@@ -34,26 +34,26 @@ class EditDocumentCubit extends Cubit<EditDocumentState> {
|
||||
super(
|
||||
EditDocumentState(
|
||||
document: document,
|
||||
correspondents: correspondentRepository.current,
|
||||
documentTypes: documentTypeRepository.current,
|
||||
storagePaths: storagePathRepository.current,
|
||||
tags: tagRepository.current,
|
||||
correspondents: correspondentRepository.current ?? {},
|
||||
documentTypes: documentTypeRepository.current ?? {},
|
||||
storagePaths: storagePathRepository.current ?? {},
|
||||
tags: tagRepository.current ?? {},
|
||||
),
|
||||
) {
|
||||
_subscriptions.add(
|
||||
_correspondentRepository.labels
|
||||
_correspondentRepository.values
|
||||
.listen((v) => emit(state.copyWith(correspondents: v))),
|
||||
);
|
||||
_subscriptions.add(
|
||||
_documentTypeRepository.labels
|
||||
_documentTypeRepository.values
|
||||
.listen((v) => emit(state.copyWith(documentTypes: v))),
|
||||
);
|
||||
_subscriptions.add(
|
||||
_storagePathRepository.labels
|
||||
_storagePathRepository.values
|
||||
.listen((v) => emit(state.copyWith(storagePaths: v))),
|
||||
);
|
||||
_subscriptions.add(
|
||||
_tagRepository.labels.listen(
|
||||
_tagRepository.values.listen(
|
||||
(v) => emit(state.copyWith(tags: v)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,13 +8,14 @@ import 'package:paperless_mobile/features/edit_label/cubit/edit_label_state.dart
|
||||
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
|
||||
final LabelRepository<T> _repository;
|
||||
|
||||
StreamSubscription<Map<int, T>>? _subscription;
|
||||
StreamSubscription<Map<int, T>?>? _subscription;
|
||||
|
||||
EditLabelCubit(LabelRepository<T> repository)
|
||||
: _repository = repository,
|
||||
super(const EditLabelInitial()) {
|
||||
_subscription = _repository.labels
|
||||
.listen((labels) => emit(EditLabelState(labels: labels)));
|
||||
_subscription = repository.values.listen(
|
||||
(update) => emit(EditLabelState(labels: update ?? {})),
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> create(T label) => _repository.create(label);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -5,6 +6,7 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
|
||||
class EditLabelPage<T extends Label> extends StatelessWidget {
|
||||
final T label;
|
||||
@@ -91,7 +93,8 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
||||
},
|
||||
child: Text(
|
||||
S.of(context).genericActionDeleteLabel,
|
||||
style: TextStyle(color: Theme.of(context).errorColor),
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -99,7 +102,13 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
||||
) ??
|
||||
false;
|
||||
if (shouldDelete) {
|
||||
context.read<EditLabelCubit<T>>().delete(label);
|
||||
try {
|
||||
context.read<EditLabelCubit<T>>().delete(label);
|
||||
} on PaperlessServerException catch (error) {
|
||||
showErrorMessage(context, error);
|
||||
} catch (error) {
|
||||
print(error);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -123,8 +123,6 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
Navigator.pop(context, createdLabel);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on DioError catch (error) {
|
||||
setState(() => _errors = error.error as PaperlessValidationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ class InfoDrawer extends StatefulWidget {
|
||||
State<InfoDrawer> createState() => _InfoDrawerState();
|
||||
}
|
||||
|
||||
enum NavigationDestinations {
|
||||
inbox,
|
||||
settings,
|
||||
reportBug,
|
||||
about,
|
||||
logout;
|
||||
}
|
||||
|
||||
class _InfoDrawerState extends State<InfoDrawer> {
|
||||
late final Future<PackageInfo> _packageInfo;
|
||||
|
||||
@@ -41,6 +49,59 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final listtTileShape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
);
|
||||
// return NavigationDrawer(
|
||||
// selectedIndex: -1,
|
||||
// children: [
|
||||
// Text(
|
||||
// "",
|
||||
// style: Theme.of(context).textTheme.titleSmall,
|
||||
// ).padded(16),
|
||||
// NavigationDrawerDestination(
|
||||
// icon: const Icon(Icons.inbox),
|
||||
// label: Text(S.of(context).bottomNavInboxPageLabel),
|
||||
// ),
|
||||
// NavigationDrawerDestination(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// label: Text(S.of(context).appDrawerSettingsLabel),
|
||||
// ),
|
||||
// const Divider(
|
||||
// indent: 16,
|
||||
// ),
|
||||
// NavigationDrawerDestination(
|
||||
// icon: const Icon(Icons.bug_report),
|
||||
// label: Text(S.of(context).appDrawerReportBugLabel),
|
||||
// ),
|
||||
// NavigationDrawerDestination(
|
||||
// icon: const Icon(Icons.info_outline),
|
||||
// label: Text(S.of(context).appDrawerAboutLabel),
|
||||
// ),
|
||||
// ],
|
||||
// onDestinationSelected: (idx) {
|
||||
// final val = NavigationDestinations.values[idx - 1];
|
||||
// switch (val) {
|
||||
// case NavigationDestinations.inbox:
|
||||
// _onOpenInbox();
|
||||
// break;
|
||||
// case NavigationDestinations.settings:
|
||||
// _onOpenSettings();
|
||||
// break;
|
||||
// case NavigationDestinations.reportBug:
|
||||
// launchUrlString(
|
||||
// 'https://github.com/astubenbord/paperless-mobile/issues/new',
|
||||
// );
|
||||
// break;
|
||||
// case NavigationDestinations.about:
|
||||
// _onShowAboutDialog();
|
||||
// break;
|
||||
// case NavigationDestinations.logout:
|
||||
// _onLogout();
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
@@ -146,10 +207,12 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
||||
ListTile(
|
||||
title: Text(S.of(context).bottomNavInboxPageLabel),
|
||||
leading: const Icon(Icons.inbox),
|
||||
onTap: () => _onOpenInbox(context),
|
||||
onTap: () => _onOpenInbox(),
|
||||
shape: listtTileShape,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
shape: listtTileShape,
|
||||
title: Text(
|
||||
S.of(context).appDrawerSettingsLabel,
|
||||
),
|
||||
@@ -169,71 +232,45 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
||||
launchUrlString(
|
||||
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||
},
|
||||
shape: listtTileShape,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(S.of(context).appDrawerAboutLabel),
|
||||
leading: Icon(Icons.info_outline_rounded),
|
||||
onTap: _onShowAboutDialog,
|
||||
shape: listtTileShape,
|
||||
),
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: _packageInfo,
|
||||
builder: (context, snapshot) {
|
||||
return AboutListTile(
|
||||
icon: const Icon(Icons.info),
|
||||
applicationIcon: const ImageIcon(
|
||||
AssetImage('assets/logos/paperless_logo_green.png'),
|
||||
),
|
||||
applicationName: 'Paperless Mobile',
|
||||
applicationVersion: (snapshot.data?.version ?? '') +
|
||||
'+' +
|
||||
(snapshot.data?.buildNumber ?? ''),
|
||||
aboutBoxChildren: [
|
||||
Text(
|
||||
'${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'),
|
||||
Link(
|
||||
uri: Uri.parse(
|
||||
'https://github.com/astubenbord/paperless-mobile'),
|
||||
builder: (context, followLink) => GestureDetector(
|
||||
onTap: followLink,
|
||||
child: Text(
|
||||
'https://github.com/astubenbord/paperless-mobile',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.tertiary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Credits',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
_buildOnboardingImageCredits(),
|
||||
],
|
||||
child: Text(S.of(context).appDrawerAboutLabel),
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text(S.of(context).appDrawerLogoutLabel),
|
||||
shape: listtTileShape,
|
||||
onTap: () {
|
||||
try {
|
||||
context.read<AuthenticationCubit>().logout();
|
||||
context.read<LocalVault>().clear();
|
||||
context.read<ApplicationSettingsCubit>().clear();
|
||||
context.read<LabelRepository<Tag>>().clear();
|
||||
context.read<LabelRepository<Correspondent>>().clear();
|
||||
context.read<LabelRepository<DocumentType>>().clear();
|
||||
context.read<LabelRepository<StoragePath>>().clear();
|
||||
context.read<SavedViewRepository>().clear();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
_onLogout();
|
||||
},
|
||||
)
|
||||
].expand((element) => [element, const Divider()]),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onOpenInbox(BuildContext context) async {
|
||||
void _onLogout() {
|
||||
try {
|
||||
context.read<AuthenticationCubit>().logout();
|
||||
context.read<LocalVault>().clear();
|
||||
context.read<ApplicationSettingsCubit>().clear();
|
||||
context.read<LabelRepository<Tag>>().clear();
|
||||
context.read<LabelRepository<Correspondent>>().clear();
|
||||
context.read<LabelRepository<DocumentType>>().clear();
|
||||
context.read<LabelRepository<StoragePath>>().clear();
|
||||
context.read<SavedViewRepository>().clear();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onOpenInbox() async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LabelRepositoriesProvider(
|
||||
@@ -251,6 +288,17 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
||||
widget.afterInboxClosed?.call();
|
||||
}
|
||||
|
||||
void _onOpenSettings() {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: context.read<ApplicationSettingsCubit>(),
|
||||
child: const SettingsPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Link _buildOnboardingImageCredits() {
|
||||
return Link(
|
||||
uri: Uri.parse(
|
||||
@@ -270,4 +318,35 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onShowAboutDialog() async {
|
||||
final snapshot = await _packageInfo;
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationIcon: const ImageIcon(
|
||||
AssetImage('assets/logos/paperless_logo_green.png'),
|
||||
),
|
||||
applicationName: 'Paperless Mobile',
|
||||
applicationVersion: snapshot.version + '+' + snapshot.buildNumber,
|
||||
children: [
|
||||
Text('${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'),
|
||||
Link(
|
||||
uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'),
|
||||
builder: (context, followLink) => GestureDetector(
|
||||
onTap: followLink,
|
||||
child: Text(
|
||||
'https://github.com/astubenbord/paperless-mobile',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Credits',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
_buildOnboardingImageCredits(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class _InboxPageState extends State<InboxPage> {
|
||||
child: Text(
|
||||
S.of(context).inboxPageUsageHintText,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
).padded(),
|
||||
),
|
||||
...slivers
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:paperless_mobile/features/document_details/bloc/document_details
|
||||
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class InboxItem extends StatelessWidget {
|
||||
static const _a4AspectRatio = 1 / 1.4142;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
@@ -13,11 +12,17 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
|
||||
|
||||
LabelCubit(LabelRepository<T> repository)
|
||||
: _repository = repository,
|
||||
super(LabelState(labels: repository.current, isLoaded: true)) {
|
||||
_subscription = _repository.labels.listen(
|
||||
(update) => emit(
|
||||
LabelState(isLoaded: true, labels: update),
|
||||
),
|
||||
super(LabelState(
|
||||
isLoaded: repository.isInitialized,
|
||||
labels: repository.current ?? {},
|
||||
)) {
|
||||
_subscription = _repository.values.listen(
|
||||
(update) {
|
||||
if (update == null) {
|
||||
emit(LabelState());
|
||||
}
|
||||
emit(LabelState(isLoaded: true, labels: update!));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ class LabelState<T extends Label> {
|
||||
final Map<int, T> labels;
|
||||
|
||||
LabelState({
|
||||
required this.isLoaded,
|
||||
required this.labels,
|
||||
this.isLoaded = false,
|
||||
this.labels = const {},
|
||||
});
|
||||
|
||||
T? getLabel(int? key) {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart';
|
||||
|
||||
class CorrespondentWidget extends StatelessWidget {
|
||||
final int? correspondentId;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart';
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_typ
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart';
|
||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/document_type_bloc_provider.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/storage_path_bloc_provider.dart';
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LabelItem<T extends Label> extends StatelessWidget {
|
||||
final T label;
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||
@@ -38,12 +37,12 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, state) {
|
||||
if (state == ConnectivityState.notConnected) {
|
||||
return const OfflineWidget();
|
||||
}
|
||||
builder: (context, connectivityState) {
|
||||
return BlocBuilder<LabelCubit<T>, LabelState<T>>(
|
||||
builder: (context, state) {
|
||||
if (!state.isLoaded && !connectivityState.isConnected) {
|
||||
return const OfflineWidget();
|
||||
}
|
||||
final labels = state.labels.values.toList()..sort();
|
||||
if (labels.isEmpty) {
|
||||
return Center(
|
||||
@@ -57,13 +56,15 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: onAddNew,
|
||||
child: Text(emptyStateActionButtonLabel),
|
||||
)
|
||||
),
|
||||
].padded(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh: context.read<LabelCubit<T>>().reload,
|
||||
notificationPredicate: (notification) =>
|
||||
connectivityState.isConnected,
|
||||
child: ListView(
|
||||
children: labels
|
||||
.map(
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:paperless_mobile/features/documents/view/widgets/list/document_l
|
||||
import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LinkedDocumentsPage extends StatefulWidget {
|
||||
const LinkedDocumentsPage({super.key});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart';
|
||||
@@ -27,45 +25,32 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
|
||||
ClientCertificate? clientCertificate,
|
||||
}) async {
|
||||
assert(credentials.username != null && credentials.password != null);
|
||||
try {
|
||||
print(_dioWrapper.client.hashCode);
|
||||
_dioWrapper.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
);
|
||||
|
||||
final token = await _authApi.login(
|
||||
username: credentials.username!,
|
||||
password: credentials.password!,
|
||||
);
|
||||
_dioWrapper.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
);
|
||||
final token = await _authApi.login(
|
||||
username: credentials.username!,
|
||||
password: credentials.password!,
|
||||
);
|
||||
|
||||
_dioWrapper.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
authToken: token,
|
||||
);
|
||||
_dioWrapper.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
authToken: token,
|
||||
);
|
||||
|
||||
emit(
|
||||
AuthenticationState(
|
||||
wasLoginStored: false,
|
||||
authentication: AuthenticationInformation(
|
||||
serverUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
token: token,
|
||||
),
|
||||
emit(
|
||||
AuthenticationState(
|
||||
wasLoginStored: false,
|
||||
authentication: AuthenticationInformation(
|
||||
serverUrl: serverUrl,
|
||||
clientCertificate: clientCertificate,
|
||||
token: token,
|
||||
),
|
||||
);
|
||||
} on TlsException catch (_) {
|
||||
const error = PaperlessServerException(
|
||||
ErrorCode.invalidClientCertificateConfiguration);
|
||||
throw error;
|
||||
} on SocketException catch (err) {
|
||||
if (err.message.contains("connection timed out")) {
|
||||
throw const PaperlessServerException(ErrorCode.requestTimedOut);
|
||||
} else {
|
||||
throw const PaperlessServerException.unknown();
|
||||
}
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
8
lib/features/login/model/reachability_status.dart
Normal file
8
lib/features/login/model/reachability_status.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
enum ReachabilityStatus {
|
||||
unknown,
|
||||
reachable,
|
||||
notReachable,
|
||||
unknownHost,
|
||||
missingClientCertificate,
|
||||
invalidClientCertificateConfiguration,
|
||||
}
|
||||
@@ -2,14 +2,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/server_connection_page.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
|
||||
import 'widgets/never_scrollable_scroll_behavior.dart';
|
||||
import 'widgets/server_login_page.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -20,53 +23,73 @@ class LoginPage extends StatefulWidget {
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
bool _isLoginLoading = false;
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).loginPageTitle),
|
||||
bottom: _isLoginLoading
|
||||
? const PreferredSize(
|
||||
preferredSize: Size(double.infinity, 4),
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
const ServerAddressFormField().padded(),
|
||||
const UserCredentialsFormField(),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Text(
|
||||
S.of(context).loginPageAdvancedLabel,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).padded(),
|
||||
),
|
||||
),
|
||||
const ClientCertificateFormField(),
|
||||
LayoutBuilder(builder: (context, constraints) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
child: _buildLoginButton(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false, // appBar: AppBar(
|
||||
// title: Text(S.of(context).loginPageTitle),
|
||||
// bottom: _isLoginLoading
|
||||
// ? const PreferredSize(
|
||||
// preferredSize: Size(double.infinity, 4),
|
||||
// child: LinearProgressIndicator(),
|
||||
// )
|
||||
// : null,
|
||||
// ),
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
scrollBehavior: NeverScrollableScrollBehavior(),
|
||||
children: [
|
||||
ServerConnectionPage(
|
||||
formBuilderKey: _formKey,
|
||||
onContinue: () {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut);
|
||||
},
|
||||
),
|
||||
ServerLoginPage(
|
||||
formBuilderKey: _formKey,
|
||||
onDone: _login,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: FormBuilder(
|
||||
// key: _formKey,
|
||||
// child: ListView(
|
||||
// children: [
|
||||
// const ServerAddressFormField().padded(),
|
||||
// const UserCredentialsFormField(),
|
||||
// Align(
|
||||
// alignment: Alignment.centerLeft,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(top: 16.0),
|
||||
// child: Text(
|
||||
// S.of(context).loginPageAdvancedLabel,
|
||||
// style: Theme.of(context).textTheme.bodyLarge,
|
||||
// ).padded(),
|
||||
// ),
|
||||
// ),
|
||||
// const ClientCertificateFormField(),
|
||||
// LayoutBuilder(builder: (context, constraints) {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: SizedBox(
|
||||
// width: constraints.maxWidth,
|
||||
// child: _buildLoginButton(),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +112,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||
void _login() async {
|
||||
FocusScope.of(context).unfocus();
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
setState(() => _isLoginLoading = true);
|
||||
final form = _formKey.currentState!.value;
|
||||
try {
|
||||
await context.read<AuthenticationCubit>().login(
|
||||
@@ -104,9 +126,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
showGenericError(context, error.values.first, stackTrace);
|
||||
} catch (unknownError, stackTrace) {
|
||||
showGenericError(context, unknownError.toString(), stackTrace);
|
||||
} finally {
|
||||
setState(() => _isLoginLoading = false);
|
||||
}
|
||||
} finally {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class ClientCertificateFormField extends StatefulWidget {
|
||||
static const fkClientCertificate = 'clientCertificate';
|
||||
const ClientCertificateFormField({Key? key}) : super(key: key);
|
||||
|
||||
final void Function(ClientCertificate? cert) onChanged;
|
||||
const ClientCertificateFormField({
|
||||
Key? key,
|
||||
required this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ClientCertificateFormField> createState() =>
|
||||
@@ -19,11 +24,13 @@ class ClientCertificateFormField extends StatefulWidget {
|
||||
|
||||
class _ClientCertificateFormFieldState
|
||||
extends State<ClientCertificateFormField> {
|
||||
RestorableString? _selectedFilePath;
|
||||
File? _selectedFile;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<ClientCertificate?>(
|
||||
key: const ValueKey('login-client-cert'),
|
||||
onChanged: widget.onChanged,
|
||||
initialValue: null,
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
@@ -38,54 +45,59 @@ class _ClientCertificateFormFieldState
|
||||
return null;
|
||||
},
|
||||
builder: (field) {
|
||||
return ExpansionTile(
|
||||
title: Text(S.of(context).loginPageClientCertificateSettingLabel),
|
||||
subtitle: Text(
|
||||
S.of(context).loginPageClientCertificateSettingDescriptionText),
|
||||
children: [
|
||||
InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
errorText: field.errorText,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: ElevatedButton(
|
||||
onPressed: () => _onSelectFile(field),
|
||||
child: Text(S.of(context).genericActionSelectText),
|
||||
),
|
||||
title: _buildSelectedFileText(field),
|
||||
trailing: AbsorbPointer(
|
||||
absorbing: field.value == null,
|
||||
child: _selectedFile != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => setState(() {
|
||||
_selectedFile = null;
|
||||
field.didChange(null);
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (_selectedFile != null) ...[
|
||||
ObscuredInputTextFormField(
|
||||
key: const ValueKey('login-client-cert-passphrase'),
|
||||
initialValue: field.value?.passphrase,
|
||||
onChanged: (value) => field.didChange(
|
||||
field.value?.copyWith(passphrase: value),
|
||||
final theme =
|
||||
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
|
||||
return Theme(
|
||||
data: theme,
|
||||
child: ExpansionTile(
|
||||
title: Text(S.of(context).loginPageClientCertificateSettingLabel),
|
||||
subtitle: Text(
|
||||
S.of(context).loginPageClientCertificateSettingDescriptionText),
|
||||
children: [
|
||||
InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
errorText: field.errorText,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: ElevatedButton(
|
||||
onPressed: () => _onSelectFile(field),
|
||||
child: Text(S.of(context).genericActionSelectText),
|
||||
),
|
||||
label: S
|
||||
.of(context)
|
||||
.loginPageClientCertificatePassphraseLabel,
|
||||
).padded(),
|
||||
] else
|
||||
...[]
|
||||
],
|
||||
title: _buildSelectedFileText(field),
|
||||
trailing: AbsorbPointer(
|
||||
absorbing: field.value == null,
|
||||
child: _selectedFile != null
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => setState(() {
|
||||
_selectedFile = null;
|
||||
field.didChange(null);
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (_selectedFile != null) ...[
|
||||
ObscuredInputTextFormField(
|
||||
key: const ValueKey('login-client-cert-passphrase'),
|
||||
initialValue: field.value?.passphrase,
|
||||
onChanged: (value) => field.didChange(
|
||||
field.value?.copyWith(passphrase: value),
|
||||
),
|
||||
label: S
|
||||
.of(context)
|
||||
.loginPageClientCertificatePassphraseLabel,
|
||||
).padded(),
|
||||
] else
|
||||
...[]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
name: ClientCertificateFormField.fkClientCertificate,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class NeverScrollableScrollBehavior extends ScrollBehavior {
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
return const NeverScrollableScrollPhysics();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ServerAddressFormField extends StatefulWidget {
|
||||
static const String fkServerAddress = "serverAddress";
|
||||
|
||||
final void Function(String address) onDone;
|
||||
const ServerAddressFormField({
|
||||
Key? key,
|
||||
required this.onDone,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -17,13 +20,7 @@ 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) {
|
||||
@@ -31,75 +28,25 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
key: const ValueKey('login-server-address'),
|
||||
controller: _textEditingController,
|
||||
name: ServerAddressFormField.fkServerAddress,
|
||||
validator: FormBuilderValidators.compose(
|
||||
[
|
||||
FormBuilderValidators.required(
|
||||
errorText:
|
||||
S.of(context).loginPageServerUrlValidatorMessageRequiredText,
|
||||
),
|
||||
FormBuilderValidators.match(
|
||||
_urlRegex.pattern,
|
||||
errorText: S
|
||||
.of(context)
|
||||
.loginPageServerUrlValidatorMessageInvalidAddressText,
|
||||
),
|
||||
],
|
||||
validator: FormBuilderValidators.required(
|
||||
errorText: S.of(context).loginPageServerUrlValidatorMessageRequiredText,
|
||||
),
|
||||
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();
|
||||
String address = value.trim();
|
||||
address = _replaceTrailingSlashes(address);
|
||||
_textEditingController.text = address;
|
||||
if (_urlRegex.hasMatch(address) && address.endsWith("/")) {
|
||||
_textEditingController.text = address.replaceAll(RegExp(r'\/$'), '');
|
||||
}
|
||||
widget.onDone(address);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildIsReachableIcon() {
|
||||
switch (_reachabilityStatus) {
|
||||
case ReachabilityStatus.reachable:
|
||||
return const Icon(
|
||||
Icons.done,
|
||||
color: Colors.green,
|
||||
);
|
||||
case ReachabilityStatus.notReachable:
|
||||
return Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
);
|
||||
case ReachabilityStatus.testing:
|
||||
return const RefreshProgressIndicator();
|
||||
case ReachabilityStatus.undefined:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateIsAddressReachableStatus(String? address) async {
|
||||
if (address == null || !_urlRegex.hasMatch(address)) {
|
||||
setState(() {
|
||||
_reachabilityStatus = ReachabilityStatus.undefined;
|
||||
});
|
||||
return;
|
||||
}
|
||||
//https://stackoverflow.com/questions/49648022/check-whether-there-is-an-internet-connection-available-on-flutter-app
|
||||
setState(() => _reachabilityStatus = ReachabilityStatus.testing);
|
||||
final isReachable = await context
|
||||
.read<ConnectivityStatusService>()
|
||||
.isServerReachable(address.trim());
|
||||
setState(
|
||||
() => _reachabilityStatus = isReachable
|
||||
? ReachabilityStatus.reachable
|
||||
: ReachabilityStatus.notReachable,
|
||||
);
|
||||
String _replaceTrailingSlashes(String src) {
|
||||
return src.replaceAll(RegExp(r'^\/+|\/+$'), '');
|
||||
}
|
||||
}
|
||||
|
||||
enum ReachabilityStatus { reachable, notReachable, testing, undefined }
|
||||
|
||||
133
lib/features/login/view/widgets/server_connection_page.dart
Normal file
133
lib/features/login/view/widgets/server_connection_page.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ServerConnectionPage extends StatefulWidget {
|
||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
||||
final void Function() onContinue;
|
||||
|
||||
const ServerConnectionPage({
|
||||
super.key,
|
||||
required this.formBuilderKey,
|
||||
required this.onContinue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServerConnectionPage> createState() => _ServerConnectionPageState();
|
||||
}
|
||||
|
||||
class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logoHeight = MediaQuery.of(context).size.width / 2;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).loginPageTitle),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: Column(
|
||||
children: [
|
||||
ServerAddressFormField(
|
||||
onDone: (address) {
|
||||
_updateReachability();
|
||||
},
|
||||
).padded(),
|
||||
ClientCertificateFormField(
|
||||
onChanged: (_) => _updateReachability(),
|
||||
).padded(),
|
||||
_buildStatusIndicator(),
|
||||
],
|
||||
).padded(),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton(
|
||||
child: Text("Continue"),
|
||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
|
||||
? widget.onContinue
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateReachability() async {
|
||||
final status = await context
|
||||
.read<ConnectivityStatusService>()
|
||||
.isPaperlessServerReachable(
|
||||
widget.formBuilderKey.currentState!
|
||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||
widget.formBuilderKey.currentState?.getRawValue(
|
||||
ClientCertificateFormField.fkClientCertificate,
|
||||
),
|
||||
);
|
||||
setState(() => _reachabilityStatus = status);
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator() {
|
||||
Color errorColor = Theme.of(context).colorScheme.error;
|
||||
switch (_reachabilityStatus) {
|
||||
case ReachabilityStatus.unknown:
|
||||
return Container();
|
||||
case ReachabilityStatus.reachable:
|
||||
return _buildIconText(
|
||||
Icons.done,
|
||||
"Connection established.",
|
||||
Colors.green,
|
||||
);
|
||||
case ReachabilityStatus.notReachable:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
"Could not establish a connection to the server.",
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.unknownHost:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
"Host could not be resolved.",
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.missingClientCertificate:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
"A client certificate was expected but not sent. Please provide a certificate.",
|
||||
errorColor,
|
||||
);
|
||||
case ReachabilityStatus.invalidClientCertificateConfiguration:
|
||||
return _buildIconText(
|
||||
Icons.close,
|
||||
"Incorrect or missing client certificate passphrase.",
|
||||
errorColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildIconText(
|
||||
IconData icon,
|
||||
String text, [
|
||||
Color? color,
|
||||
]) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color),
|
||||
),
|
||||
leading: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
49
lib/features/login/view/widgets/server_login_page.dart
Normal file
49
lib/features/login/view/widgets/server_login_page.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
|
||||
|
||||
class ServerLoginPage extends StatefulWidget {
|
||||
final VoidCallback onDone;
|
||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
||||
const ServerLoginPage({
|
||||
super.key,
|
||||
required this.onDone,
|
||||
required this.formBuilderKey,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServerLoginPage> createState() => _ServerLoginPageState();
|
||||
}
|
||||
|
||||
class _ServerLoginPageState extends State<ServerLoginPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serverAddress = (widget.formBuilderKey.currentState
|
||||
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
|
||||
?.replaceAll(RegExp(r'https?://'), '');
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Sign In"),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
Text("Sign in to $serverAddress").padded(),
|
||||
UserCredentialsFormField(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: widget.onDone,
|
||||
child: Text("Sign In"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class SavedViewCubit extends Cubit<SavedViewState> {
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
SavedViewCubit(this._repository) : super(SavedViewState(value: {})) {
|
||||
_subscription = _repository.savedViews.listen(
|
||||
_subscription = _repository.values.listen(
|
||||
(savedViews) {
|
||||
if (savedViews == null) {
|
||||
emit(state.copyWith(isLoaded: false));
|
||||
|
||||
@@ -4,9 +4,7 @@ import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
|
||||
class DocumentScannerCubit extends Cubit<List<File>> {
|
||||
DocumentScannerCubit() : super(const []);
|
||||
|
||||
Reference in New Issue
Block a user