mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 10:07:51 -06:00
feat: bugfixes, finished go_router migration, implemented better visibility of states
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
class GoRouterRefreshStream extends ChangeNotifier {
|
|
||||||
GoRouterRefreshStream(Stream<dynamic> stream) {
|
|
||||||
notifyListeners();
|
|
||||||
_subscription = stream.asBroadcastStream().listen(
|
|
||||||
(dynamic _) => notifyListeners(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final StreamSubscription<dynamic> _subscription;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_subscription.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
|
||||||
|
|
||||||
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
|
||||||
DocumentStatusCubit() : super(null);
|
|
||||||
|
|
||||||
void updateStatus(DocumentProcessingStatus? status) => emit(status);
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/user_repository.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
Future<void> pushSavedViewDetailsRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required SavedView savedView,
|
|
||||||
}) {
|
|
||||||
final apiVersion = context.read<ApiVersion>();
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
Provider.value(value: apiVersion),
|
|
||||||
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
|
|
||||||
Provider.value(value: context.read<UserRepository>()),
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<CacheManager>()),
|
|
||||||
Provider.value(value: context.read<ConnectivityCubit>()),
|
|
||||||
],
|
|
||||||
builder: (_, child) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => SavedViewDetailsCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
LocalUserAppState.current,
|
|
||||||
context.read(),
|
|
||||||
savedView: savedView,
|
|
||||||
),
|
|
||||||
child: SavedViewDetailsPage(
|
|
||||||
onDelete: context.read<SavedViewCubit>().remove,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditCorrespondentRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<DocumentModel> selection,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return FullscreenBulkEditLabelPage(
|
|
||||||
options: state.correspondents,
|
|
||||||
selection: state.selection,
|
|
||||||
labelMapper: (document) => document.correspondent,
|
|
||||||
leadingIcon: const Icon(Icons.person_outline),
|
|
||||||
hintText: S.of(context)!.startTyping,
|
|
||||||
onSubmit: context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyCorrespondent,
|
|
||||||
assignMessageBuilder: (int count, String name) {
|
|
||||||
return S.of(context)!.bulkEditCorrespondentAssignMessage(
|
|
||||||
name,
|
|
||||||
count,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeMessageBuilder: (int count) {
|
|
||||||
return S
|
|
||||||
.of(context)!
|
|
||||||
.bulkEditCorrespondentRemoveMessage(count);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditStoragePathRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<DocumentModel> selection,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return FullscreenBulkEditLabelPage(
|
|
||||||
options: state.storagePaths,
|
|
||||||
selection: state.selection,
|
|
||||||
labelMapper: (document) => document.storagePath,
|
|
||||||
leadingIcon: const Icon(Icons.folder_outlined),
|
|
||||||
hintText: S.of(context)!.startTyping,
|
|
||||||
onSubmit: context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyStoragePath,
|
|
||||||
assignMessageBuilder: (int count, String name) {
|
|
||||||
return S.of(context)!.bulkEditStoragePathAssignMessage(
|
|
||||||
count,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeMessageBuilder: (int count) {
|
|
||||||
return S.of(context)!.bulkEditStoragePathRemoveMessage(count);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditTagsRoute(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<DocumentModel> selection,
|
|
||||||
}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: Builder(builder: (context) {
|
|
||||||
return const FullscreenBulkEditTagsWidget();
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
|
|
||||||
{required List<DocumentModel> selection}) {
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => MultiProvider(
|
|
||||||
providers: [
|
|
||||||
..._getRequiredBulkEditProviders(context),
|
|
||||||
],
|
|
||||||
builder: (_, __) => BlocProvider(
|
|
||||||
create: (_) => DocumentBulkActionCubit(
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
context.read(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return FullscreenBulkEditLabelPage(
|
|
||||||
options: state.documentTypes,
|
|
||||||
selection: state.selection,
|
|
||||||
labelMapper: (document) => document.documentType,
|
|
||||||
leadingIcon: const Icon(Icons.description_outlined),
|
|
||||||
hintText: S.of(context)!.startTyping,
|
|
||||||
onSubmit: context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyDocumentType,
|
|
||||||
assignMessageBuilder: (int count, String name) {
|
|
||||||
return S.of(context)!.bulkEditDocumentTypeAssignMessage(
|
|
||||||
count,
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeMessageBuilder: (int count) {
|
|
||||||
return S
|
|
||||||
.of(context)!
|
|
||||||
.bulkEditDocumentTypeRemoveMessage(count);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Provider> _getRequiredBulkEditProviders(BuildContext context) {
|
|
||||||
return [
|
|
||||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
|
||||||
Provider.value(value: context.read<LabelRepository>()),
|
|
||||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,10 @@ class DocumentChangedNotifier {
|
|||||||
|
|
||||||
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
|
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
|
||||||
|
|
||||||
|
Stream<DocumentModel> get $updated => _updated.asBroadcastStream();
|
||||||
|
|
||||||
|
Stream<DocumentModel> get $deleted => _deleted.asBroadcastStream();
|
||||||
|
|
||||||
void notifyUpdated(DocumentModel updated) {
|
void notifyUpdated(DocumentModel updated) {
|
||||||
debugPrint("Notifying updated document ${updated.id}");
|
debugPrint("Notifying updated document ${updated.id}");
|
||||||
_updated.add(updated);
|
_updated.add(updated);
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
class FileDescription {
|
|
||||||
final String filename;
|
|
||||||
final String extension;
|
|
||||||
|
|
||||||
FileDescription({
|
|
||||||
required this.filename,
|
|
||||||
required this.extension,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory FileDescription.fromPath(String path) {
|
|
||||||
final filename = path.split(RegExp(r"/")).last;
|
|
||||||
final fragments = filename.split(".");
|
|
||||||
final ext = fragments.removeLast();
|
|
||||||
final name = fragments.join(".");
|
|
||||||
return FileDescription(
|
|
||||||
filename: name,
|
|
||||||
extension: ext,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -14,9 +13,6 @@ class FileService {
|
|||||||
String filename,
|
String filename,
|
||||||
) async {
|
) async {
|
||||||
final dir = await documentsDirectory;
|
final dir = await documentsDirectory;
|
||||||
if (dir == null) {
|
|
||||||
throw const PaperlessApiException.unknown(); //TODO: better handling
|
|
||||||
}
|
|
||||||
File file = File("${dir.path}/$filename");
|
File file = File("${dir.path}/$filename");
|
||||||
return file..writeAsBytes(bytes);
|
return file..writeAsBytes(bytes);
|
||||||
}
|
}
|
||||||
@@ -43,7 +39,7 @@ class FileService {
|
|||||||
|
|
||||||
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
||||||
|
|
||||||
static Future<Directory?> get documentsDirectory async {
|
static Future<Directory> get documentsDirectory async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
return (await getExternalStorageDirectories(
|
return (await getExternalStorageDirectories(
|
||||||
type: StorageDirectory.documents,
|
type: StorageDirectory.documents,
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
|
||||||
// import 'package:web_socket_channel/io.dart';
|
|
||||||
|
|
||||||
abstract class StatusService {
|
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
|
||||||
String httpUrl, UserCredentials credentials, String documentFileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebSocketStatusService implements StatusService {
|
|
||||||
late WebSocket? socket;
|
|
||||||
// late IOWebSocketChannel? _channel;
|
|
||||||
|
|
||||||
WebSocketStatusService();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
|
||||||
String httpUrl,
|
|
||||||
UserCredentials credentials,
|
|
||||||
String documentFileName,
|
|
||||||
) async {
|
|
||||||
// socket = await WebSocket.connect(
|
|
||||||
// httpUrl.replaceFirst("http", "ws") + "/ws/status/",
|
|
||||||
// customClient: getIt<HttpClient>(),
|
|
||||||
// headers: {
|
|
||||||
// 'Authorization': 'Token ${credentials.token}',
|
|
||||||
// },
|
|
||||||
// ).catchError((_) {
|
|
||||||
// // Use long polling if connection could not be established
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (socket != null) {
|
|
||||||
// socket!.where(isNotNull).listen((event) {
|
|
||||||
// final status = DocumentProcessingStatus.fromJson(event);
|
|
||||||
// getIt<DocumentStatusCubit>().updateStatus(status);
|
|
||||||
// if (status.currentProgress == 100) {
|
|
||||||
// socket!.close();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LongPollingStatusService implements StatusService {
|
|
||||||
final Dio client;
|
|
||||||
const LongPollingStatusService(this.client);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> startListeningBeforeDocumentUpload(
|
|
||||||
String httpUrl,
|
|
||||||
UserCredentials credentials,
|
|
||||||
String documentFileName,
|
|
||||||
) async {
|
|
||||||
// final today = DateTime.now();
|
|
||||||
// bool consumptionFinished = false;
|
|
||||||
// int retryCount = 0;
|
|
||||||
|
|
||||||
// getIt<DocumentStatusCubit>().updateStatus(
|
|
||||||
// DocumentProcessingStatus(
|
|
||||||
// currentProgress: 0,
|
|
||||||
// filename: documentFileName,
|
|
||||||
// maxProgress: 100,
|
|
||||||
// message: ProcessingMessage.new_file,
|
|
||||||
// status: ProcessingStatus.working,
|
|
||||||
// taskId: DocumentProcessingStatus.unknownTaskId,
|
|
||||||
// documentId: null,
|
|
||||||
// isApproximated: true,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// do {
|
|
||||||
// final response = await httpClient.get(
|
|
||||||
// Uri.parse(
|
|
||||||
// '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
|
||||||
// );
|
|
||||||
// final data = await compute(
|
|
||||||
// PagedSearchResult.fromJson,
|
|
||||||
// PagedSearchResultJsonSerializer(
|
|
||||||
// jsonDecode(response.body), DocumentModel.fromJson),
|
|
||||||
// );
|
|
||||||
// if (data.count > 0) {
|
|
||||||
// consumptionFinished = true;
|
|
||||||
// final docId = data.results[0].id;
|
|
||||||
// getIt<DocumentStatusCubit>().updateStatus(
|
|
||||||
// DocumentProcessingStatus(
|
|
||||||
// currentProgress: 100,
|
|
||||||
// filename: documentFileName,
|
|
||||||
// maxProgress: 100,
|
|
||||||
// message: ProcessingMessage.finished,
|
|
||||||
// status: ProcessingStatus.success,
|
|
||||||
// taskId: DocumentProcessingStatus.unknownTaskId,
|
|
||||||
// documentId: docId,
|
|
||||||
// isApproximated: true,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// sleep(const Duration(seconds: 1));
|
|
||||||
// } while (!consumptionFinished && retryCount < maxRetries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
// import 'package:paperless_mobile/constants.dart';
|
|
||||||
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
|
||||||
// import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
|
||||||
// import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|
||||||
// import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
|
||||||
// import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
// import 'package:url_launcher/link.dart';
|
|
||||||
// import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
// /// Declares selectable actions in menu.
|
|
||||||
// enum AppPopupMenuEntries {
|
|
||||||
// // Documents preview
|
|
||||||
// documentsSelectListView,
|
|
||||||
// documentsSelectGridView,
|
|
||||||
// // Generic actions
|
|
||||||
// openAboutThisAppDialog,
|
|
||||||
// reportBug,
|
|
||||||
// openSettings,
|
|
||||||
// // Adds a divider
|
|
||||||
// divider;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// class AppOptionsPopupMenu extends StatelessWidget {
|
|
||||||
// final List<AppPopupMenuEntries> displayedActions;
|
|
||||||
// const AppOptionsPopupMenu({
|
|
||||||
// super.key,
|
|
||||||
// required this.displayedActions,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return PopupMenuButton<AppPopupMenuEntries>(
|
|
||||||
// position: PopupMenuPosition.under,
|
|
||||||
// icon: const Icon(Icons.more_vert),
|
|
||||||
// onSelected: (action) {
|
|
||||||
// switch (action) {
|
|
||||||
// case AppPopupMenuEntries.documentsSelectListView:
|
|
||||||
// context.read<ApplicationSettingsCubit>().setViewType(ViewType.list);
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.documentsSelectGridView:
|
|
||||||
// context.read<ApplicationSettingsCubit>().setViewType(ViewType.grid);
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.openAboutThisAppDialog:
|
|
||||||
// _showAboutDialog(context);
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.openSettings:
|
|
||||||
// Navigator.of(context).push(
|
|
||||||
// MaterialPageRoute(
|
|
||||||
// builder: (context) => BlocProvider.value(
|
|
||||||
// value: context.read<ApplicationSettingsCubit>(),
|
|
||||||
// child: const SettingsPage(),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.reportBug:
|
|
||||||
// launchUrlString(
|
|
||||||
// 'https://github.com/astubenbord/paperless-mobile/issues/new',
|
|
||||||
// );
|
|
||||||
// break;
|
|
||||||
// default:
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// itemBuilder: _buildEntries,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// PopupMenuItem<AppPopupMenuEntries> _buildReportBugTile(BuildContext context) {
|
|
||||||
// return PopupMenuItem(
|
|
||||||
// value: AppPopupMenuEntries.reportBug,
|
|
||||||
// padding: EdgeInsets.zero,
|
|
||||||
// child: ListTile(
|
|
||||||
// leading: const Icon(Icons.bug_report),
|
|
||||||
// title: Text(S.of(context)!.reportABug),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// PopupMenuItem<AppPopupMenuEntries> _buildSettingsTile(BuildContext context) {
|
|
||||||
// return PopupMenuItem(
|
|
||||||
// padding: EdgeInsets.zero,
|
|
||||||
// value: AppPopupMenuEntries.openSettings,
|
|
||||||
// child: ListTile(
|
|
||||||
// leading: const Icon(Icons.settings_outlined),
|
|
||||||
// title: Text(S.of(context)!.settings),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// PopupMenuItem<AppPopupMenuEntries> _buildAboutTile(BuildContext context) {
|
|
||||||
// return PopupMenuItem(
|
|
||||||
// padding: EdgeInsets.zero,
|
|
||||||
// value: AppPopupMenuEntries.openAboutThisAppDialog,
|
|
||||||
// child: ListTile(
|
|
||||||
// leading: const Icon(Icons.info_outline),
|
|
||||||
// title: Text(S.of(context)!.aboutThisApp),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// PopupMenuItem<AppPopupMenuEntries> _buildListViewTile() {
|
|
||||||
// return PopupMenuItem(
|
|
||||||
// padding: EdgeInsets.zero,
|
|
||||||
// child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
|
||||||
// builder: (context, state) {
|
|
||||||
// return ListTile(
|
|
||||||
// leading: const Icon(Icons.list),
|
|
||||||
// title: const Text("List"),
|
|
||||||
// trailing: state.preferredViewType == ViewType.list
|
|
||||||
// ? const Icon(Icons.check)
|
|
||||||
// : null,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// value: AppPopupMenuEntries.documentsSelectListView,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// PopupMenuItem<AppPopupMenuEntries> _buildGridViewTile() {
|
|
||||||
// return PopupMenuItem(
|
|
||||||
// value: AppPopupMenuEntries.documentsSelectGridView,
|
|
||||||
// padding: EdgeInsets.zero,
|
|
||||||
// child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
|
||||||
// builder: (context, state) {
|
|
||||||
// return ListTile(
|
|
||||||
// leading: const Icon(Icons.grid_view_rounded),
|
|
||||||
// title: const Text("Grid"),
|
|
||||||
// trailing: state.preferredViewType == ViewType.grid
|
|
||||||
// ? const Icon(Icons.check)
|
|
||||||
// : null,
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void _showAboutDialog(BuildContext context) {
|
|
||||||
// showAboutDialog(
|
|
||||||
// context: context,
|
|
||||||
// applicationIcon: const ImageIcon(
|
|
||||||
// AssetImage('assets/logos/paperless_logo_green.png'),
|
|
||||||
// ),
|
|
||||||
// applicationName: 'Paperless Mobile',
|
|
||||||
// applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber,
|
|
||||||
// children: [
|
|
||||||
// Text(S.of(context)!.developedBy('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(),
|
|
||||||
// ],
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildOnboardingImageCredits() {
|
|
||||||
// return Link(
|
|
||||||
// uri: Uri.parse(
|
|
||||||
// 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'),
|
|
||||||
// builder: (context, followLink) => Wrap(
|
|
||||||
// children: [
|
|
||||||
// const Text('Onboarding images by '),
|
|
||||||
// GestureDetector(
|
|
||||||
// onTap: followLink,
|
|
||||||
// child: Text(
|
|
||||||
// 'pch.vector',
|
|
||||||
// style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// const Text(' on Freepik.')
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// List<PopupMenuEntry<AppPopupMenuEntries>> _buildEntries(
|
|
||||||
// BuildContext context) {
|
|
||||||
// List<PopupMenuEntry<AppPopupMenuEntries>> items = [];
|
|
||||||
// for (final entry in displayedActions) {
|
|
||||||
// switch (entry) {
|
|
||||||
// case AppPopupMenuEntries.documentsSelectListView:
|
|
||||||
// items.add(_buildListViewTile());
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.documentsSelectGridView:
|
|
||||||
// items.add(_buildGridViewTile());
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.openAboutThisAppDialog:
|
|
||||||
// items.add(_buildAboutTile(context));
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.reportBug:
|
|
||||||
// items.add(_buildReportBugTile(context));
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.openSettings:
|
|
||||||
// items.add(_buildSettingsTile(context));
|
|
||||||
// break;
|
|
||||||
// case AppPopupMenuEntries.divider:
|
|
||||||
// items.add(const PopupMenuDivider());
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return items;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
|
||||||
|
|
||||||
typedef SelectionToTextTransformer<T> = String Function(T suggestion);
|
|
||||||
|
|
||||||
/// Text field that auto-completes user input from a list of items
|
|
||||||
class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
|
||||||
/// Called with the search pattern to get the search suggestions.
|
|
||||||
///
|
|
||||||
/// This callback must not be null. It is be called by the TypeAhead widget
|
|
||||||
/// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
|
|
||||||
/// of suggestions either synchronously, or asynchronously (as the result of a
|
|
||||||
/// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
|
|
||||||
/// Typically, the list of suggestions should not contain more than 4 or 5
|
|
||||||
/// entries. These entries will then be provided to [itemBuilder] to display
|
|
||||||
/// the suggestions.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// suggestionsCallback: (pattern) async {
|
|
||||||
/// return await _getSuggestions(pattern);
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
final SuggestionsCallback<T> suggestionsCallback;
|
|
||||||
|
|
||||||
/// Called when a suggestion is tapped.
|
|
||||||
///
|
|
||||||
/// This callback must not be null. It is called by the TypeAhead widget and
|
|
||||||
/// provided with the value of the tapped suggestion.
|
|
||||||
///
|
|
||||||
/// For example, you might want to navigate to a specific view when the user
|
|
||||||
/// tabs a suggestion:
|
|
||||||
/// ```dart
|
|
||||||
/// onSuggestionSelected: (suggestion) {
|
|
||||||
/// Navigator.of(context).push(MaterialPageRoute(
|
|
||||||
/// builder: (context) => SearchResult(
|
|
||||||
/// searchItem: suggestion
|
|
||||||
/// )
|
|
||||||
/// ));
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Or to set the value of the text field:
|
|
||||||
/// ```dart
|
|
||||||
/// onSuggestionSelected: (suggestion) {
|
|
||||||
/// _controller.text = suggestion['name'];
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
final SuggestionSelectionCallback<T>? onSuggestionSelected;
|
|
||||||
|
|
||||||
/// Called for each suggestion returned by [suggestionsCallback] to build the
|
|
||||||
/// corresponding widget.
|
|
||||||
///
|
|
||||||
/// This callback must not be null. It is called by the TypeAhead widget for
|
|
||||||
/// each suggestion, and expected to build a widget to display this
|
|
||||||
/// suggestion's info. For example:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// itemBuilder: (context, suggestion) {
|
|
||||||
/// return ListTile(
|
|
||||||
/// title: Text(suggestion['name']),
|
|
||||||
/// subtitle: Text('USD' + suggestion['price'].toString())
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
final ItemBuilder<T> itemBuilder;
|
|
||||||
|
|
||||||
/// The decoration of the material sheet that contains the suggestions.
|
|
||||||
///
|
|
||||||
/// If null, default decoration with an elevation of 4.0 is used
|
|
||||||
final SuggestionsBoxDecoration suggestionsBoxDecoration;
|
|
||||||
|
|
||||||
/// Used to control the `_SuggestionsBox`. Allows manual control to
|
|
||||||
/// open, close, toggle, or resize the `_SuggestionsBox`.
|
|
||||||
final SuggestionsBoxController? suggestionsBoxController;
|
|
||||||
|
|
||||||
/// The duration to wait after the user stops typing before calling
|
|
||||||
/// [suggestionsCallback]
|
|
||||||
///
|
|
||||||
/// This is useful, because, if not set, a request for suggestions will be
|
|
||||||
/// sent for every character that the user types.
|
|
||||||
///
|
|
||||||
/// This duration is set by default to 300 milliseconds
|
|
||||||
final Duration debounceDuration;
|
|
||||||
|
|
||||||
/// Called when waiting for [suggestionsCallback] to return.
|
|
||||||
///
|
|
||||||
/// It is expected to return a widget to display while waiting.
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// (BuildContext context) {
|
|
||||||
/// return Text('Loading...');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown
|
|
||||||
final WidgetBuilder? loadingBuilder;
|
|
||||||
|
|
||||||
/// Called when [suggestionsCallback] returns an empty array.
|
|
||||||
///
|
|
||||||
/// It is expected to return a widget to display when no suggestions are
|
|
||||||
/// available.
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// (BuildContext context) {
|
|
||||||
/// return Text('No Items Found!');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If not specified, a simple text is shown
|
|
||||||
final WidgetBuilder? noItemsFoundBuilder;
|
|
||||||
|
|
||||||
/// Called when [suggestionsCallback] throws an exception.
|
|
||||||
///
|
|
||||||
/// It is called with the error object, and expected to return a widget to
|
|
||||||
/// display when an exception is thrown
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// (BuildContext context, error) {
|
|
||||||
/// return Text('$error');
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html)
|
|
||||||
final ErrorBuilder? errorBuilder;
|
|
||||||
|
|
||||||
/// Called to display animations when [suggestionsCallback] returns suggestions
|
|
||||||
///
|
|
||||||
/// It is provided with the suggestions box instance and the animation
|
|
||||||
/// controller, and expected to return some animation that uses the controller
|
|
||||||
/// to display the suggestion box.
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
/// ```dart
|
|
||||||
/// transitionBuilder: (context, suggestionsBox, animationController) {
|
|
||||||
/// return FadeTransition(
|
|
||||||
/// child: suggestionsBox,
|
|
||||||
/// opacity: CurvedAnimation(
|
|
||||||
/// parent: animationController,
|
|
||||||
/// curve: Curves.fastOutSlowIn
|
|
||||||
/// ),
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// This argument is best used with [animationDuration] and [animationStart]
|
|
||||||
/// to fully control the animation.
|
|
||||||
///
|
|
||||||
/// To fully remove the animation, just return `suggestionsBox`
|
|
||||||
///
|
|
||||||
/// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
|
|
||||||
final AnimationTransitionBuilder? transitionBuilder;
|
|
||||||
|
|
||||||
/// The duration that [transitionBuilder] animation takes.
|
|
||||||
///
|
|
||||||
/// This argument is best used with [transitionBuilder] and [animationStart]
|
|
||||||
/// to fully control the animation.
|
|
||||||
///
|
|
||||||
/// Defaults to 500 milliseconds.
|
|
||||||
final Duration animationDuration;
|
|
||||||
|
|
||||||
/// Determine the [SuggestionBox]'s direction.
|
|
||||||
///
|
|
||||||
/// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
|
|
||||||
/// and the [_SuggestionsList] will grow **down**.
|
|
||||||
///
|
|
||||||
/// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
|
|
||||||
/// and the [_SuggestionsList] will grow **up**.
|
|
||||||
///
|
|
||||||
/// [AxisDirection.left] and [AxisDirection.right] are not allowed.
|
|
||||||
final AxisDirection direction;
|
|
||||||
|
|
||||||
/// The value at which the [transitionBuilder] animation starts.
|
|
||||||
///
|
|
||||||
/// This argument is best used with [transitionBuilder] and [animationDuration]
|
|
||||||
/// to fully control the animation.
|
|
||||||
///
|
|
||||||
/// Defaults to 0.25.
|
|
||||||
final double animationStart;
|
|
||||||
|
|
||||||
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
|
||||||
/// that the TypeAhead widget displays
|
|
||||||
final TextFieldConfiguration textFieldConfiguration;
|
|
||||||
|
|
||||||
/// How far below the text field should the suggestions box be
|
|
||||||
///
|
|
||||||
/// Defaults to 5.0
|
|
||||||
final double suggestionsBoxVerticalOffset;
|
|
||||||
|
|
||||||
/// If set to true, suggestions will be fetched immediately when the field is
|
|
||||||
/// added to the view.
|
|
||||||
///
|
|
||||||
/// But the suggestions box will only be shown when the field receives focus.
|
|
||||||
/// To make the field receive focus immediately, you can set the `autofocus`
|
|
||||||
/// property in the [textFieldConfiguration] to true
|
|
||||||
///
|
|
||||||
/// Defaults to false
|
|
||||||
final bool getImmediateSuggestions;
|
|
||||||
|
|
||||||
/// If set to true, no loading box will be shown while suggestions are
|
|
||||||
/// being fetched. [loadingBuilder] will also be ignored.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool hideOnLoading;
|
|
||||||
|
|
||||||
/// If set to true, nothing will be shown if there are no results.
|
|
||||||
/// [noItemsFoundBuilder] will also be ignored.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool hideOnEmpty;
|
|
||||||
|
|
||||||
/// If set to true, nothing will be shown if there is an error.
|
|
||||||
/// [errorBuilder] will also be ignored.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool hideOnError;
|
|
||||||
|
|
||||||
/// If set to false, the suggestions box will stay opened after
|
|
||||||
/// the keyboard is closed.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool hideSuggestionsOnKeyboardHide;
|
|
||||||
|
|
||||||
/// If set to false, the suggestions box will show a circular
|
|
||||||
/// progress indicator when retrieving suggestions.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool keepSuggestionsOnLoading;
|
|
||||||
|
|
||||||
/// If set to true, the suggestions box will remain opened even after
|
|
||||||
/// selecting a suggestion.
|
|
||||||
///
|
|
||||||
/// Note that if this is enabled, the only way
|
|
||||||
/// to close the suggestions box is either manually via the
|
|
||||||
/// `SuggestionsBoxController` or when the user closes the software
|
|
||||||
/// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
|
|
||||||
/// with a physical keyboard will be unable to close the
|
|
||||||
/// box without a manual way via `SuggestionsBoxController`.
|
|
||||||
///
|
|
||||||
/// Defaults to false.
|
|
||||||
final bool keepSuggestionsOnSuggestionSelected;
|
|
||||||
|
|
||||||
/// If set to true, in the case where the suggestions box has less than
|
|
||||||
/// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
|
|
||||||
/// will be temporarily flipped if there's more room available in the opposite
|
|
||||||
/// direction.
|
|
||||||
///
|
|
||||||
/// Defaults to false
|
|
||||||
final bool autoFlipDirection;
|
|
||||||
|
|
||||||
final SelectionToTextTransformer<T>? selectionToTextTransformer;
|
|
||||||
|
|
||||||
/// Controls the text being edited.
|
|
||||||
///
|
|
||||||
/// If null, this widget will create its own [TextEditingController].
|
|
||||||
final TextEditingController? controller;
|
|
||||||
|
|
||||||
final bool hideKeyboard;
|
|
||||||
|
|
||||||
final ScrollController? scrollController;
|
|
||||||
|
|
||||||
/// Creates text field that auto-completes user input from a list of items
|
|
||||||
FormBuilderTypeAhead({
|
|
||||||
Key? key,
|
|
||||||
//From Super
|
|
||||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
|
||||||
bool enabled = true,
|
|
||||||
FocusNode? focusNode,
|
|
||||||
FormFieldSetter<T>? onSaved,
|
|
||||||
FormFieldValidator<T>? validator,
|
|
||||||
InputDecoration decoration = const InputDecoration(),
|
|
||||||
required String name,
|
|
||||||
required this.itemBuilder,
|
|
||||||
required this.suggestionsCallback,
|
|
||||||
T? initialValue,
|
|
||||||
ValueChanged<T?>? onChanged,
|
|
||||||
ValueTransformer<T?>? valueTransformer,
|
|
||||||
VoidCallback? onReset,
|
|
||||||
this.animationDuration = const Duration(milliseconds: 500),
|
|
||||||
this.animationStart = 0.25,
|
|
||||||
this.autoFlipDirection = false,
|
|
||||||
this.controller,
|
|
||||||
this.debounceDuration = const Duration(milliseconds: 300),
|
|
||||||
this.direction = AxisDirection.down,
|
|
||||||
this.errorBuilder,
|
|
||||||
this.getImmediateSuggestions = false,
|
|
||||||
this.hideKeyboard = false,
|
|
||||||
this.hideOnEmpty = false,
|
|
||||||
this.hideOnError = false,
|
|
||||||
this.hideOnLoading = false,
|
|
||||||
this.hideSuggestionsOnKeyboardHide = true,
|
|
||||||
this.keepSuggestionsOnLoading = true,
|
|
||||||
this.keepSuggestionsOnSuggestionSelected = false,
|
|
||||||
this.loadingBuilder,
|
|
||||||
this.noItemsFoundBuilder,
|
|
||||||
this.onSuggestionSelected,
|
|
||||||
this.scrollController,
|
|
||||||
this.selectionToTextTransformer,
|
|
||||||
this.suggestionsBoxController,
|
|
||||||
this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(),
|
|
||||||
this.suggestionsBoxVerticalOffset = 5.0,
|
|
||||||
this.textFieldConfiguration = const TextFieldConfiguration(),
|
|
||||||
this.transitionBuilder,
|
|
||||||
}) : assert(T == String || selectionToTextTransformer != null),
|
|
||||||
super(
|
|
||||||
key: key,
|
|
||||||
initialValue: initialValue,
|
|
||||||
name: name,
|
|
||||||
validator: validator,
|
|
||||||
valueTransformer: valueTransformer,
|
|
||||||
onChanged: onChanged,
|
|
||||||
autovalidateMode: autovalidateMode,
|
|
||||||
onSaved: onSaved,
|
|
||||||
enabled: enabled,
|
|
||||||
onReset: onReset,
|
|
||||||
decoration: decoration,
|
|
||||||
focusNode: focusNode,
|
|
||||||
builder: (FormFieldState<T?> field) {
|
|
||||||
final state = field as FormBuilderTypeAheadState<T>;
|
|
||||||
final theme = Theme.of(state.context);
|
|
||||||
|
|
||||||
return TypeAheadField<T>(
|
|
||||||
textFieldConfiguration: textFieldConfiguration.copyWith(
|
|
||||||
enabled: state.enabled,
|
|
||||||
controller: state._typeAheadController,
|
|
||||||
style: state.enabled
|
|
||||||
? textFieldConfiguration.style
|
|
||||||
: theme.textTheme.titleMedium!.copyWith(
|
|
||||||
color: theme.disabledColor,
|
|
||||||
),
|
|
||||||
focusNode: state.effectiveFocusNode,
|
|
||||||
decoration: state.decoration,
|
|
||||||
),
|
|
||||||
// TODO HACK to satisfy strictness
|
|
||||||
suggestionsCallback: suggestionsCallback,
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
transitionBuilder: (context, suggestionsBox, controller) =>
|
|
||||||
suggestionsBox,
|
|
||||||
onSuggestionSelected: (T suggestion) {
|
|
||||||
state.didChange(suggestion);
|
|
||||||
onSuggestionSelected?.call(suggestion);
|
|
||||||
},
|
|
||||||
getImmediateSuggestions: getImmediateSuggestions,
|
|
||||||
errorBuilder: errorBuilder,
|
|
||||||
noItemsFoundBuilder: noItemsFoundBuilder,
|
|
||||||
loadingBuilder: loadingBuilder,
|
|
||||||
debounceDuration: debounceDuration,
|
|
||||||
suggestionsBoxDecoration: suggestionsBoxDecoration,
|
|
||||||
suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
|
|
||||||
animationDuration: animationDuration,
|
|
||||||
animationStart: animationStart,
|
|
||||||
direction: direction,
|
|
||||||
hideOnLoading: hideOnLoading,
|
|
||||||
hideOnEmpty: hideOnEmpty,
|
|
||||||
hideOnError: hideOnError,
|
|
||||||
hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
|
|
||||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
|
||||||
autoFlipDirection: autoFlipDirection,
|
|
||||||
suggestionsBoxController: suggestionsBoxController,
|
|
||||||
keepSuggestionsOnSuggestionSelected:
|
|
||||||
keepSuggestionsOnSuggestionSelected,
|
|
||||||
hideKeyboard: hideKeyboard,
|
|
||||||
scrollController: scrollController,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<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.addListener(_handleControllerChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
// void _handleControllerChanged() {
|
|
||||||
// Suppress changes that originated from within this class.
|
|
||||||
//
|
|
||||||
// In the case where a controller has been passed in to this widget, we
|
|
||||||
// register this change listener. In these cases, we'll also receive change
|
|
||||||
// notifications for changes originating from within this class -- for
|
|
||||||
// example, the reset() method. In such cases, the FormField value will
|
|
||||||
// already have been set.
|
|
||||||
// if (_typeAheadController.text != value) {
|
|
||||||
// didChange(_typeAheadController.text as T);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChange(T? value) {
|
|
||||||
super.didChange(value);
|
|
||||||
var text = _getTextString(value);
|
|
||||||
|
|
||||||
if (_typeAheadController.text != text) {
|
|
||||||
_typeAheadController.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
// Dispose the _typeAheadController when initState created it
|
|
||||||
super.dispose();
|
|
||||||
_typeAheadController.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void reset() {
|
|
||||||
super.reset();
|
|
||||||
|
|
||||||
_typeAheadController.text = _getTextString(initialValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getTextString(T? value) {
|
|
||||||
var text = value == null
|
|
||||||
? ''
|
|
||||||
: widget.selectionToTextTransformer != null
|
|
||||||
? widget.selectionToTextTransformer!(value)
|
|
||||||
: value.toString();
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2019 Simon Lightfoot
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
|
|
||||||
typedef ChipSelected<T> = void Function(T data, bool selected);
|
|
||||||
typedef ChipsBuilder<T> = Widget Function(
|
|
||||||
BuildContext context, ChipsInputState<T> state, T data);
|
|
||||||
|
|
||||||
class ChipsInput<T> extends StatefulWidget {
|
|
||||||
const ChipsInput({
|
|
||||||
super.key,
|
|
||||||
this.decoration = const InputDecoration(),
|
|
||||||
required this.chipBuilder,
|
|
||||||
required this.suggestionBuilder,
|
|
||||||
required this.findSuggestions,
|
|
||||||
required this.onChanged,
|
|
||||||
this.onChipTapped,
|
|
||||||
});
|
|
||||||
|
|
||||||
final InputDecoration decoration;
|
|
||||||
final ChipsInputSuggestions<T> findSuggestions;
|
|
||||||
final ValueChanged<List<T>> onChanged;
|
|
||||||
final ValueChanged<T>? onChipTapped;
|
|
||||||
final ChipsBuilder<T> chipBuilder;
|
|
||||||
final ChipsBuilder<T> suggestionBuilder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ChipsInputState<T> createState() => ChipsInputState<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChipsInputState<T> extends State<ChipsInput<T>> {
|
|
||||||
static const kObjectReplacementChar = 0xFFFC;
|
|
||||||
|
|
||||||
Set<T> _chips = {};
|
|
||||||
List<T> _suggestions = [];
|
|
||||||
int _searchId = 0;
|
|
||||||
|
|
||||||
FocusNode _focusNode = FocusNode();
|
|
||||||
TextEditingValue _value = const TextEditingValue();
|
|
||||||
TextInputConnection? _connection;
|
|
||||||
|
|
||||||
String get text {
|
|
||||||
return String.fromCharCodes(
|
|
||||||
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEditingValue get currentTextEditingValue => _value;
|
|
||||||
|
|
||||||
bool get _hasInputConnection =>
|
|
||||||
_connection != null && (_connection?.attached ?? false);
|
|
||||||
|
|
||||||
void requestKeyboard() {
|
|
||||||
if (_focusNode.hasFocus) {
|
|
||||||
_openInputConnection();
|
|
||||||
} else {
|
|
||||||
FocusScope.of(context).requestFocus(_focusNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectSuggestion(T data) {
|
|
||||||
setState(() {
|
|
||||||
_chips.add(data);
|
|
||||||
_updateTextInputState();
|
|
||||||
_suggestions = [];
|
|
||||||
});
|
|
||||||
widget.onChanged(_chips.toList(growable: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
void deleteChip(T data) {
|
|
||||||
setState(() {
|
|
||||||
_chips.remove(data);
|
|
||||||
_updateTextInputState();
|
|
||||||
});
|
|
||||||
widget.onChanged(_chips.toList(growable: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusNode = FocusNode();
|
|
||||||
_focusNode.addListener(_onFocusChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onFocusChanged() {
|
|
||||||
if (_focusNode.hasFocus) {
|
|
||||||
_openInputConnection();
|
|
||||||
} else {
|
|
||||||
_closeInputConnectionIfNeeded();
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
// rebuild so that _TextCursor is hidden.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_focusNode.dispose();
|
|
||||||
_closeInputConnectionIfNeeded();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _openInputConnection() {
|
|
||||||
if (!_hasInputConnection) {
|
|
||||||
_connection?.setEditingState(_value);
|
|
||||||
}
|
|
||||||
_connection?.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeInputConnectionIfNeeded() {
|
|
||||||
if (_hasInputConnection) {
|
|
||||||
_connection?.close();
|
|
||||||
_connection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var chipsChildren = _chips
|
|
||||||
.map<Widget>(
|
|
||||||
(data) => widget.chipBuilder(context, this, data),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
chipsChildren.add(
|
|
||||||
SizedBox(
|
|
||||||
height: 32.0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_TextCaret(
|
|
||||||
resumed: _focusNode.hasFocus,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
//mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: requestKeyboard,
|
|
||||||
child: InputDecorator(
|
|
||||||
decoration: widget.decoration,
|
|
||||||
isFocused: _focusNode.hasFocus,
|
|
||||||
isEmpty: _value.text.isEmpty,
|
|
||||||
child: Wrap(
|
|
||||||
children: chipsChildren,
|
|
||||||
spacing: 4.0,
|
|
||||||
runSpacing: 4.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: _suggestions.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return widget.suggestionBuilder(
|
|
||||||
context, this, _suggestions[index]);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateEditingValue(TextEditingValue value) {
|
|
||||||
final oldCount = _countReplacements(_value);
|
|
||||||
final newCount = _countReplacements(value);
|
|
||||||
setState(() {
|
|
||||||
if (newCount < oldCount) {
|
|
||||||
_chips = Set.from(_chips.take(newCount));
|
|
||||||
}
|
|
||||||
_value = value;
|
|
||||||
});
|
|
||||||
_onSearchChanged(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
int _countReplacements(TextEditingValue value) {
|
|
||||||
return value.text.codeUnits
|
|
||||||
.where((ch) => ch == kObjectReplacementChar)
|
|
||||||
.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateTextInputState() {
|
|
||||||
final text =
|
|
||||||
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
|
|
||||||
_value = TextEditingValue(
|
|
||||||
text: text,
|
|
||||||
selection: TextSelection.collapsed(offset: text.length),
|
|
||||||
composing: TextRange(start: 0, end: text.length),
|
|
||||||
);
|
|
||||||
_connection?.setEditingState(_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSearchChanged(String value) async {
|
|
||||||
final localId = ++_searchId;
|
|
||||||
final results = await widget.findSuggestions(value);
|
|
||||||
if (_searchId == localId && mounted) {
|
|
||||||
setState(() => _suggestions = results
|
|
||||||
.where((profile) => !_chips.contains(profile))
|
|
||||||
.toList(growable: false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TextCaret extends StatefulWidget {
|
|
||||||
const _TextCaret({
|
|
||||||
this.resumed = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
final bool resumed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_TextCursorState createState() => _TextCursorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TextCursorState extends State<_TextCaret>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
bool _displayed = false;
|
|
||||||
late Timer _timer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTimer(Timer timer) {
|
|
||||||
setState(() => _displayed = !_displayed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_timer.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return FractionallySizedBox(
|
|
||||||
heightFactor: 0.7,
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
|
|
||||||
child: Container(
|
|
||||||
width: 2.0,
|
|
||||||
color: theme.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
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/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
typedef LabelOptionsSelector<T extends Label> = Map<int, T> Function(
|
|
||||||
DocumentBulkActionState state);
|
|
||||||
|
|
||||||
class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
|
|
||||||
final String title;
|
|
||||||
final String formFieldLabel;
|
|
||||||
final Widget formFieldPrefixIcon;
|
|
||||||
final LabelOptionsSelector<T> availableOptionsSelector;
|
|
||||||
final void Function(int? selectedId) onSubmit;
|
|
||||||
final int? initialValue;
|
|
||||||
final bool canCreateNewLabel;
|
|
||||||
|
|
||||||
const BulkEditLabelBottomSheet({
|
|
||||||
super.key,
|
|
||||||
required this.title,
|
|
||||||
required this.formFieldLabel,
|
|
||||||
required this.formFieldPrefixIcon,
|
|
||||||
required this.availableOptionsSelector,
|
|
||||||
required this.onSubmit,
|
|
||||||
this.initialValue,
|
|
||||||
required this.canCreateNewLabel,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BulkEditLabelBottomSheet<T>> createState() =>
|
|
||||||
_BulkEditLabelBottomSheetState<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BulkEditLabelBottomSheetState<T extends Label>
|
|
||||||
extends State<BulkEditLabelBottomSheet<T>> {
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.title,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).paddedOnly(bottom: 24),
|
|
||||||
FormBuilder(
|
|
||||||
key: _formKey,
|
|
||||||
child: LabelFormField<T>(
|
|
||||||
initialValue: widget.initialValue != null
|
|
||||||
? IdQueryParameter.fromId(widget.initialValue!)
|
|
||||||
: const IdQueryParameter.unset(),
|
|
||||||
canCreateNewLabel: widget.canCreateNewLabel,
|
|
||||||
name: "labelFormField",
|
|
||||||
options: widget.availableOptionsSelector(state),
|
|
||||||
labelText: widget.formFieldLabel,
|
|
||||||
prefixIcon: widget.formFieldPrefixIcon,
|
|
||||||
allowSelectUnassigned: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
const DialogCancelButton(),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_formKey.currentState?.saveAndValidate() ??
|
|
||||||
false) {
|
|
||||||
final value = _formKey.currentState
|
|
||||||
?.getRawValue('labelFormField')
|
|
||||||
as IdQueryParameter?;
|
|
||||||
widget.onSubmit(value?.maybeWhen(
|
|
||||||
fromId: (id) => id, orElse: () => null));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.apply),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutter/src/widgets/framework.dart';
|
|
||||||
// import 'package:flutter/src/widgets/placeholder.dart';
|
|
||||||
|
|
||||||
// class LabelBulkSelectionWidget extends StatelessWidget {
|
|
||||||
// final int labelId;
|
|
||||||
// final String title;
|
|
||||||
// final bool selected;
|
|
||||||
// final bool excluded;
|
|
||||||
// final Widget Function(int id) leadingWidgetBuilder;
|
|
||||||
// final void Function(int id) onSelected;
|
|
||||||
// final void Function(int id) onUnselected;
|
|
||||||
// final void Function(int id) onRemoved;
|
|
||||||
|
|
||||||
// const LabelBulkSelectionWidget({
|
|
||||||
// super.key,
|
|
||||||
// required this.labelId,
|
|
||||||
// required this.title,
|
|
||||||
// required this.leadingWidgetBuilder,
|
|
||||||
// required this.onSelected,
|
|
||||||
// required this.onUnselected,
|
|
||||||
// required this.onRemoved,
|
|
||||||
// });
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return ListTile(
|
|
||||||
// title: Text(title),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -6,16 +6,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:open_filex/open_filex.dart';
|
import 'package:open_filex/open_filex.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_description.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
part 'document_details_cubit.freezed.dart';
|
part 'document_details_cubit.freezed.dart';
|
||||||
part 'document_details_state.dart';
|
part 'document_details_state.dart';
|
||||||
|
|
||||||
@@ -94,11 +92,9 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
await loadMetaData();
|
await loadMetaData();
|
||||||
}
|
}
|
||||||
final desc = FileDescription.fromPath(
|
final filePath = state.metaData!.mediaFilename.replaceAll("/", " ");
|
||||||
state.metaData!.mediaFilename.replaceAll("/", " "),
|
|
||||||
);
|
|
||||||
|
|
||||||
final fileName = "${desc.filename}.pdf";
|
final fileName = "${p.basenameWithoutExtension(filePath)}.pdf";
|
||||||
final file = File("${cacheDir.path}/$fileName");
|
final file = File("${cacheDir.path}/$fileName");
|
||||||
|
|
||||||
if (!file.existsSync()) {
|
if (!file.existsSync()) {
|
||||||
@@ -126,50 +122,58 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
if (state.metaData == null) {
|
if (state.metaData == null) {
|
||||||
await loadMetaData();
|
await loadMetaData();
|
||||||
}
|
}
|
||||||
String filePath = _buildDownloadFilePath(
|
String targetPath = _buildDownloadFilePath(
|
||||||
downloadOriginal,
|
downloadOriginal,
|
||||||
await FileService.downloadsDirectory,
|
await FileService.downloadsDirectory,
|
||||||
);
|
);
|
||||||
final desc = FileDescription.fromPath(
|
|
||||||
state.metaData!.mediaFilename
|
if (!await File(targetPath).exists()) {
|
||||||
.replaceAll("/", " "), // Flatten directory structure
|
await File(targetPath).create();
|
||||||
);
|
|
||||||
if (!File(filePath).existsSync()) {
|
|
||||||
File(filePath).createSync();
|
|
||||||
} else {
|
} else {
|
||||||
return _notificationService.notifyFileDownload(
|
await _notificationService.notifyFileDownload(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
filename: "${desc.filename}.${desc.extension}",
|
filename: p.basename(targetPath),
|
||||||
filePath: filePath,
|
filePath: targetPath,
|
||||||
finished: true,
|
finished: true,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _notificationService.notifyFileDownload(
|
// await _notificationService.notifyFileDownload(
|
||||||
document: state.document,
|
// document: state.document,
|
||||||
filename: "${desc.filename}.${desc.extension}",
|
// filename: p.basename(targetPath),
|
||||||
filePath: filePath,
|
// filePath: targetPath,
|
||||||
finished: false,
|
// finished: false,
|
||||||
locale: locale,
|
// locale: locale,
|
||||||
userId: userId,
|
// userId: userId,
|
||||||
);
|
// );
|
||||||
|
|
||||||
await _api.downloadToFile(
|
await _api.downloadToFile(
|
||||||
state.document,
|
state.document,
|
||||||
filePath,
|
targetPath,
|
||||||
original: downloadOriginal,
|
original: downloadOriginal,
|
||||||
|
onProgressChanged: (progress) {
|
||||||
|
_notificationService.notifyFileDownload(
|
||||||
|
document: state.document,
|
||||||
|
filename: p.basename(targetPath),
|
||||||
|
filePath: targetPath,
|
||||||
|
finished: true,
|
||||||
|
locale: locale,
|
||||||
|
userId: userId,
|
||||||
|
progress: progress,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await _notificationService.notifyFileDownload(
|
await _notificationService.notifyFileDownload(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
filename: "${desc.filename}.${desc.extension}",
|
filename: p.basename(targetPath),
|
||||||
filePath: filePath,
|
filePath: targetPath,
|
||||||
finished: true,
|
finished: true,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
);
|
);
|
||||||
debugPrint("Downloaded file to $filePath");
|
debugPrint("Downloaded file to $targetPath");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shareDocument({bool shareOriginal = false}) async {
|
Future<void> shareDocument({bool shareOriginal = false}) async {
|
||||||
@@ -220,12 +224,9 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _buildDownloadFilePath(bool original, Directory dir) {
|
String _buildDownloadFilePath(bool original, Directory dir) {
|
||||||
final description = FileDescription.fromPath(
|
final normalizedPath = state.metaData!.mediaFilename.replaceAll("/", " ");
|
||||||
state.metaData!.mediaFilename
|
final extension = original ? p.extension(normalizedPath) : '.pdf';
|
||||||
.replaceAll("/", " "), // Flatten directory structure
|
return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension";
|
||||||
);
|
|
||||||
final extension = original ? description.extension : 'pdf';
|
|
||||||
return "${dir.path}/${description.filename}.$extension";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -48,7 +47,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasMultiUserSupport =
|
final hasMultiUserSupport =
|
||||||
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
||||||
final tabLength = 4 + (hasMultiUserSupport ? 1 : 0);
|
final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0);
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
@@ -86,52 +85,53 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
collapsedHeight: kToolbarHeight,
|
collapsedHeight: kToolbarHeight,
|
||||||
expandedHeight: 250.0,
|
expandedHeight: 250.0,
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
background: Stack(
|
background: BlocBuilder<DocumentDetailsCubit,
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
children: [
|
|
||||||
BlocBuilder<DocumentDetailsCubit,
|
|
||||||
DocumentDetailsState>(
|
DocumentDetailsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Positioned.fill(
|
return Hero(
|
||||||
|
tag: "thumb_${state.document.id}",
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
DocumentPreviewRoute($extra: state.document)
|
DocumentPreviewRoute($extra: state.document)
|
||||||
.push(context);
|
.push(context);
|
||||||
},
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
child: DocumentPreview(
|
child: DocumentPreview(
|
||||||
|
enableHero: false,
|
||||||
document: state.document,
|
document: state.document,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
stops: [0.2, 0.4],
|
||||||
|
colors: [
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.background
|
||||||
|
.withOpacity(0.6),
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.background
|
||||||
|
.withOpacity(0.3),
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
// Positioned.fill(
|
|
||||||
// top: -kToolbarHeight,
|
|
||||||
// child: DecoratedBox(
|
|
||||||
// decoration: BoxDecoration(
|
|
||||||
// gradient: LinearGradient(
|
|
||||||
// colors: [
|
|
||||||
// Theme.of(context)
|
|
||||||
// .colorScheme
|
|
||||||
// .background
|
|
||||||
// .withOpacity(0.8),
|
|
||||||
// Theme.of(context)
|
|
||||||
// .colorScheme
|
|
||||||
// .background
|
|
||||||
// .withOpacity(0.5),
|
|
||||||
// Colors.transparent,
|
|
||||||
// Colors.transparent,
|
|
||||||
// Colors.transparent,
|
|
||||||
// ],
|
|
||||||
// begin: Alignment.topCenter,
|
|
||||||
// end: Alignment.bottomCenter,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
bottom: ColoredTabBar(
|
bottom: ColoredTabBar(
|
||||||
tabBar: TabBar(
|
tabBar: TabBar(
|
||||||
@@ -177,7 +177,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasMultiUserSupport)
|
if (hasMultiUserSupport && false)
|
||||||
Tab(
|
Tab(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Permissions",
|
"Permissions",
|
||||||
@@ -266,7 +266,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (hasMultiUserSupport)
|
if (hasMultiUserSupport && false)
|
||||||
CustomScrollView(
|
CustomScrollView(
|
||||||
controller: _pagingScrollController,
|
controller: _pagingScrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
@@ -406,7 +406,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
if (delete) {
|
if (delete) {
|
||||||
try {
|
try {
|
||||||
await context.read<DocumentDetailsCubit>().delete(document);
|
await context.read<DocumentDetailsCubit>().delete(document);
|
||||||
showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
static const fkContent = 'content';
|
static const fkContent = 'content';
|
||||||
|
|
||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||||
bool _isSubmitLoading = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -314,18 +313,13 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
tags: (values[fkTags] as IdsTagsQuery?)?.include,
|
tags: (values[fkTags] as IdsTagsQuery?)?.include,
|
||||||
content: values[fkContent],
|
content: values[fkContent],
|
||||||
);
|
);
|
||||||
setState(() {
|
|
||||||
_isSubmitLoading = true;
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
|
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
|
||||||
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
|
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
|
||||||
_isSubmitLoading = false;
|
|
||||||
});
|
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
|
|
||||||
import 'package:image/image.dart' as im;
|
|
||||||
|
|
||||||
typedef ImageOperationCallback = im.Image Function(im.Image);
|
|
||||||
|
|
||||||
class DecodeParam {
|
|
||||||
final File file;
|
|
||||||
final SendPort sendPort;
|
|
||||||
final im.Image Function(im.Image) imageOperation;
|
|
||||||
DecodeParam(this.file, this.sendPort, this.imageOperation);
|
|
||||||
}
|
|
||||||
|
|
||||||
void decodeIsolate(DecodeParam param) {
|
|
||||||
// Read an image from file (webp in this case).
|
|
||||||
// decodeImage will identify the format of the image and use the appropriate
|
|
||||||
// decoder.
|
|
||||||
var image = im.decodeImage(param.file.readAsBytesSync())!;
|
|
||||||
// Resize the image to a 120x? thumbnail (maintaining the aspect ratio).
|
|
||||||
var processed = param.imageOperation(image);
|
|
||||||
param.sendPort.send(processed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode and process an image file in a separate thread (isolate) to avoid
|
|
||||||
// stalling the main UI thread.
|
|
||||||
Future<File> processImage(
|
|
||||||
File file,
|
|
||||||
ImageOperationCallback imageOperation,
|
|
||||||
) async {
|
|
||||||
var receivePort = ReceivePort();
|
|
||||||
|
|
||||||
await Isolate.spawn(
|
|
||||||
decodeIsolate,
|
|
||||||
DecodeParam(
|
|
||||||
file,
|
|
||||||
receivePort.sendPort,
|
|
||||||
imageOperation,
|
|
||||||
));
|
|
||||||
|
|
||||||
var image = await receivePort.first as im.Image;
|
|
||||||
|
|
||||||
return file.writeAsBytes(im.encodePng(image));
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import 'package:paperless_mobile/constants.dart';
|
|||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/global/constants.dart';
|
import 'package:paperless_mobile/core/global/constants.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_description.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
import 'package:paperless_mobile/features/document_scan/cubit/document_scanner_cubit.dart';
|
||||||
@@ -22,7 +21,6 @@ import 'package:paperless_mobile/features/document_scan/view/widgets/scanned_ima
|
|||||||
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
@@ -261,12 +259,12 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
$extra: file.bytes,
|
$extra: file.bytes,
|
||||||
fileExtension: file.extension,
|
fileExtension: file.extension,
|
||||||
).push<DocumentUploadResult>(context);
|
).push<DocumentUploadResult>(context);
|
||||||
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
|
if (uploadResult?.success ?? false) {
|
||||||
// For paperless version older than 1.11.3, task id will always be null!
|
// For paperless version older than 1.11.3, task id will always be null!
|
||||||
context.read<DocumentScannerCubit>().reset();
|
context.read<DocumentScannerCubit>().reset();
|
||||||
context
|
// context
|
||||||
.read<PendingTasksNotifier>()
|
// .read<PendingTasksNotifier>()
|
||||||
.listenToTaskChanges(uploadResult!.taskId!);
|
// .listenToTaskChanges(uploadResult!.taskId!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,17 +348,17 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
void _onUploadFromFilesystem() async {
|
void _onUploadFromFilesystem() async {
|
||||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: supportedFileExtensions,
|
allowedExtensions:
|
||||||
|
supportedFileExtensions.map((e) => e.replaceAll(".", "")).toList(),
|
||||||
withData: true,
|
withData: true,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
);
|
);
|
||||||
if (result?.files.single.path != null) {
|
if (result?.files.single.path != null) {
|
||||||
final path = result!.files.single.path!;
|
final path = result!.files.single.path!;
|
||||||
final fileDescription = FileDescription.fromPath(path);
|
final extension = p.extension(path);
|
||||||
|
final filename = p.basenameWithoutExtension(path);
|
||||||
File file = File(path);
|
File file = File(path);
|
||||||
if (!supportedFileExtensions.contains(
|
if (!supportedFileExtensions.contains(extension.toLowerCase())) {
|
||||||
fileDescription.extension.toLowerCase(),
|
|
||||||
)) {
|
|
||||||
showErrorMessage(
|
showErrorMessage(
|
||||||
context,
|
context,
|
||||||
const PaperlessApiException(ErrorCode.unsupportedFileFormat),
|
const PaperlessApiException(ErrorCode.unsupportedFileFormat),
|
||||||
@@ -369,10 +367,15 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
}
|
}
|
||||||
DocumentUploadRoute(
|
DocumentUploadRoute(
|
||||||
$extra: file.readAsBytesSync(),
|
$extra: file.readAsBytesSync(),
|
||||||
filename: fileDescription.filename,
|
filename: filename,
|
||||||
title: fileDescription.filename,
|
title: filename,
|
||||||
fileExtension: fileDescription.extension,
|
fileExtension: extension,
|
||||||
).push(context);
|
).push<DocumentUploadResult>(context);
|
||||||
|
// if (uploadResult.success && uploadResult.taskId != null) {
|
||||||
|
// context
|
||||||
|
// .read<PendingTasksNotifier>()
|
||||||
|
// .listenToTaskChanges(uploadResult.taskId!);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
|||||||
with DocumentPagingBlocMixin {
|
with DocumentPagingBlocMixin {
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
@@ -25,6 +27,7 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
|||||||
this.api,
|
this.api,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
this._userAppState,
|
this._userAppState,
|
||||||
|
this.connectivityStatusService,
|
||||||
) : super(
|
) : super(
|
||||||
DocumentSearchState(
|
DocumentSearchState(
|
||||||
searchHistory: _userAppState.documentSearchHistory),
|
searchHistory: _userAppState.documentSearchHistory),
|
||||||
@@ -120,9 +123,4 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
Future<void> onFilterUpdated(DocumentFilter filter) async {}
|
||||||
|
|
||||||
@override
|
|
||||||
// TODO: implement connectivityStatusService
|
|
||||||
ConnectivityStatusService get connectivityStatusService =>
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:paperless_mobile/features/document_search/cubit/document_search_
|
|||||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
||||||
|
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -51,7 +53,21 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.menu),
|
icon: ListenableBuilder(
|
||||||
|
listenable:
|
||||||
|
context.read<ConsumptionChangeNotifier>(),
|
||||||
|
builder: (context, child) {
|
||||||
|
return Badge(
|
||||||
|
isLabelVisible: context
|
||||||
|
.read<ConsumptionChangeNotifier>()
|
||||||
|
.pendingFiles
|
||||||
|
.isNotEmpty,
|
||||||
|
child: const Icon(Icons.menu),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
smallSize: 8,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
onPressed: Scaffold.of(context).openDrawer,
|
onPressed: Scaffold.of(context).openDrawer,
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
@@ -81,6 +97,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
|||||||
context.read(),
|
context.read(),
|
||||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
|
||||||
.get(context.read<LocalUserAccount>().id)!,
|
.get(context.read<LocalUserAccount>().id)!,
|
||||||
|
context.read(),
|
||||||
),
|
),
|
||||||
child: const DocumentSearchPage(),
|
child: const DocumentSearchPage(),
|
||||||
);
|
);
|
||||||
@@ -95,10 +112,7 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => Provider.value(
|
builder: (_) => const ManageAccountsPage(),
|
||||||
value: context.read<LocalUserAccount>(),
|
|
||||||
child: const ManageAccountsPage(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
@@ -9,7 +8,6 @@ import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dar
|
|||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
|
||||||
|
|
||||||
class SliverSearchBar extends StatelessWidget {
|
class SliverSearchBar extends StatelessWidget {
|
||||||
final bool floating;
|
final bool floating;
|
||||||
@@ -24,10 +22,8 @@ class SliverSearchBar extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) {
|
if (context.watch<LocalUserAccount>().paperlessUser.canViewDocuments) {
|
||||||
return SliverAppBar(
|
return const SliverAppBar(
|
||||||
titleSpacing: 8,
|
titleSpacing: 8,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
title: DocumentSearchBar(),
|
title: DocumentSearchBar(),
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
|
|
||||||
part 'document_upload_state.dart';
|
part 'document_upload_state.dart';
|
||||||
|
|
||||||
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||||
final PaperlessDocumentsApi _documentApi;
|
final PaperlessDocumentsApi _documentApi;
|
||||||
|
final PendingTasksNotifier _tasksNotifier;
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
final ConnectivityStatusService _connectivityStatusService;
|
final ConnectivityStatusService _connectivityStatusService;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this._documentApi,
|
this._documentApi,
|
||||||
this._connectivityStatusService,
|
this._connectivityStatusService,
|
||||||
|
this._tasksNotifier,
|
||||||
) : super(const DocumentUploadState()) {
|
) : super(const DocumentUploadState()) {
|
||||||
_labelRepository.addListener(
|
_labelRepository.addListener(
|
||||||
this,
|
this,
|
||||||
@@ -43,7 +45,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
int? asn,
|
int? asn,
|
||||||
}) async {
|
}) async {
|
||||||
return await _documentApi.create(
|
final taskId = await _documentApi.create(
|
||||||
bytes,
|
bytes,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -53,6 +55,10 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
asn: asn,
|
asn: asn,
|
||||||
);
|
);
|
||||||
|
if (taskId != null) {
|
||||||
|
_tasksNotifier.listenToTaskChanges(taskId);
|
||||||
|
}
|
||||||
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
@@ -24,7 +23,6 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie
|
|||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
|
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.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 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -247,8 +247,13 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: WillPopScope(
|
body: WillPopScope(
|
||||||
onWillPop: () async {
|
onWillPop: () async {
|
||||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
final cubit = context.read<DocumentsCubit>();
|
||||||
context.read<DocumentsCubit>().resetSelection();
|
if (cubit.state.selection.isNotEmpty) {
|
||||||
|
cubit.resetSelection();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cubit.state.filter.appliedFiltersCount > 0 || cubit.state.filter.selectedView != null) {
|
||||||
|
await _onResetFilter();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -31,16 +31,19 @@ class DocumentPreview extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ConnectivityAwareActionWrapper(
|
return ConnectivityAwareActionWrapper(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: isClickable
|
onTap: isClickable
|
||||||
? () => DocumentPreviewRoute($extra: document).push(context)
|
? () => DocumentPreviewRoute($extra: document).push(context)
|
||||||
: null,
|
: null,
|
||||||
child: HeroMode(
|
child: Builder(builder: (context) {
|
||||||
enabled: enableHero,
|
if (enableHero) {
|
||||||
child: Hero(
|
return Hero(
|
||||||
tag: "thumb_${document.id}",
|
tag: "thumb_${document.id}",
|
||||||
child: _buildPreview(context),
|
child: _buildPreview(context),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
|
return _buildPreview(context);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
|
|
||||||
class NewItemsLoadingWidget extends StatelessWidget {
|
|
||||||
const NewItemsLoadingWidget({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Center(child: const CircularProgressIndicator().padded());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
|
||||||
|
|
||||||
enum DateRangeSelection { before, after }
|
enum DateRangeSelection { before, after }
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
@@ -17,14 +16,9 @@ import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
|||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/verify_identity_route.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class HomeShellWidget extends StatelessWidget {
|
class HomeShellWidget extends StatelessWidget {
|
||||||
@@ -52,16 +46,16 @@ class HomeShellWidget extends StatelessWidget {
|
|||||||
return GlobalSettingsBuilder(
|
return GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
final currentUserId = settings.loggedInUserId;
|
final currentUserId = settings.loggedInUserId;
|
||||||
if (currentUserId == null) {
|
|
||||||
// This is currently the case (only for a few ms) when the current user logs out of the app.
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final apiVersion = ApiVersion(paperlessApiVersion);
|
final apiVersion = ApiVersion(paperlessApiVersion);
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable:
|
valueListenable:
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
Hive.localUserAccountBox.listenable(keys: [currentUserId]),
|
||||||
.listenable(keys: [currentUserId]),
|
|
||||||
builder: (context, box, _) {
|
builder: (context, box, _) {
|
||||||
|
if (currentUserId == null) {
|
||||||
|
//This only happens during logout...
|
||||||
|
//TODO: Find way so this does not occur anymore
|
||||||
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
final currentLocalUser = box.get(currentUserId)!;
|
final currentLocalUser = box.get(currentUserId)!;
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
key: ValueKey(currentUserId),
|
key: ValueKey(currentUserId),
|
||||||
@@ -181,9 +175,7 @@ class HomeShellWidget extends StatelessWidget {
|
|||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
);
|
);
|
||||||
if (currentLocalUser
|
if (currentLocalUser.paperlessUser.canViewInbox) {
|
||||||
.paperlessUser.canViewDocuments &&
|
|
||||||
currentLocalUser.paperlessUser.canViewTags) {
|
|
||||||
inboxCubit.initialize();
|
inboxCubit.initialize();
|
||||||
}
|
}
|
||||||
return inboxCubit;
|
return inboxCubit;
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class RouteDescription {
|
|
||||||
final String label;
|
|
||||||
final Icon icon;
|
|
||||||
final Icon selectedIcon;
|
|
||||||
final Widget Function(Widget icon)? badgeBuilder;
|
|
||||||
final bool enabled;
|
|
||||||
|
|
||||||
RouteDescription({
|
|
||||||
required this.label,
|
|
||||||
required this.icon,
|
|
||||||
required this.selectedIcon,
|
|
||||||
this.badgeBuilder,
|
|
||||||
this.enabled = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
NavigationDestination toNavigationDestination() {
|
|
||||||
return NavigationDestination(
|
|
||||||
label: label,
|
|
||||||
icon: badgeBuilder?.call(icon) ?? icon,
|
|
||||||
selectedIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationRailDestination toNavigationRailDestination() {
|
|
||||||
return NavigationRailDestination(
|
|
||||||
label: Text(label),
|
|
||||||
icon: icon,
|
|
||||||
selectedIcon: selectedIcon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomNavigationBarItem toBottomNavigationBarItem() {
|
|
||||||
return BottomNavigationBarItem(
|
|
||||||
label: label,
|
|
||||||
icon: badgeBuilder?.call(icon) ?? icon,
|
|
||||||
activeIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,6 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
|||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/theme.dart';
|
import 'package:paperless_mobile/theme.dart';
|
||||||
|
|
||||||
const _landingPage = 0;
|
|
||||||
const _documentsIndex = 1;
|
|
||||||
const _scannerIndex = 2;
|
|
||||||
const _labelsIndex = 3;
|
|
||||||
const _inboxIndex = 4;
|
|
||||||
|
|
||||||
class ScaffoldWithNavigationBar extends StatefulWidget {
|
class ScaffoldWithNavigationBar extends StatefulWidget {
|
||||||
final UserModel authenticatedUser;
|
final UserModel authenticatedUser;
|
||||||
final StatefulNavigationShell navigationShell;
|
final StatefulNavigationShell navigationShell;
|
||||||
@@ -29,11 +23,6 @@ class ScaffoldWithNavigationBar extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> {
|
class ScaffoldWithNavigationBarState extends State<ScaffoldWithNavigationBar> {
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.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/cubit/authentication_cubit.dart';
|
|
||||||
|
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.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)!.verifyYourIdentity),
|
|
||||||
),
|
|
||||||
body: UserAccountBuilder(
|
|
||||||
builder: (context, settings) {
|
|
||||||
if (settings == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(S
|
|
||||||
.of(context)!
|
|
||||||
.useTheConfiguredBiometricFactorToAuthenticate)
|
|
||||||
.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)!.disconnect,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => context
|
|
||||||
.read<AuthenticationCubit>()
|
|
||||||
.restoreSessionState(),
|
|
||||||
child: Text(S.of(context)!.verifyIdentity),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(16),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _logout(BuildContext context) {
|
|
||||||
context.read<AuthenticationCubit>().logout();
|
|
||||||
context.read<LabelRepository>().clear();
|
|
||||||
context.read<SavedViewRepository>().clear();
|
|
||||||
HydratedBloc.storage.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,8 +19,10 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
|
|
||||||
final PaperlessDocumentsApi _documentsApi;
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final ConnectivityStatusService connectivityStatusService;
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
@@ -35,21 +37,34 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
this.connectivityStatusService,
|
this.connectivityStatusService,
|
||||||
) : super(InboxState(
|
) : super(InboxState(labels: _labelRepository.state)) {
|
||||||
labels: _labelRepository.state,
|
|
||||||
)) {
|
|
||||||
notifier.addListener(
|
notifier.addListener(
|
||||||
this,
|
this,
|
||||||
onDeleted: remove,
|
onDeleted: remove,
|
||||||
onUpdated: (document) {
|
onUpdated: (document) {
|
||||||
if (document.tags
|
final hasInboxTag = document.tags
|
||||||
.toSet()
|
.toSet()
|
||||||
.intersection(state.inboxTags.toSet())
|
.intersection(state.inboxTags.toSet())
|
||||||
.isEmpty) {
|
.isNotEmpty;
|
||||||
|
final wasInInboxBeforeUpdate =
|
||||||
|
state.documents.map((e) => e.id).contains(document.id);
|
||||||
|
if (!hasInboxTag && wasInInboxBeforeUpdate) {
|
||||||
|
print(
|
||||||
|
"INBOX: Removing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||||
remove(document);
|
remove(document);
|
||||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||||
} else {
|
} else if (hasInboxTag) {
|
||||||
|
if (wasInInboxBeforeUpdate) {
|
||||||
|
print(
|
||||||
|
"INBOX: Replacing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||||
replace(document);
|
replace(document);
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
"INBOX: Adding document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||||
|
_addDocument(document);
|
||||||
|
emit(
|
||||||
|
state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -61,22 +76,20 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
await refreshItemsInInboxCount(false);
|
await refreshItemsInInboxCount(false);
|
||||||
await loadInbox();
|
await loadInbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
||||||
|
debugPrint("Checking for new items in inbox...");
|
||||||
final stats = await _statsApi.getServerStatistics();
|
final stats = await _statsApi.getServerStatistics();
|
||||||
|
|
||||||
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
|
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
|
||||||
await loadInbox();
|
await loadInbox();
|
||||||
}
|
}
|
||||||
emit(
|
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
|
||||||
state.copyWith(
|
|
||||||
itemsInInboxCount: stats.documentsInInbox,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -85,7 +98,6 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
Future<void> loadInbox() async {
|
Future<void> loadInbox() async {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
debugPrint("Initializing inbox...");
|
debugPrint("Initializing inbox...");
|
||||||
|
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
@@ -113,11 +125,22 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _addDocument(DocumentModel document) async {
|
||||||
|
emit(state.copyWith(
|
||||||
|
value: [
|
||||||
|
...state.value,
|
||||||
|
PagedSearchResult(
|
||||||
|
count: 1,
|
||||||
|
results: [document],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Fetches inbox tag ids and loads the inbox items (documents).
|
/// Fetches inbox tag ids and loads the inbox items (documents).
|
||||||
///
|
///
|
||||||
Future<void> reloadInbox() async {
|
Future<void> reloadInbox() async {
|
||||||
emit(state.copyWith(hasLoaded: false, isLoading: true));
|
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
@@ -134,6 +157,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
}
|
}
|
||||||
emit(state.copyWith(inboxTags: inboxTags));
|
emit(state.copyWith(inboxTags: inboxTags));
|
||||||
updateFilter(
|
updateFilter(
|
||||||
|
emitLoading: false,
|
||||||
filter: DocumentFilter(
|
filter: DocumentFilter(
|
||||||
sortField: SortField.added,
|
sortField: SortField.added,
|
||||||
tags: TagsQuery.ids(include: inboxTags.toList()),
|
tags: TagsQuery.ids(include: inboxTags.toList()),
|
||||||
@@ -154,7 +178,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
document.copyWith(tags: updatedTags),
|
document.copyWith(tags: updatedTags),
|
||||||
);
|
);
|
||||||
// Remove first so document is not replaced first.
|
// Remove first so document is not replaced first.
|
||||||
remove(document);
|
// remove(document);
|
||||||
notifier.notifyUpdated(updatedDocument);
|
notifier.notifyUpdated(updatedDocument);
|
||||||
return tagsToRemove;
|
return tagsToRemove;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
context.read<InboxCubit>().reloadInbox();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_nestedScrollViewKey.currentState!.innerController
|
_nestedScrollViewKey.currentState!.innerController
|
||||||
.addListener(_scrollExtentChangedListener);
|
.addListener(_scrollExtentChangedListener);
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
|
|
||||||
class StoragePathWidget extends StatelessWidget {
|
|
||||||
final StoragePath? storagePath;
|
|
||||||
final Color? textColor;
|
|
||||||
final bool isClickable;
|
|
||||||
final void Function(int? id)? onSelected;
|
|
||||||
|
|
||||||
const StoragePathWidget({
|
|
||||||
Key? key,
|
|
||||||
this.storagePath,
|
|
||||||
this.textColor,
|
|
||||||
this.isClickable = true,
|
|
||||||
this.onSelected,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AbsorbPointer(
|
|
||||||
absorbing: !isClickable,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => onSelected?.call(storagePath?.id),
|
|
||||||
child: Text(
|
|
||||||
storagePath?.name ?? "-",
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: textColor ?? Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -68,9 +69,10 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showFab = MediaQuery.viewInsetsOf(context).bottom == 0;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
floatingActionButton: widget.allowCreation
|
floatingActionButton: widget.allowCreation && showFab
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
heroTag: "fab_tags_form",
|
heroTag: "fab_tags_form",
|
||||||
onPressed: _onAddTag,
|
onPressed: _onAddTag,
|
||||||
@@ -238,10 +240,16 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
var matches = _options
|
var matches = _options
|
||||||
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||||
if (matches.isEmpty && widget.allowCreation) {
|
if (matches.isEmpty && widget.allowCreation) {
|
||||||
yield Text(S.of(context)!.noItemsFound);
|
yield Center(
|
||||||
yield TextButton(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(S.of(context)!.noItemsFound).padded(),
|
||||||
|
TextButton(
|
||||||
child: Text(S.of(context)!.addTag),
|
child: Text(S.of(context)!.addTag),
|
||||||
onPressed: _onAddTag,
|
onPressed: _onAddTag,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (final tag in matches) {
|
for (final tag in matches) {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showFab = MediaQuery.viewInsetsOf(context).bottom == 0;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final options = _filterOptionsByQuery(_textEditingController.text);
|
final options = _filterOptionsByQuery(_textEditingController.text);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -124,6 +125,13 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
floatingActionButton: showFab && widget.onCreateNewLabel != null
|
||||||
|
? FloatingActionButton(
|
||||||
|
heroTag: "fab_label_form",
|
||||||
|
onPressed: _onCreateNewLabel,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
body: Builder(
|
body: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
translateMatchingAlgorithmName(
|
translateMatchingAlgorithmName(
|
||||||
context, l.matchingAlgorithm) +
|
context, l.matchingAlgorithm) +
|
||||||
((l.match?.isNotEmpty ?? false)
|
(l.match.isNotEmpty ? ": ${l.match}" : ""),
|
||||||
? ": ${l.match}"
|
|
||||||
: ""),
|
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
),
|
||||||
onOpenEditPage: canEdit ? onEdit : null,
|
onOpenEditPage: canEdit ? onEdit : null,
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||||||
shape: Theme.of(context).cardTheme.shape,
|
shape: Theme.of(context).cardTheme.shape,
|
||||||
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
title: Text(S.of(context)!.documentsInInbox),
|
title: Text(S.of(context)!.documentsInInbox),
|
||||||
onTap: currentUser.canViewTags && currentUser.canViewDocuments
|
onTap: currentUser.canViewInbox
|
||||||
? () => InboxRoute().go(context)
|
? () => InboxRoute().go(context)
|
||||||
: null,
|
: null,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
@@ -161,9 +161,11 @@ class _LandingPageState extends State<LandingPage> {
|
|||||||
shape: Theme.of(context).cardTheme.shape,
|
shape: Theme.of(context).cardTheme.shape,
|
||||||
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
titleTextStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
title: Text(S.of(context)!.totalDocuments),
|
title: Text(S.of(context)!.totalDocuments),
|
||||||
onTap: () {
|
onTap: currentUser.canViewDocuments
|
||||||
|
? () {
|
||||||
DocumentsRoute().go(context);
|
DocumentsRoute().go(context);
|
||||||
},
|
}
|
||||||
|
: null,
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
stats.documentsTotal.toString(),
|
stats.documentsTotal.toString(),
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:fl_chart/fl_chart.dart';
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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:go_router/go_router.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
||||||
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
|
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
@@ -14,6 +12,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'
|
|||||||
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||||
|
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
@@ -22,21 +21,26 @@ import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
|||||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
part 'authentication_state.dart';
|
part 'authentication_state.dart';
|
||||||
|
|
||||||
|
typedef _FutureVoidCallback = Future<void> Function();
|
||||||
|
|
||||||
class AuthenticationCubit extends Cubit<AuthenticationState> {
|
class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||||
final LocalAuthenticationService _localAuthService;
|
final LocalAuthenticationService _localAuthService;
|
||||||
final PaperlessApiFactory _apiFactory;
|
final PaperlessApiFactory _apiFactory;
|
||||||
final SessionManager _sessionManager;
|
final SessionManager _sessionManager;
|
||||||
final ConnectivityStatusService _connectivityService;
|
final ConnectivityStatusService _connectivityService;
|
||||||
|
final LocalNotificationService _notificationService;
|
||||||
|
|
||||||
AuthenticationCubit(
|
AuthenticationCubit(
|
||||||
this._localAuthService,
|
this._localAuthService,
|
||||||
this._apiFactory,
|
this._apiFactory,
|
||||||
this._sessionManager,
|
this._sessionManager,
|
||||||
this._connectivityService,
|
this._connectivityService,
|
||||||
|
this._notificationService,
|
||||||
) : super(const UnauthenticatedState());
|
) : super(const UnauthenticatedState());
|
||||||
|
|
||||||
Future<void> login({
|
Future<void> login({
|
||||||
@@ -45,7 +49,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
}) async {
|
}) async {
|
||||||
assert(credentials.username != null && credentials.password != null);
|
assert(credentials.username != null && credentials.password != null);
|
||||||
emit(const CheckingLoginState());
|
if (state is AuthenticatingState) {
|
||||||
|
// Cancel duplicate login requests
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(const AuthenticatingState(AuthenticatingStage.authenticating));
|
||||||
final localUserId = "${credentials.username}@$serverUrl";
|
final localUserId = "${credentials.username}@$serverUrl";
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"login",
|
"login",
|
||||||
@@ -58,7 +66,29 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
credentials,
|
credentials,
|
||||||
clientCertificate,
|
clientCertificate,
|
||||||
_sessionManager,
|
_sessionManager,
|
||||||
|
onFetchUserInformation: () async {
|
||||||
|
emit(const AuthenticatingState(
|
||||||
|
AuthenticatingStage.fetchingUserInformation));
|
||||||
|
},
|
||||||
|
onPerformLogin: () async {
|
||||||
|
emit(const AuthenticatingState(AuthenticatingStage.authenticating));
|
||||||
|
},
|
||||||
|
onPersistLocalUserData: () async {
|
||||||
|
emit(const AuthenticatingState(
|
||||||
|
AuthenticatingStage.persistingLocalUserData));
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
AuthenticationErrorState(
|
||||||
|
serverUrl: serverUrl,
|
||||||
|
username: credentials.username!,
|
||||||
|
password: credentials.password!,
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark logged in user as currently active user.
|
// Mark logged in user as currently active user.
|
||||||
final globalSettings =
|
final globalSettings =
|
||||||
@@ -71,22 +101,28 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"login",
|
"login",
|
||||||
"User successfully logged in.",
|
"User successfully logged in.",
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
emit(const UnauthenticatedState());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switches to another account if it exists.
|
/// Switches to another account if it exists.
|
||||||
Future<void> switchAccount(String localUserId) async {
|
Future<void> switchAccount(String localUserId) async {
|
||||||
emit(const SwitchingAccountsState());
|
emit(const SwitchingAccountsState());
|
||||||
|
_debugPrintMessage(
|
||||||
|
"switchAccount",
|
||||||
|
"Trying to switch to user $localUserId...",
|
||||||
|
);
|
||||||
|
|
||||||
final globalSettings =
|
final globalSettings =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||||
if (globalSettings.loggedInUserId == localUserId) {
|
// if (globalSettings.loggedInUserId == localUserId) {
|
||||||
emit(AuthenticatedState(localUserId: localUserId));
|
// _debugPrintMessage(
|
||||||
return;
|
// "switchAccount",
|
||||||
}
|
// "User $localUserId is already logged in.",
|
||||||
final userAccountBox =
|
// );
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
// emit(AuthenticatedState(localUserId: localUserId));
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
final userAccountBox = Hive.localUserAccountBox;
|
||||||
|
|
||||||
if (!userAccountBox.containsKey(localUserId)) {
|
if (!userAccountBox.containsKey(localUserId)) {
|
||||||
debugPrint("User $localUserId not yet registered.");
|
debugPrint("User $localUserId not yet registered.");
|
||||||
@@ -99,10 +135,18 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
final authenticated = await _localAuthService
|
final authenticated = await _localAuthService
|
||||||
.authenticateLocalUser("Authenticate to switch your account.");
|
.authenticateLocalUser("Authenticate to switch your account.");
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
debugPrint("User not authenticated.");
|
_debugPrintMessage(
|
||||||
|
"switchAccount",
|
||||||
|
"User could not be authenticated.",
|
||||||
|
);
|
||||||
|
emit(VerifyIdentityState(userId: localUserId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
final currentlyLoggedInUser = globalSettings.loggedInUserId;
|
||||||
|
if (currentlyLoggedInUser != localUserId) {
|
||||||
|
await _notificationService.cancelUserNotifications(localUserId);
|
||||||
|
}
|
||||||
await withEncryptedBox<UserCredentials, void>(
|
await withEncryptedBox<UserCredentials, void>(
|
||||||
HiveBoxes.localUserCredentials, (credentialsBox) async {
|
HiveBoxes.localUserCredentials, (credentialsBox) async {
|
||||||
if (!credentialsBox.containsKey(localUserId)) {
|
if (!credentialsBox.containsKey(localUserId)) {
|
||||||
@@ -131,9 +175,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
apiVersion,
|
apiVersion,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(AuthenticatedState(
|
emit(AuthenticatedState(localUserId: localUserId));
|
||||||
localUserId: localUserId,
|
|
||||||
));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,19 +184,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
required String serverUrl,
|
required String serverUrl,
|
||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
required bool enableBiometricAuthentication,
|
required bool enableBiometricAuthentication,
|
||||||
|
required String locale,
|
||||||
}) async {
|
}) async {
|
||||||
assert(credentials.password != null && credentials.username != null);
|
assert(credentials.password != null && credentials.username != null);
|
||||||
final localUserId = "${credentials.username}@$serverUrl";
|
final localUserId = "${credentials.username}@$serverUrl";
|
||||||
final sessionManager = SessionManager();
|
|
||||||
|
final sessionManager = SessionManager([
|
||||||
|
LanguageHeaderInterceptor(locale),
|
||||||
|
]);
|
||||||
|
try {
|
||||||
await _addUser(
|
await _addUser(
|
||||||
localUserId,
|
localUserId,
|
||||||
serverUrl,
|
serverUrl,
|
||||||
credentials,
|
credentials,
|
||||||
clientCertificate,
|
clientCertificate,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
|
// onPerformLogin: () async {
|
||||||
|
// emit(AuthenticatingState(AuthenticatingStage.authenticating));
|
||||||
|
// await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
// },
|
||||||
);
|
);
|
||||||
|
|
||||||
return localUserId;
|
return localUserId;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
print(error);
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeAccount(String userId) async {
|
Future<void> removeAccount(String userId) async {
|
||||||
@@ -170,28 +226,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Performs a conditional hydration based on the local authentication success.
|
/// Restores the previous session if exists.
|
||||||
///
|
///
|
||||||
Future<void> restoreSessionState() async {
|
Future<void> restoreSession([String? userId]) async {
|
||||||
|
emit(const RestoringSessionState());
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"Trying to restore previous session...",
|
"Trying to restore previous session...",
|
||||||
);
|
);
|
||||||
final globalSettings =
|
final globalSettings =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||||
final localUserId = globalSettings.loggedInUserId;
|
final restoreSessionForUser = userId ?? globalSettings.loggedInUserId;
|
||||||
if (localUserId == null) {
|
// final localUserId = globalSettings.loggedInUserId;
|
||||||
|
if (restoreSessionForUser == null) {
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"There is nothing to restore.",
|
"There is nothing to restore.",
|
||||||
);
|
);
|
||||||
|
final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty;
|
||||||
// If there is nothing to restore, we can quit here.
|
// If there is nothing to restore, we can quit here.
|
||||||
emit(const UnauthenticatedState());
|
emit(
|
||||||
|
UnauthenticatedState(redirectToAccountSelection: otherAccountsExist),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final localUserAccountBox =
|
final localUserAccountBox =
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||||
final localUserAccount = localUserAccountBox.get(localUserId)!;
|
final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!;
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"Checking if biometric authentication is required...",
|
"Checking if biometric authentication is required...",
|
||||||
@@ -207,7 +268,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
final localAuthSuccess =
|
final localAuthSuccess =
|
||||||
await _localAuthService.authenticateLocalUser(authenticationMesage);
|
await _localAuthService.authenticateLocalUser(authenticationMesage);
|
||||||
if (!localAuthSuccess) {
|
if (!localAuthSuccess) {
|
||||||
emit(const RequiresLocalAuthenticationState());
|
emit(VerifyIdentityState(userId: restoreSessionForUser));
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"User could not be authenticated.",
|
"User could not be authenticated.",
|
||||||
@@ -231,7 +292,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
final authentication =
|
final authentication =
|
||||||
await withEncryptedBox<UserCredentials, UserCredentials>(
|
await withEncryptedBox<UserCredentials, UserCredentials>(
|
||||||
HiveBoxes.localUserCredentials, (box) {
|
HiveBoxes.localUserCredentials, (box) {
|
||||||
return box.get(globalSettings.loggedInUserId!);
|
return box.get(restoreSessionForUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (authentication == null) {
|
if (authentication == null) {
|
||||||
@@ -290,8 +351,9 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"Skipping update of server user (server could not be reached).",
|
"Skipping update of server user (server could not be reached).",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
globalSettings.loggedInUserId = restoreSessionForUser;
|
||||||
emit(AuthenticatedState(localUserId: localUserId));
|
await globalSettings.save();
|
||||||
|
emit(AuthenticatedState(localUserId: restoreSessionForUser));
|
||||||
|
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
@@ -300,7 +362,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout([bool removeAccount = false]) async {
|
Future<void> logout([bool removeAccount = false]) async {
|
||||||
emit(const LogginOutState());
|
emit(const LoggingOutState());
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"logout",
|
"logout",
|
||||||
"Trying to log out current user...",
|
"Trying to log out current user...",
|
||||||
@@ -308,13 +370,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
await _resetExternalState();
|
await _resetExternalState();
|
||||||
final globalSettings = Hive.globalSettingsBox.getValue()!;
|
final globalSettings = Hive.globalSettingsBox.getValue()!;
|
||||||
final userId = globalSettings.loggedInUserId!;
|
final userId = globalSettings.loggedInUserId!;
|
||||||
|
await _notificationService.cancelUserNotifications(userId);
|
||||||
|
|
||||||
|
final otherAccountsExist = Hive.localUserAccountBox.length > 1;
|
||||||
|
emit(UnauthenticatedState(redirectToAccountSelection: otherAccountsExist));
|
||||||
if (removeAccount) {
|
if (removeAccount) {
|
||||||
this.removeAccount(userId);
|
await this.removeAccount(userId);
|
||||||
}
|
}
|
||||||
globalSettings.loggedInUserId = null;
|
globalSettings.loggedInUserId = null;
|
||||||
await globalSettings.save();
|
await globalSettings.save();
|
||||||
|
|
||||||
emit(const UnauthenticatedState());
|
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"logout",
|
"logout",
|
||||||
"User successfully logged out.",
|
"User successfully logged out.",
|
||||||
@@ -322,16 +387,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resetExternalState() async {
|
Future<void> _resetExternalState() async {
|
||||||
_debugPrintMessage(
|
|
||||||
"_resetExternalState",
|
|
||||||
"Resetting session manager and clearing storage...",
|
|
||||||
);
|
|
||||||
_sessionManager.resetSettings();
|
_sessionManager.resetSettings();
|
||||||
await HydratedBloc.storage.clear();
|
await HydratedBloc.storage.clear();
|
||||||
_debugPrintMessage(
|
|
||||||
"_resetExternalState",
|
|
||||||
"Session manager successfully reset and storage cleared.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _addUser(
|
Future<int> _addUser(
|
||||||
@@ -339,8 +396,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
String serverUrl,
|
String serverUrl,
|
||||||
LoginFormCredentials credentials,
|
LoginFormCredentials credentials,
|
||||||
ClientCertificate? clientCert,
|
ClientCertificate? clientCert,
|
||||||
SessionManager sessionManager,
|
SessionManager sessionManager, {
|
||||||
) async {
|
_FutureVoidCallback? onPerformLogin,
|
||||||
|
_FutureVoidCallback? onPersistLocalUserData,
|
||||||
|
_FutureVoidCallback? onFetchUserInformation,
|
||||||
|
}) async {
|
||||||
assert(credentials.username != null && credentials.password != null);
|
assert(credentials.username != null && credentials.password != null);
|
||||||
_debugPrintMessage("_addUser", "Adding new user $localUserId...");
|
_debugPrintMessage("_addUser", "Adding new user $localUserId...");
|
||||||
|
|
||||||
@@ -356,6 +416,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"Trying to login user ${credentials.username} on $serverUrl...",
|
"Trying to login user ${credentials.username} on $serverUrl...",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await onPerformLogin?.call();
|
||||||
|
|
||||||
final token = await authApi.login(
|
final token = await authApi.login(
|
||||||
username: credentials.username!,
|
username: credentials.username!,
|
||||||
password: credentials.password!,
|
password: credentials.password!,
|
||||||
@@ -384,6 +446,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
);
|
);
|
||||||
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
|
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
|
||||||
}
|
}
|
||||||
|
await onFetchUserInformation?.call();
|
||||||
final apiVersion = await _getApiVersion(sessionManager.client);
|
final apiVersion = await _getApiVersion(sessionManager.client);
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"_addUser",
|
"_addUser",
|
||||||
@@ -413,6 +476,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"_addUser",
|
"_addUser",
|
||||||
"Persisting local user account...",
|
"Persisting local user account...",
|
||||||
);
|
);
|
||||||
|
await onPersistLocalUserData?.call();
|
||||||
// Create user account
|
// Create user account
|
||||||
await userAccountBox.put(
|
await userAccountBox.put(
|
||||||
localUserId,
|
localUserId,
|
||||||
@@ -490,7 +554,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"API version ($apiVersion) successfully retrieved.",
|
"API version ($apiVersion) successfully retrieved.",
|
||||||
);
|
);
|
||||||
return apiVersion;
|
return apiVersion;
|
||||||
} on DioException catch (e) {
|
} on DioException catch (_) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,34 +7,76 @@ sealed class AuthenticationState {
|
|||||||
switch (this) { AuthenticatedState() => true, _ => false };
|
switch (this) { AuthenticatedState() => true, _ => false };
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnauthenticatedState extends AuthenticationState {
|
class UnauthenticatedState extends AuthenticationState with EquatableMixin {
|
||||||
const UnauthenticatedState();
|
final bool redirectToAccountSelection;
|
||||||
|
|
||||||
|
const UnauthenticatedState({this.redirectToAccountSelection = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [redirectToAccountSelection];
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequiresLocalAuthenticationState extends AuthenticationState {
|
class RestoringSessionState extends AuthenticationState {
|
||||||
const RequiresLocalAuthenticationState();
|
const RestoringSessionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class CheckingLoginState extends AuthenticationState {
|
class VerifyIdentityState extends AuthenticationState {
|
||||||
const CheckingLoginState();
|
final String userId;
|
||||||
|
const VerifyIdentityState({required this.userId});
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogginOutState extends AuthenticationState {
|
class AuthenticatingState extends AuthenticationState with EquatableMixin {
|
||||||
const LogginOutState();
|
final AuthenticatingStage currentStage;
|
||||||
|
const AuthenticatingState(this.currentStage);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [currentStage];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticatedState extends AuthenticationState {
|
class LoggingOutState extends AuthenticationState {
|
||||||
|
const LoggingOutState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticatedState extends AuthenticationState with EquatableMixin {
|
||||||
final String localUserId;
|
final String localUserId;
|
||||||
|
|
||||||
const AuthenticatedState({
|
const AuthenticatedState({required this.localUserId});
|
||||||
required this.localUserId,
|
|
||||||
});
|
@override
|
||||||
|
List<Object?> get props => [localUserId];
|
||||||
}
|
}
|
||||||
|
|
||||||
class SwitchingAccountsState extends AuthenticationState {
|
class SwitchingAccountsState extends AuthenticationState {
|
||||||
const SwitchingAccountsState();
|
const SwitchingAccountsState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticationErrorState extends AuthenticationState {
|
class AuthenticationErrorState extends AuthenticationState with EquatableMixin {
|
||||||
const AuthenticationErrorState();
|
final ErrorCode? errorCode;
|
||||||
|
final String serverUrl;
|
||||||
|
final ClientCertificate? clientCertificate;
|
||||||
|
final String username;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const AuthenticationErrorState({
|
||||||
|
this.errorCode,
|
||||||
|
required this.serverUrl,
|
||||||
|
this.clientCertificate,
|
||||||
|
required this.username,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
errorCode,
|
||||||
|
serverUrl,
|
||||||
|
clientCertificate,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthenticatingStage {
|
||||||
|
authenticating,
|
||||||
|
persistingLocalUserData,
|
||||||
|
fetchingUserInformation,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
|
||||||
|
|
||||||
class OldAuthenticationState with EquatableMixin {
|
|
||||||
final bool showBiometricAuthenticationScreen;
|
|
||||||
final bool isAuthenticated;
|
|
||||||
final String? username;
|
|
||||||
final String? fullName;
|
|
||||||
final String? localUserId;
|
|
||||||
final int? apiVersion;
|
|
||||||
|
|
||||||
const OldAuthenticationState({
|
|
||||||
this.isAuthenticated = false,
|
|
||||||
this.showBiometricAuthenticationScreen = false,
|
|
||||||
this.username,
|
|
||||||
this.fullName,
|
|
||||||
this.localUserId,
|
|
||||||
this.apiVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
OldAuthenticationState copyWith({
|
|
||||||
bool? isAuthenticated,
|
|
||||||
bool? showBiometricAuthenticationScreen,
|
|
||||||
String? username,
|
|
||||||
String? fullName,
|
|
||||||
String? localUserId,
|
|
||||||
int? apiVersion,
|
|
||||||
}) {
|
|
||||||
return OldAuthenticationState(
|
|
||||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
|
||||||
showBiometricAuthenticationScreen: showBiometricAuthenticationScreen ??
|
|
||||||
this.showBiometricAuthenticationScreen,
|
|
||||||
username: username ?? this.username,
|
|
||||||
fullName: fullName ?? this.fullName,
|
|
||||||
localUserId: localUserId ?? this.localUserId,
|
|
||||||
apiVersion: apiVersion ?? this.apiVersion,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
localUserId,
|
|
||||||
username,
|
|
||||||
fullName,
|
|
||||||
isAuthenticated,
|
|
||||||
showBiometricAuthenticationScreen,
|
|
||||||
apiVersion,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -12,5 +12,9 @@ class ClientCertificate {
|
|||||||
@HiveField(1)
|
@HiveField(1)
|
||||||
String? passphrase;
|
String? passphrase;
|
||||||
|
|
||||||
ClientCertificate({required this.bytes, this.passphrase});
|
|
||||||
|
ClientCertificate({
|
||||||
|
required this.bytes,
|
||||||
|
this.passphrase,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ class ClientCertificateFormModel {
|
|||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
final String? passphrase;
|
final String? passphrase;
|
||||||
|
|
||||||
ClientCertificateFormModel({required this.bytes, this.passphrase});
|
ClientCertificateFormModel({
|
||||||
|
required this.bytes,
|
||||||
|
this.passphrase,
|
||||||
|
});
|
||||||
|
|
||||||
ClientCertificateFormModel copyWith({Uint8List? bytes, String? passphrase}) {
|
ClientCertificateFormModel copyWith({
|
||||||
|
Uint8List? bytes,
|
||||||
|
String? passphrase,
|
||||||
|
String? filePath,
|
||||||
|
}) {
|
||||||
return ClientCertificateFormModel(
|
return ClientCertificateFormModel(
|
||||||
bytes: bytes ?? this.bytes,
|
bytes: bytes ?? this.bytes,
|
||||||
passphrase: passphrase ?? this.passphrase,
|
passphrase: passphrase ?? this.passphrase,
|
||||||
|
|||||||
@@ -3,27 +3,21 @@ 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';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
||||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.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 AddAccountPage extends StatefulWidget {
|
class AddAccountPage extends StatefulWidget {
|
||||||
final FutureOr<void> Function(
|
final FutureOr<void> Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -33,17 +27,27 @@ class AddAccountPage extends StatefulWidget {
|
|||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
) onSubmit;
|
) onSubmit;
|
||||||
|
|
||||||
final String submitText;
|
final String? initialServerUrl;
|
||||||
final String titleString;
|
final String? initialUsername;
|
||||||
|
final String? initialPassword;
|
||||||
|
final ClientCertificate? initialClientCertificate;
|
||||||
|
|
||||||
|
final String submitText;
|
||||||
|
final String titleText;
|
||||||
final bool showLocalAccounts;
|
final bool showLocalAccounts;
|
||||||
|
|
||||||
|
final Widget? bottomLeftButton;
|
||||||
const AddAccountPage({
|
const AddAccountPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.submitText,
|
required this.submitText,
|
||||||
required this.titleString,
|
required this.titleText,
|
||||||
this.showLocalAccounts = false,
|
this.showLocalAccounts = false,
|
||||||
|
this.initialServerUrl,
|
||||||
|
this.initialUsername,
|
||||||
|
this.initialPassword,
|
||||||
|
this.initialClientCertificate,
|
||||||
|
this.bottomLeftButton,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -52,86 +56,170 @@ class AddAccountPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _AddAccountPageState extends State<AddAccountPage> {
|
class _AddAccountPageState extends State<AddAccountPage> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
bool _isCheckingConnection = false;
|
||||||
|
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
||||||
|
|
||||||
final PageController _pageController = PageController();
|
bool _isFormSubmitted = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable:
|
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
|
|
||||||
builder: (context, localAccounts, child) {
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
body: FormBuilder(
|
|
||||||
key: _formKey,
|
|
||||||
child: PageView(
|
|
||||||
controller: _pageController,
|
|
||||||
scrollBehavior: NeverScrollableScrollBehavior(),
|
|
||||||
children: [
|
|
||||||
if (widget.showLocalAccounts && localAccounts.isNotEmpty)
|
|
||||||
Scaffold(
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(S.of(context)!.logInToExistingAccount),
|
title: Text(widget.titleText),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: BottomAppBar(
|
bottomNavigationBar: BottomAppBar(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: widget.bottomLeftButton != null
|
||||||
|
? MainAxisAlignment.spaceBetween
|
||||||
|
: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
if (widget.bottomLeftButton != null) widget.bottomLeftButton!,
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: Text(S.of(context)!.goToLogin),
|
child: Text(S.of(context)!.loginPageSignInTitle),
|
||||||
onPressed: () {
|
onPressed: _reachabilityStatus == ReachabilityStatus.reachable &&
|
||||||
_pageController.nextPage(
|
!_isFormSubmitted
|
||||||
duration: const Duration(milliseconds: 300),
|
? _onSubmit
|
||||||
curve: Curves.easeInOut,
|
: null,
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ListView.builder(
|
resizeToAvoidBottomInset: true,
|
||||||
itemBuilder: (context, index) {
|
body: FormBuilder(
|
||||||
final account = localAccounts.values.elementAt(index);
|
key: _formKey,
|
||||||
return Card(
|
child: ListView(
|
||||||
child: UserAccountListTile(
|
children: [
|
||||||
account: account,
|
ServerAddressFormField(
|
||||||
onTap: () {
|
initialValue: widget.initialServerUrl,
|
||||||
context
|
onSubmit: (address) {
|
||||||
.read<AuthenticationCubit>()
|
_updateReachability(address);
|
||||||
.switchAccount(account.id);
|
|
||||||
},
|
},
|
||||||
|
).padded(),
|
||||||
|
ClientCertificateFormField(
|
||||||
|
initialBytes: widget.initialClientCertificate?.bytes,
|
||||||
|
initialPassphrase: widget.initialClientCertificate?.passphrase,
|
||||||
|
onChanged: (_) => _updateReachability(),
|
||||||
|
).padded(),
|
||||||
|
_buildStatusIndicator(),
|
||||||
|
if (_reachabilityStatus == ReachabilityStatus.reachable) ...[
|
||||||
|
UserCredentialsFormField(
|
||||||
|
formKey: _formKey,
|
||||||
|
initialUsername: widget.initialUsername,
|
||||||
|
initialPassword: widget.initialPassword,
|
||||||
|
onFieldsSubmitted: _onSubmit,
|
||||||
),
|
),
|
||||||
);
|
Text(
|
||||||
},
|
S.of(context)!.loginRequiredPermissionsHint,
|
||||||
itemCount: localAccounts.length,
|
style: Theme.of(context).textTheme.bodySmall?.apply(
|
||||||
),
|
color: Theme.of(context)
|
||||||
),
|
.colorScheme
|
||||||
ServerConnectionPage(
|
.onBackground
|
||||||
titleText: widget.titleString,
|
.withOpacity(0.6),
|
||||||
formBuilderKey: _formKey,
|
|
||||||
onContinue: () {
|
|
||||||
_pageController.nextPage(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ServerLoginPage(
|
|
||||||
formBuilderKey: _formKey,
|
|
||||||
submitText: widget.submitText,
|
|
||||||
onSubmit: _login,
|
|
||||||
),
|
),
|
||||||
|
).padded(16),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _login() async {
|
Future<void> _updateReachability([String? address]) async {
|
||||||
|
setState(() {
|
||||||
|
_isCheckingConnection = true;
|
||||||
|
});
|
||||||
|
final certForm =
|
||||||
|
_formKey.currentState?.getRawValue<ClientCertificateFormModel>(
|
||||||
|
ClientCertificateFormField.fkClientCertificate,
|
||||||
|
);
|
||||||
|
final status = await context
|
||||||
|
.read<ConnectivityStatusService>()
|
||||||
|
.isPaperlessServerReachable(
|
||||||
|
address ??
|
||||||
|
_formKey.currentState!
|
||||||
|
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||||
|
certForm != null
|
||||||
|
? ClientCertificate(
|
||||||
|
bytes: certForm.bytes,
|
||||||
|
passphrase: certForm.passphrase,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isCheckingConnection = false;
|
||||||
|
_reachabilityStatus = status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusIndicator() {
|
||||||
|
if (_isCheckingConnection) {
|
||||||
|
return const ListTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color errorColor = Theme.of(context).colorScheme.error;
|
||||||
|
switch (_reachabilityStatus) {
|
||||||
|
case ReachabilityStatus.unknown:
|
||||||
|
return Container();
|
||||||
|
case ReachabilityStatus.reachable:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.done,
|
||||||
|
S.of(context)!.connectionSuccessfulylEstablished,
|
||||||
|
Colors.green,
|
||||||
|
);
|
||||||
|
case ReachabilityStatus.notReachable:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.close,
|
||||||
|
S.of(context)!.couldNotEstablishConnectionToTheServer,
|
||||||
|
errorColor,
|
||||||
|
);
|
||||||
|
case ReachabilityStatus.unknownHost:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.close,
|
||||||
|
S.of(context)!.hostCouldNotBeResolved,
|
||||||
|
errorColor,
|
||||||
|
);
|
||||||
|
case ReachabilityStatus.missingClientCertificate:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.close,
|
||||||
|
S.of(context)!.loginPageReachabilityMissingClientCertificateText,
|
||||||
|
errorColor,
|
||||||
|
);
|
||||||
|
case ReachabilityStatus.invalidClientCertificateConfiguration:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.close,
|
||||||
|
S.of(context)!.incorrectOrMissingCertificatePassphrase,
|
||||||
|
errorColor,
|
||||||
|
);
|
||||||
|
case ReachabilityStatus.connectionTimeout:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.close,
|
||||||
|
S.of(context)!.connectionTimedOut,
|
||||||
|
errorColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSubmit() async {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
setState(() {
|
||||||
|
_isFormSubmitted = true;
|
||||||
|
});
|
||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
final form = _formKey.currentState!.value;
|
final form = _formKey.currentState!.value;
|
||||||
ClientCertificate? clientCert;
|
ClientCertificate? clientCert;
|
||||||
@@ -162,6 +250,10 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
|||||||
showInfoMessage(context, error);
|
showInfoMessage(context, error);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showGenericError(context, error);
|
showGenericError(context, error);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isFormSubmitted = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
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:go_router/go_router.dart';
|
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||||
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
||||||
@@ -13,18 +13,41 @@ import 'package:paperless_mobile/features/login/model/login_form_credentials.dar
|
|||||||
import 'package:paperless_mobile/features/login/view/add_account_page.dart';
|
import 'package:paperless_mobile/features/login/view/add_account_page.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 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||||
|
|
||||||
class LoginPage extends StatelessWidget {
|
class LoginPage extends StatelessWidget {
|
||||||
const LoginPage({super.key});
|
final String? initialServerUrl;
|
||||||
|
final String? initialUsername;
|
||||||
|
final String? initialPassword;
|
||||||
|
final ClientCertificate? initialClientCertificate;
|
||||||
|
|
||||||
|
const LoginPage({
|
||||||
|
super.key,
|
||||||
|
this.initialServerUrl,
|
||||||
|
this.initialUsername,
|
||||||
|
this.initialPassword,
|
||||||
|
this.initialClientCertificate,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AddAccountPage(
|
return AddAccountPage(
|
||||||
titleString: S.of(context)!.connectToPaperless,
|
titleText: S.of(context)!.connectToPaperless,
|
||||||
submitText: S.of(context)!.signIn,
|
submitText: S.of(context)!.signIn,
|
||||||
onSubmit: _onLogin,
|
onSubmit: _onLogin,
|
||||||
showLocalAccounts: true,
|
showLocalAccounts: true,
|
||||||
|
initialServerUrl: initialServerUrl,
|
||||||
|
initialUsername: initialUsername,
|
||||||
|
initialPassword: initialPassword,
|
||||||
|
initialClientCertificate: initialClientCertificate,
|
||||||
|
bottomLeftButton: Hive.localUserAccountBox.isNotEmpty
|
||||||
|
? TextButton(
|
||||||
|
child: Text(S.of(context)!.logInToExistingAccount),
|
||||||
|
onPressed: () {
|
||||||
|
const LoginToExistingAccountRoute().go(context);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
lib/features/login/view/login_to_existing_account_page.dart
Normal file
66
lib/features/login/view/login_to_existing_account_page.dart
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.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/routes/typed/top_level/login_route.dart';
|
||||||
|
|
||||||
|
class LoginToExistingAccountPage extends StatelessWidget {
|
||||||
|
const LoginToExistingAccountPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: Hive.localUserAccountBox.listenable(),
|
||||||
|
builder: (context, value, _) {
|
||||||
|
final localAccounts = value.values;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: Text(S.of(context)!.logInToExistingAccount),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: BottomAppBar(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(S.of(context)!.addAnotherAccount),
|
||||||
|
onPressed: () {
|
||||||
|
const LoginRoute().go(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final account = localAccounts.elementAt(index);
|
||||||
|
return Card(
|
||||||
|
child: UserAccountListTile(
|
||||||
|
account: account,
|
||||||
|
onTap: () {
|
||||||
|
context
|
||||||
|
.read<AuthenticationCubit>()
|
||||||
|
.switchAccount(account.id);
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
tooltip: S.of(context)!.remove,
|
||||||
|
icon: Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
context
|
||||||
|
.read<AuthenticationCubit>()
|
||||||
|
.removeAccount(account.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: localAccounts.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
lib/features/login/view/verify_identity_page.dart
Normal file
64
lib/features/login/view/verify_identity_page.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class VerifyIdentityPage extends StatelessWidget {
|
||||||
|
final String userId;
|
||||||
|
const VerifyIdentityPage({super.key, required this.userId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
title: Text(S.of(context)!.verifyYourIdentity),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: BottomAppBar(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
const LoginToExistingAccountRoute().go(context);
|
||||||
|
},
|
||||||
|
child: Text(S.of(context)!.goToLogin),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<AuthenticationCubit>().restoreSession(userId),
|
||||||
|
child: Text(S.of(context)!.verifyIdentity),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).paddedSymmetrically(horizontal: 16),
|
||||||
|
const Icon(
|
||||||
|
Icons.fingerprint,
|
||||||
|
size: 96,
|
||||||
|
),
|
||||||
|
// Wrap(
|
||||||
|
// alignment: WrapAlignment.spaceBetween,
|
||||||
|
// runAlignment: WrapAlignment.spaceBetween,
|
||||||
|
// runSpacing: 8,
|
||||||
|
// spacing: 8,
|
||||||
|
// children: [
|
||||||
|
|
||||||
|
// ],
|
||||||
|
// ).padded(16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -12,11 +13,16 @@ import 'obscured_input_text_form_field.dart';
|
|||||||
class ClientCertificateFormField extends StatefulWidget {
|
class ClientCertificateFormField extends StatefulWidget {
|
||||||
static const fkClientCertificate = 'clientCertificate';
|
static const fkClientCertificate = 'clientCertificate';
|
||||||
|
|
||||||
|
final String? initialPassphrase;
|
||||||
|
final Uint8List? initialBytes;
|
||||||
|
|
||||||
final void Function(ClientCertificateFormModel? cert) onChanged;
|
final void Function(ClientCertificateFormModel? cert) onChanged;
|
||||||
const ClientCertificateFormField({
|
const ClientCertificateFormField({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
}) : super(key: key);
|
this.initialPassphrase,
|
||||||
|
this.initialBytes,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ClientCertificateFormField> createState() =>
|
State<ClientCertificateFormField> createState() =>
|
||||||
@@ -31,7 +37,12 @@ class _ClientCertificateFormFieldState
|
|||||||
return FormBuilderField<ClientCertificateFormModel?>(
|
return FormBuilderField<ClientCertificateFormModel?>(
|
||||||
key: const ValueKey('login-client-cert'),
|
key: const ValueKey('login-client-cert'),
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
initialValue: null,
|
initialValue: widget.initialBytes != null
|
||||||
|
? ClientCertificateFormModel(
|
||||||
|
bytes: widget.initialBytes!,
|
||||||
|
passphrase: widget.initialPassphrase,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -108,8 +119,7 @@ class _ClientCertificateFormFieldState
|
|||||||
),
|
),
|
||||||
label: S.of(context)!.passphrase,
|
label: S.of(context)!.passphrase,
|
||||||
).padded(),
|
).padded(),
|
||||||
] else
|
]
|
||||||
...[]
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -122,21 +132,24 @@ class _ClientCertificateFormFieldState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onSelectFile(
|
Future<void> _onSelectFile(
|
||||||
FormFieldState<ClientCertificateFormModel?> field) async {
|
FormFieldState<ClientCertificateFormModel?> field,
|
||||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
) async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
);
|
);
|
||||||
if (result != null && result.files.single.path != null) {
|
if (result == null || result.files.single.path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
File file = File(result.files.single.path!);
|
File file = File(result.files.single.path!);
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedFile = file;
|
_selectedFile = file;
|
||||||
});
|
});
|
||||||
final changedValue =
|
final bytes = await file.readAsBytes();
|
||||||
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
|
|
||||||
ClientCertificateFormModel(bytes: file.readAsBytesSync());
|
final changedValue = field.value?.copyWith(bytes: bytes) ??
|
||||||
|
ClientCertificateFormModel(bytes: bytes);
|
||||||
field.didChange(changedValue);
|
field.didChange(changedValue);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSelectedFileText(
|
Widget _buildSelectedFileText(
|
||||||
FormFieldState<ClientCertificateFormModel?> field) {
|
FormFieldState<ClientCertificateFormModel?> field) {
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ 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 String? initialValue;
|
||||||
final void Function(String? address) onSubmit;
|
final void Function(String? address) onSubmit;
|
||||||
const ServerAddressFormField({
|
const ServerAddressFormField({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
|
this.initialValue,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -38,6 +39,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderField<String>(
|
return FormBuilderField<String>(
|
||||||
|
initialValue: widget.initialValue,
|
||||||
name: ServerAddressFormField.fkServerAddress,
|
name: ServerAddressFormField.fkServerAddress,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -90,7 +92,7 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: false,
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
onFieldSubmitted();
|
onFieldSubmitted();
|
||||||
_formatInput();
|
_formatInput();
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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_extensions.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class UserCredentialsFormField extends StatefulWidget {
|
class UserCredentialsFormField extends StatefulWidget {
|
||||||
static const fkCredentials = 'credentials';
|
static const fkCredentials = 'credentials';
|
||||||
|
|
||||||
final void Function() onFieldsSubmitted;
|
final void Function() onFieldsSubmitted;
|
||||||
|
final String? initialUsername;
|
||||||
|
final String? initialPassword;
|
||||||
|
final GlobalKey<FormBuilderState> formKey;
|
||||||
const UserCredentialsFormField({
|
const UserCredentialsFormField({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.onFieldsSubmitted,
|
required this.onFieldsSubmitted,
|
||||||
|
this.initialUsername,
|
||||||
|
this.initialPassword,
|
||||||
|
required this.formKey,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,6 +36,10 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormBuilderField<LoginFormCredentials?>(
|
return FormBuilderField<LoginFormCredentials?>(
|
||||||
|
initialValue: LoginFormCredentials(
|
||||||
|
password: widget.initialPassword,
|
||||||
|
username: widget.initialUsername,
|
||||||
|
),
|
||||||
name: UserCredentialsFormField.fkCredentials,
|
name: UserCredentialsFormField.fkCredentials,
|
||||||
builder: (field) => AutofillGroup(
|
builder: (field) => AutofillGroup(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -50,6 +62,17 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
|||||||
if (value?.trim().isEmpty ?? true) {
|
if (value?.trim().isEmpty ?? true) {
|
||||||
return S.of(context)!.usernameMustNotBeEmpty;
|
return S.of(context)!.usernameMustNotBeEmpty;
|
||||||
}
|
}
|
||||||
|
final serverAddress = widget.formKey.currentState!
|
||||||
|
.getRawValue<String>(
|
||||||
|
ServerAddressFormField.fkServerAddress);
|
||||||
|
if (serverAddress != null) {
|
||||||
|
final userExists = Hive.localUserAccountBox.values
|
||||||
|
.map((e) => e.id)
|
||||||
|
.contains('$value@$serverAddress');
|
||||||
|
if (userExists) {
|
||||||
|
return S.of(context)!.userAlreadyExists;
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
autofillHints: const [AutofillHints.username],
|
autofillHints: const [AutofillHints.username],
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
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/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class ServerConnectionPage extends StatefulWidget {
|
|
||||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
|
||||||
final VoidCallback onContinue;
|
|
||||||
final String titleText;
|
|
||||||
|
|
||||||
const ServerConnectionPage({
|
|
||||||
super.key,
|
|
||||||
required this.formBuilderKey,
|
|
||||||
required this.onContinue,
|
|
||||||
required this.titleText,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ServerConnectionPage> createState() => _ServerConnectionPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|
||||||
bool _isCheckingConnection = false;
|
|
||||||
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
toolbarHeight: kToolbarHeight - 4,
|
|
||||||
title: Text(widget.titleText),
|
|
||||||
bottom: PreferredSize(
|
|
||||||
child: _isCheckingConnection
|
|
||||||
? const LinearProgressIndicator()
|
|
||||||
: const SizedBox(height: 4.0),
|
|
||||||
preferredSize: const Size.fromHeight(4.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
resizeToAvoidBottomInset: true,
|
|
||||||
body: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ServerAddressFormField(
|
|
||||||
onSubmit: (address) {
|
|
||||||
_updateReachability(address);
|
|
||||||
},
|
|
||||||
).padded(),
|
|
||||||
ClientCertificateFormField(
|
|
||||||
onChanged: (_) => _updateReachability(),
|
|
||||||
).padded(),
|
|
||||||
_buildStatusIndicator(),
|
|
||||||
],
|
|
||||||
).padded(),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: BottomAppBar(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
child: Text(S.of(context)!.testConnection),
|
|
||||||
onPressed: _updateReachability,
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
child: Text(S.of(context)!.continueLabel),
|
|
||||||
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
|
|
||||||
? widget.onContinue
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateReachability([String? address]) async {
|
|
||||||
setState(() {
|
|
||||||
_isCheckingConnection = true;
|
|
||||||
});
|
|
||||||
final certForm = widget.formBuilderKey.currentState
|
|
||||||
?.getRawValue(ClientCertificateFormField.fkClientCertificate)
|
|
||||||
as ClientCertificateFormModel?;
|
|
||||||
final status = await context
|
|
||||||
.read<ConnectivityStatusService>()
|
|
||||||
.isPaperlessServerReachable(
|
|
||||||
address ??
|
|
||||||
widget.formBuilderKey.currentState!
|
|
||||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
|
||||||
certForm != null
|
|
||||||
? ClientCertificate(
|
|
||||||
bytes: certForm.bytes, passphrase: certForm.passphrase)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
_isCheckingConnection = false;
|
|
||||||
_reachabilityStatus = status;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatusIndicator() {
|
|
||||||
if (_isCheckingConnection) {
|
|
||||||
return const ListTile();
|
|
||||||
}
|
|
||||||
Color errorColor = Theme.of(context).colorScheme.error;
|
|
||||||
switch (_reachabilityStatus) {
|
|
||||||
case ReachabilityStatus.unknown:
|
|
||||||
return Container();
|
|
||||||
case ReachabilityStatus.reachable:
|
|
||||||
return _buildIconText(
|
|
||||||
Icons.done,
|
|
||||||
S.of(context)!.connectionSuccessfulylEstablished,
|
|
||||||
Colors.green,
|
|
||||||
);
|
|
||||||
case ReachabilityStatus.notReachable:
|
|
||||||
return _buildIconText(
|
|
||||||
Icons.close,
|
|
||||||
S.of(context)!.couldNotEstablishConnectionToTheServer,
|
|
||||||
errorColor,
|
|
||||||
);
|
|
||||||
case ReachabilityStatus.unknownHost:
|
|
||||||
return _buildIconText(
|
|
||||||
Icons.close,
|
|
||||||
S.of(context)!.hostCouldNotBeResolved,
|
|
||||||
errorColor,
|
|
||||||
);
|
|
||||||
case ReachabilityStatus.missingClientCertificate:
|
|
||||||
return _buildIconText(
|
|
||||||
Icons.close,
|
|
||||||
S.of(context)!.loginPageReachabilityMissingClientCertificateText,
|
|
||||||
errorColor,
|
|
||||||
);
|
|
||||||
case ReachabilityStatus.invalidClientCertificateConfiguration:
|
|
||||||
return _buildIconText(
|
|
||||||
Icons.close,
|
|
||||||
S.of(context)!.incorrectOrMissingCertificatePassphrase,
|
|
||||||
errorColor,
|
|
||||||
);
|
|
||||||
case ReachabilityStatus.connectionTimeout:
|
|
||||||
return _buildIconText(
|
|
||||||
Icons.close,
|
|
||||||
S.of(context)!.connectionTimedOut,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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/form_fields/server_address_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class ServerLoginPage extends StatefulWidget {
|
|
||||||
final String submitText;
|
|
||||||
final Future<void> Function() onSubmit;
|
|
||||||
final GlobalKey<FormBuilderState> formBuilderKey;
|
|
||||||
|
|
||||||
const ServerLoginPage({
|
|
||||||
super.key,
|
|
||||||
required this.onSubmit,
|
|
||||||
required this.formBuilderKey,
|
|
||||||
required this.submitText,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ServerLoginPage> createState() => _ServerLoginPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerLoginPageState extends State<ServerLoginPage> {
|
|
||||||
bool _isLoginLoading = false;
|
|
||||||
@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(S.of(context)!.loginPageSignInTitle),
|
|
||||||
bottom: _isLoginLoading
|
|
||||||
? const PreferredSize(
|
|
||||||
preferredSize: Size.fromHeight(4.0),
|
|
||||||
child: LinearProgressIndicator(),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
S.of(context)!.signInToServer(serverAddress) + ":",
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
).padded(16),
|
|
||||||
UserCredentialsFormField(
|
|
||||||
onFieldsSubmitted: widget.onSubmit,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
S.of(context)!.loginRequiredPermissionsHint,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.apply(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onBackground
|
|
||||||
.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
).padded(16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
bottomNavigationBar: BottomAppBar(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
FilledButton(
|
|
||||||
onPressed: !_isLoginLoading
|
|
||||||
? () async {
|
|
||||||
setState(() => _isLoginLoading = true);
|
|
||||||
try {
|
|
||||||
await widget.onSubmit();
|
|
||||||
} finally {
|
|
||||||
setState(() => _isLoginLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Text(S.of(context)!.signIn),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
lib/features/login/view/widgets/login_transition_page.dart
Normal file
34
lib/features/login/view/widgets/login_transition_page.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/theme.dart';
|
||||||
|
|
||||||
|
class LoginTransitionPage extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
const LoginTransitionPage({super.key, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async => false,
|
||||||
|
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: buildOverlayStyle(
|
||||||
|
Theme.of(context),
|
||||||
|
systemNavigationBarColor: Theme.of(context).colorScheme.background,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Text(text).paddedOnly(bottom: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
class NeverScrollableScrollBehavior extends ScrollBehavior {
|
|
||||||
@override
|
|
||||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
|
||||||
return const NeverScrollableScrollPhysics();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,6 +54,7 @@ class LocalNotificationService {
|
|||||||
required bool finished,
|
required bool finished,
|
||||||
required String locale,
|
required String locale,
|
||||||
required String userId,
|
required String userId,
|
||||||
|
double? progress,
|
||||||
}) async {
|
}) async {
|
||||||
final tr = await S.delegate.load(Locale(locale));
|
final tr = await S.delegate.load(Locale(locale));
|
||||||
|
|
||||||
@@ -68,8 +69,10 @@ class LocalNotificationService {
|
|||||||
android: AndroidNotificationDetails(
|
android: AndroidNotificationDetails(
|
||||||
NotificationChannel.documentDownload.id + "_${document.id}",
|
NotificationChannel.documentDownload.id + "_${document.id}",
|
||||||
NotificationChannel.documentDownload.name,
|
NotificationChannel.documentDownload.name,
|
||||||
|
progress: ((progress ?? 0) * 100).toInt(),
|
||||||
|
maxProgress: 100,
|
||||||
|
indeterminate: progress == null && !finished,
|
||||||
ongoing: !finished,
|
ongoing: !finished,
|
||||||
indeterminate: true,
|
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.high,
|
priority: Priority.high,
|
||||||
showProgress: !finished,
|
showProgress: !finished,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:rxdart/streams.dart';
|
|
||||||
|
|
||||||
import 'paged_documents_state.dart';
|
import 'paged_documents_state.dart';
|
||||||
|
|
||||||
@@ -51,6 +50,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
|||||||
/// Use [loadMore] to load more data.
|
/// Use [loadMore] to load more data.
|
||||||
Future<void> updateFilter({
|
Future<void> updateFilter({
|
||||||
final DocumentFilter filter = const DocumentFilter(),
|
final DocumentFilter filter = const DocumentFilter(),
|
||||||
|
bool emitLoading = true,
|
||||||
}) async {
|
}) async {
|
||||||
final hasConnection =
|
final hasConnection =
|
||||||
await connectivityStatusService.isConnectedToInternet();
|
await connectivityStatusService.isConnectedToInternet();
|
||||||
@@ -60,7 +60,9 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
|||||||
.expand((page) => page.results)
|
.expand((page) => page.results)
|
||||||
.where((doc) => filter.matches(doc))
|
.where((doc) => filter.matches(doc))
|
||||||
.toList();
|
.toList();
|
||||||
|
if (emitLoading) {
|
||||||
emit(state.copyWithPaged(isLoading: true));
|
emit(state.copyWithPaged(isLoading: true));
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWithPaged(
|
state.copyWithPaged(
|
||||||
@@ -79,7 +81,9 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (emitLoading) {
|
||||||
emit(state.copyWithPaged(isLoading: true));
|
emit(state.copyWithPaged(isLoading: true));
|
||||||
|
}
|
||||||
final result = await api.findAll(filter.copyWith(page: 1));
|
final result = await api.findAll(filter.copyWith(page: 1));
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
@@ -146,7 +150,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
|||||||
/// Deletes a document and removes it from the currently loaded state.
|
/// Deletes a document and removes it from the currently loaded state.
|
||||||
///
|
///
|
||||||
Future<void> delete(DocumentModel document) async {
|
Future<void> delete(DocumentModel document) async {
|
||||||
emit(state.copyWithPaged(isLoading: true));
|
// emit(state.copyWithPaged(isLoading: true));
|
||||||
try {
|
try {
|
||||||
await api.delete(document);
|
await api.delete(document);
|
||||||
notifier.notifyDeleted(document);
|
notifier.notifyDeleted(document);
|
||||||
@@ -213,6 +217,7 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
notifier.removeListener(this);
|
notifier.removeListener(this);
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view/view/saved_view_loading_sliver_list.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class SavedViewList extends StatelessWidget {
|
|
||||||
const SavedViewList({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectivity) {
|
|
||||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return state.when(
|
|
||||||
initial: () => const SavedViewLoadingSliverList(),
|
|
||||||
loading: () => const SavedViewLoadingSliverList(),
|
|
||||||
loaded: (savedViews) {
|
|
||||||
if (savedViews.isEmpty) {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: HintCard(
|
|
||||||
hintText: S
|
|
||||||
.of(context)!
|
|
||||||
.createViewsToQuicklyFilterYourDocuments,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return SliverList.builder(
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final view = savedViews.values.elementAt(index);
|
|
||||||
return ListTile(
|
|
||||||
enabled: connectivity.isConnected,
|
|
||||||
title: Text(view.name),
|
|
||||||
subtitle: Text(
|
|
||||||
S.of(context)!.nFiltersSet(view.filterRules.length),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
pushSavedViewDetailsRoute(context, savedView: view);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
itemCount: savedViews.length,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: () => const SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
"An error occurred while trying to load the saved views.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
|
||||||
|
|
||||||
class SavedViewLoadingSliverList extends StatelessWidget {
|
|
||||||
const SavedViewLoadingSliverList({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SliverList.builder(
|
|
||||||
itemBuilder: (context, index) => ShimmerPlaceholder(
|
|
||||||
child: ListTile(
|
|
||||||
title: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Container(
|
|
||||||
width: 300,
|
|
||||||
height: 14,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
subtitle: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Container(
|
|
||||||
width: 150,
|
|
||||||
height: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/navigation/push_routes.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.dart';
|
|
||||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
|
||||||
|
|
||||||
class SavedViewDetailsPage extends StatefulWidget {
|
|
||||||
final Future<void> Function(SavedView savedView) onDelete;
|
|
||||||
const SavedViewDetailsPage({
|
|
||||||
super.key,
|
|
||||||
required this.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SavedViewDetailsPage> createState() => _SavedViewDetailsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SavedViewDetailsPageState extends State<SavedViewDetailsPage>
|
|
||||||
with DocumentPagingViewMixin<SavedViewDetailsPage, SavedViewDetailsCubit> {
|
|
||||||
@override
|
|
||||||
final pagingScrollController = ScrollController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final cubit = context.watch<SavedViewDetailsCubit>();
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(cubit.savedView.name),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () async {
|
|
||||||
final shouldDelete = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ConfirmDeleteSavedViewDialog(
|
|
||||||
view: cubit.savedView,
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
false;
|
|
||||||
if (shouldDelete) {
|
|
||||||
await widget.onDelete(cubit.savedView);
|
|
||||||
context.pop(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return ViewTypeSelectionWidget(
|
|
||||||
viewType: state.viewType,
|
|
||||||
onChanged: cubit.setViewType,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: BlocBuilder<SavedViewDetailsCubit, SavedViewDetailsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
|
||||||
return DocumentsEmptyState(state: state);
|
|
||||||
}
|
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectivity) {
|
|
||||||
return CustomScrollView(
|
|
||||||
controller: pagingScrollController,
|
|
||||||
slivers: [
|
|
||||||
SliverAdaptiveDocumentsView(
|
|
||||||
documents: state.documents,
|
|
||||||
hasInternetConnection: connectivity.isConnected,
|
|
||||||
isLabelClickable: false,
|
|
||||||
isLoading: state.isLoading,
|
|
||||||
hasLoaded: state.hasLoaded,
|
|
||||||
onTap: (document) {
|
|
||||||
DocumentDetailsRoute(
|
|
||||||
$extra: document,
|
|
||||||
isLabelClickable: false,
|
|
||||||
).push(context);
|
|
||||||
},
|
|
||||||
viewType: state.viewType,
|
|
||||||
),
|
|
||||||
if (state.hasLoaded && state.isLoading)
|
|
||||||
const SliverToBoxAdapter(
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
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:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/adapters.dart';
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/view/add_account_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.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/routes/typed/top_level/add_account_route.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class ManageAccountsPage extends StatelessWidget {
|
class ManageAccountsPage extends StatelessWidget {
|
||||||
@@ -20,16 +17,15 @@ class ManageAccountsPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GlobalSettingsBuilder(
|
return GlobalSettingsBuilder(
|
||||||
builder: (context, globalSettings) {
|
builder: (context, globalSettings) {
|
||||||
// This is one of the few places where the currentLoggedInUser can be null
|
// // This is one of the few places where the currentLoggedInUser can be null
|
||||||
// (exactly after loggin out as the current user to be precise).
|
// // (exactly after loggin out as the current user to be precise).
|
||||||
|
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: Hive.localUserAccountBox.listenable(),
|
||||||
|
builder: (context, box, _) {
|
||||||
if (globalSettings.loggedInUserId == null) {
|
if (globalSettings.loggedInUserId == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return ValueListenableBuilder(
|
|
||||||
valueListenable:
|
|
||||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
|
|
||||||
.listenable(),
|
|
||||||
builder: (context, box, _) {
|
|
||||||
final userIds = box.keys.toList().cast<String>();
|
final userIds = box.keys.toList().cast<String>();
|
||||||
final otherAccounts = userIds
|
final otherAccounts = userIds
|
||||||
.whereNot((element) => element == globalSettings.loggedInUserId)
|
.whereNot((element) => element == globalSettings.loggedInUserId)
|
||||||
@@ -70,6 +66,7 @@ class ManageAccountsPage extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
onSelected: (value) async {
|
onSelected: (value) async {
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
await context
|
await context
|
||||||
.read<AuthenticationCubit>()
|
.read<AuthenticationCubit>()
|
||||||
.logout(true);
|
.logout(true);
|
||||||
@@ -133,11 +130,12 @@ class ManageAccountsPage extends StatelessWidget {
|
|||||||
_onAddAccount(context, globalSettings.loggedInUserId!);
|
_onAddAccount(context, globalSettings.loggedInUserId!);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
|
//TODO: Implement permission/user settings at some point...
|
||||||
ListTile(
|
// if (context.watch<LocalUserAccount>().hasMultiUserSupport)
|
||||||
leading: const Icon(Icons.admin_panel_settings),
|
// ListTile(
|
||||||
title: Text(S.of(context)!.managePermissions),
|
// leading: const Icon(Icons.admin_panel_settings),
|
||||||
),
|
// title: Text(S.of(context)!.managePermissions),
|
||||||
|
// ),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -147,43 +145,43 @@ class ManageAccountsPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAddAccount(BuildContext context, String currentUser) async {
|
Future<void> _onAddAccount(BuildContext context, String currentUser) async {
|
||||||
final userId = await Navigator.push(
|
Navigator.of(context).pop();
|
||||||
context,
|
AddAccountRoute().push<String>(context);
|
||||||
MaterialPageRoute(
|
// final userId = await Navigator.push(
|
||||||
builder: (context) => AddAccountPage(
|
// context,
|
||||||
titleString: S.of(context)!.addAccount,
|
// MaterialPageRoute(
|
||||||
onSubmit: (context, username, password, serverUrl,
|
// builder: (context) => AddAccountPage(
|
||||||
clientCertificate) async {
|
// titleText: S.of(context)!.addAccount,
|
||||||
final userId = await context.read<AuthenticationCubit>().addAccount(
|
// onSubmit: (context, username, password, serverUrl,
|
||||||
credentials: LoginFormCredentials(
|
// clientCertificate) async {
|
||||||
username: username,
|
// try {
|
||||||
password: password,
|
// final userId =
|
||||||
),
|
// await context.read<AuthenticationCubit>().addAccount(
|
||||||
clientCertificate: clientCertificate,
|
// credentials: LoginFormCredentials(
|
||||||
serverUrl: serverUrl,
|
// username: username,
|
||||||
//TODO: Ask user whether to enable biometric authentication
|
// password: password,
|
||||||
enableBiometricAuthentication: false,
|
// ),
|
||||||
);
|
// clientCertificate: clientCertificate,
|
||||||
Navigator.of(context).pop<String?>(userId);
|
// serverUrl: serverUrl,
|
||||||
},
|
// //TODO: Ask user whether to enable biometric authentication
|
||||||
submitText: S.of(context)!.addAccount,
|
// enableBiometricAuthentication: false,
|
||||||
),
|
// );
|
||||||
),
|
|
||||||
);
|
// Navigator.of(context).pop<String?>(userId);
|
||||||
if (userId != null) {
|
// } on PaperlessFormValidationException catch (error) {}
|
||||||
final shoudSwitch = await showDialog<bool>(
|
// },
|
||||||
context: context,
|
// submitText: S.of(context)!.addAccount,
|
||||||
builder: (context) => const SwitchAccountDialog(),
|
// ),
|
||||||
) ??
|
// ),
|
||||||
false;
|
// );
|
||||||
if (shoudSwitch) {
|
|
||||||
_onSwitchAccount(context, currentUser, userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSwitchAccount(
|
void _onSwitchAccount(
|
||||||
BuildContext context, String currentUser, String newUser) async {
|
BuildContext context,
|
||||||
|
String currentUser,
|
||||||
|
String newUser,
|
||||||
|
) async {
|
||||||
if (currentUser == newUser) return;
|
if (currentUser == newUser) return;
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class SwitchingAccountsPage extends StatelessWidget {
|
|
||||||
const SwitchingAccountsPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: () async => false,
|
|
||||||
child: Material(
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
S.of(context)!.switchingAccountsPleaseWait,
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
'tr': LanguageOption('Türkçe', true),
|
'tr': LanguageOption('Türkçe', true),
|
||||||
'pl': LanguageOption('Polska', true),
|
'pl': LanguageOption('Polska', true),
|
||||||
'ca': LanguageOption('Catalan', true),
|
'ca': LanguageOption('Catalan', true),
|
||||||
|
'ru': LanguageOption('Русский', true),
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -34,9 +35,9 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
onTap: () => showDialog<String>(
|
onTap: () => showDialog<String>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => RadioSettingsDialog<String>(
|
builder: (_) => RadioSettingsDialog<String>(
|
||||||
footer: const Text(
|
// footer: const Text(
|
||||||
"* Not fully translated yet. Some words may be displayed in English!",
|
// "* Not fully translated yet. Some words may be displayed in English!",
|
||||||
),
|
// ),
|
||||||
titleText: S.of(context)!.language,
|
titleText: S.of(context)!.language,
|
||||||
options: [
|
options: [
|
||||||
for (var language in _languageOptions.entries)
|
for (var language in _languageOptions.entries)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SkipDocumentPreprationOnShareSetting extends StatelessWidget {
|
class SkipDocumentPreprationOnShareSetting extends StatelessWidget {
|
||||||
const SkipDocumentPreprationOnShareSetting({super.key});
|
const SkipDocumentPreprationOnShareSetting({super.key});
|
||||||
@@ -9,9 +10,8 @@ class SkipDocumentPreprationOnShareSetting extends StatelessWidget {
|
|||||||
return GlobalSettingsBuilder(
|
return GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return SwitchListTile(
|
return SwitchListTile(
|
||||||
title: Text("Direct share"),
|
title: Text(S.of(context)!.skipEditingReceivedFiles),
|
||||||
subtitle:
|
subtitle: Text(S.of(context)!.uploadWithoutPromptingUploadForm),
|
||||||
Text("Always directly upload when sharing files with the app."),
|
|
||||||
value: settings.skipDocumentPreprarationOnUpload,
|
value: settings.skipDocumentPreprarationOnUpload,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
settings.skipDocumentPreprarationOnUpload = value;
|
settings.skipDocumentPreprarationOnUpload = value;
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|
||||||
import 'package:paperless_mobile/core/global/constants.dart';
|
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
|
||||||
import 'package:paperless_mobile/features/sharing/model/share_intent_queue.dart';
|
|
||||||
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
class UploadQueueProcessor {
|
|
||||||
final ShareIntentQueue queue;
|
|
||||||
|
|
||||||
UploadQueueProcessor({required this.queue});
|
|
||||||
|
|
||||||
bool _isFileTypeSupported(File file) {
|
|
||||||
final isSupported =
|
|
||||||
supportedFileExtensions.contains(p.extension(file.path));
|
|
||||||
return isSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
void processIncomingFiles(
|
|
||||||
BuildContext context, {
|
|
||||||
required List<SharedMediaFile> sharedFiles,
|
|
||||||
}) async {
|
|
||||||
if (sharedFiles.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Iterable<File> files = sharedFiles.map((file) => File(file.path));
|
|
||||||
if (Platform.isIOS) {
|
|
||||||
files = files
|
|
||||||
.map((file) => File(file.path.replaceAll('file://', '')))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
final supportedFiles = files.where(_isFileTypeSupported);
|
|
||||||
final unsupportedFiles = files.whereNot(_isFileTypeSupported);
|
|
||||||
debugPrint(
|
|
||||||
"Received ${files.length} files, out of which ${supportedFiles.length} are supported.}");
|
|
||||||
if (supportedFiles.isEmpty) {
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg: translateError(
|
|
||||||
context,
|
|
||||||
ErrorCode.unsupportedFileFormat,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
// As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines.
|
|
||||||
await SystemNavigator.pop();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (unsupportedFiles.isNotEmpty) {
|
|
||||||
//TODO: INTL
|
|
||||||
Fluttertoast.showToast(
|
|
||||||
msg:
|
|
||||||
"${unsupportedFiles.length}/${files.length} files could not be processed.");
|
|
||||||
}
|
|
||||||
await ShareIntentQueue.instance.addAll(
|
|
||||||
supportedFiles,
|
|
||||||
userId: context.read<LocalUserAccount>().id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:hive/hive.dart';
|
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
|
|
||||||
class ShareIntentQueue extends ChangeNotifier {
|
|
||||||
final Map<String, Queue<File>> _queues = {};
|
|
||||||
|
|
||||||
ShareIntentQueue._();
|
|
||||||
|
|
||||||
static final instance = ShareIntentQueue._();
|
|
||||||
|
|
||||||
Future<void> initialize() async {
|
|
||||||
final users = Hive.localUserAccountBox.values;
|
|
||||||
for (final user in users) {
|
|
||||||
final userId = user.id;
|
|
||||||
debugPrint("Locating remaining files to be uploaded for $userId...");
|
|
||||||
final consumptionDir =
|
|
||||||
await FileService.getConsumptionDirectory(userId: userId);
|
|
||||||
final files = await FileService.getAllFiles(consumptionDir);
|
|
||||||
debugPrint(
|
|
||||||
"Found ${files.length} files to be uploaded for $userId. Adding to queue...");
|
|
||||||
getQueue(userId).addAll(files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(
|
|
||||||
File file, {
|
|
||||||
required String userId,
|
|
||||||
}) =>
|
|
||||||
addAll([file], userId: userId);
|
|
||||||
|
|
||||||
Future<void> addAll(
|
|
||||||
Iterable<File> files, {
|
|
||||||
required String userId,
|
|
||||||
}) async {
|
|
||||||
if (files.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final consumptionDirectory =
|
|
||||||
await FileService.getConsumptionDirectory(userId: userId);
|
|
||||||
final copiedFiles = await Future.wait([
|
|
||||||
for (var file in files)
|
|
||||||
file.copy('${consumptionDirectory.path}/${p.basename(file.path)}')
|
|
||||||
]);
|
|
||||||
|
|
||||||
debugPrint(
|
|
||||||
"Adding received files to queue: ${files.map((e) => e.path).join(",")}",
|
|
||||||
);
|
|
||||||
getQueue(userId).addAll(copiedFiles);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes and returns the first item in the requested user's queue if it exists.
|
|
||||||
File? pop(String userId) {
|
|
||||||
if (hasUnhandledFiles(userId: userId)) {
|
|
||||||
final file = getQueue(userId).removeFirst();
|
|
||||||
notifyListeners();
|
|
||||||
return file;
|
|
||||||
// Don't notify listeners, only when new item is added.
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> onConsumed(File file) {
|
|
||||||
debugPrint(
|
|
||||||
"File ${file.path} successfully consumed. Delelting local copy.");
|
|
||||||
return file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> discard(File file) {
|
|
||||||
debugPrint("Discarding file ${file.path}.");
|
|
||||||
return file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the queue of the requested user contains files waiting for processing.
|
|
||||||
bool hasUnhandledFiles({
|
|
||||||
required String userId,
|
|
||||||
}) =>
|
|
||||||
getQueue(userId).isNotEmpty;
|
|
||||||
|
|
||||||
int unhandledFileCount({
|
|
||||||
required String userId,
|
|
||||||
}) =>
|
|
||||||
getQueue(userId).length;
|
|
||||||
|
|
||||||
Queue<File> getQueue(String userId) {
|
|
||||||
if (!_queues.containsKey(userId)) {
|
|
||||||
_queues[userId] = Queue<File>();
|
|
||||||
}
|
|
||||||
return _queues[userId]!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserAwareShareMediaFile {
|
|
||||||
final String userId;
|
|
||||||
final SharedMediaFile sharedFile;
|
|
||||||
|
|
||||||
UserAwareShareMediaFile(this.userId, this.sharedFile);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
|
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/view/widgets/upload_queue_shell.dart';
|
import 'package:paperless_mobile/features/sharing/view/widgets/event_listener_shell.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||||
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
|
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/view/dialog/pending_files_info_dialog.dart';
|
|
||||||
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.dart';
|
import 'package:paperless_mobile/features/tasks/model/pending_tasks_notifier.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 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
@@ -23,25 +26,33 @@ import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
|||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
|
||||||
class UploadQueueShell extends StatefulWidget {
|
class EventListenerShell extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const UploadQueueShell({super.key, required this.child});
|
const EventListenerShell({super.key, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<UploadQueueShell> createState() => _UploadQueueShellState();
|
State<EventListenerShell> createState() => _EventListenerShellState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UploadQueueShellState extends State<UploadQueueShell> {
|
class _EventListenerShellState extends State<EventListenerShell>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
StreamSubscription? _subscription;
|
StreamSubscription? _subscription;
|
||||||
|
StreamSubscription? _documentDeletedSubscription;
|
||||||
|
Timer? _inboxTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
ReceiveSharingIntent.getInitialMedia().then(_onReceiveSharedFiles);
|
ReceiveSharingIntent.getInitialMedia().then(_onReceiveSharedFiles);
|
||||||
_subscription =
|
_subscription =
|
||||||
ReceiveSharingIntent.getMediaStream().listen(_onReceiveSharedFiles);
|
ReceiveSharingIntent.getMediaStream().listen(_onReceiveSharedFiles);
|
||||||
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
|
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
|
||||||
|
_documentDeletedSubscription =
|
||||||
|
context.read<DocumentChangedNotifier>().$deleted.listen((event) {
|
||||||
|
showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
|
||||||
|
});
|
||||||
|
_listenToInboxChanges();
|
||||||
// WidgetsBinding.instance.addPostFrameCallback((_) async {
|
// WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
// final notifier = context.read<ConsumptionChangeNotifier>();
|
// final notifier = context.read<ConsumptionChangeNotifier>();
|
||||||
// await notifier.isInitialized;
|
// await notifier.isInitialized;
|
||||||
@@ -67,6 +78,52 @@ class _UploadQueueShellState extends State<UploadQueueShell> {
|
|||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _listenToInboxChanges() {
|
||||||
|
final cubit = context.read<InboxCubit>();
|
||||||
|
final currentUser = context.read<LocalUserAccount>();
|
||||||
|
if (!currentUser.paperlessUser.canViewInbox || _inboxTimer != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cubit.refreshItemsInInboxCount(false);
|
||||||
|
_inboxTimer = Timer.periodic(30.seconds, (_) {
|
||||||
|
cubit.refreshItemsInInboxCount(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_subscription?.cancel();
|
||||||
|
_documentDeletedSubscription?.cancel();
|
||||||
|
_inboxTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
debugPrint(
|
||||||
|
"App resumed, reloading connectivity and "
|
||||||
|
"restarting periodic query for inbox changes...",
|
||||||
|
);
|
||||||
|
context.read<ConnectivityCubit>().reload();
|
||||||
|
_listenToInboxChanges();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.inactive:
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
case AppLifecycleState.detached:
|
||||||
|
default:
|
||||||
|
_inboxTimer?.cancel();
|
||||||
|
_inboxTimer = null;
|
||||||
|
debugPrint(
|
||||||
|
"App either paused or hidden, stopping "
|
||||||
|
"periodic query for inbox changes.",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onTasksChanged() {
|
void _onTasksChanged() {
|
||||||
final taskNotifier = context.read<PendingTasksNotifier>();
|
final taskNotifier = context.read<PendingTasksNotifier>();
|
||||||
final userId = context.read<LocalUserAccount>().id;
|
final userId = context.read<LocalUserAccount>().id;
|
||||||
@@ -96,12 +153,6 @@ class _UploadQueueShellState extends State<UploadQueueShell> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_subscription?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return widget.child;
|
return widget.child;
|
||||||
@@ -167,9 +218,9 @@ Future<void> consumeLocalFile(
|
|||||||
);
|
);
|
||||||
await consumptionNotifier.discardFile(file, userId: userId);
|
await consumptionNotifier.discardFile(file, userId: userId);
|
||||||
|
|
||||||
if (result.taskId != null) {
|
// if (result.taskId != null) {
|
||||||
taskNotifier.listenToTaskChanges(result.taskId!);
|
// taskNotifier.listenToTaskChanges(result.taskId!);
|
||||||
}
|
// }
|
||||||
if (exitAppAfterConsumed) {
|
if (exitAppAfterConsumed) {
|
||||||
SystemNavigator.pop();
|
SystemNavigator.pop();
|
||||||
}
|
}
|
||||||
@@ -52,10 +52,10 @@ class PendingTasksNotifier extends ValueNotifier<Map<String, Task>> {
|
|||||||
_subscriptions[taskId]?.cancel();
|
_subscriptions[taskId]?.cancel();
|
||||||
_subscriptions.remove(taskId);
|
_subscriptions.remove(taskId);
|
||||||
} else {
|
} else {
|
||||||
_subscriptions.forEach((key, value) {
|
for (var sub in _subscriptions.values) {
|
||||||
value.cancel();
|
sub.cancel();
|
||||||
_subscriptions.remove(key);
|
}
|
||||||
});
|
_subscriptions.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
String extractFilenameFromPath(String path) {
|
|
||||||
return path.split(RegExp('[./]')).reversed.skip(1).first;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Taken from https://github.com/flutter/flutter/issues/26127#issuecomment-782083060
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
Future<void> loadImage(ImageProvider provider) {
|
|
||||||
final config = ImageConfiguration(
|
|
||||||
bundle: rootBundle,
|
|
||||||
devicePixelRatio: window.devicePixelRatio,
|
|
||||||
platform: defaultTargetPlatform,
|
|
||||||
);
|
|
||||||
final Completer<void> completer = Completer();
|
|
||||||
final ImageStream stream = provider.resolve(config);
|
|
||||||
|
|
||||||
late final ImageStreamListener listener;
|
|
||||||
|
|
||||||
listener = ImageStreamListener((ImageInfo image, bool sync) {
|
|
||||||
debugPrint("Image ${image.debugLabel} finished loading");
|
|
||||||
completer.complete();
|
|
||||||
stream.removeListener(listener);
|
|
||||||
}, onError: (dynamic exception, StackTrace? stackTrace) {
|
|
||||||
completer.complete();
|
|
||||||
stream.removeListener(listener);
|
|
||||||
FlutterError.reportError(FlutterErrorDetails(
|
|
||||||
context: ErrorDescription('image failed to load'),
|
|
||||||
library: 'image resource service',
|
|
||||||
exception: exception,
|
|
||||||
stack: stackTrace,
|
|
||||||
silent: true,
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.addListener(listener);
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Erneut versuchen",
|
"tryAgain": "Erneut versuchen",
|
||||||
"discardFile": "Datei verwerfen?",
|
"discardFile": "Datei verwerfen?",
|
||||||
"discard": "Verwerfen"
|
"discard": "Verwerfen",
|
||||||
|
"backToLogin": "Zurück zur Anmeldung",
|
||||||
|
"skipEditingReceivedFiles": "Bearbeitung von empfangenen Dateien überspringen",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Mit der App geteilte Dateien immer direkt hochladen, ohne das Upload-Formular anzuzeigen.",
|
||||||
|
"authenticatingDots": "Authentifizieren...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Nutzerinformationen werden gespeichert...",
|
||||||
|
"fetchingUserInformation": "Benutzerinformationen werden abgerufen...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Sitzung wird wiederhergestellt...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -980,5 +980,21 @@
|
|||||||
},
|
},
|
||||||
"tryAgain": "Try again",
|
"tryAgain": "Try again",
|
||||||
"discardFile": "Discard file?",
|
"discardFile": "Discard file?",
|
||||||
"discard": "Discard"
|
"discard": "Discard",
|
||||||
|
"backToLogin": "Back to login",
|
||||||
|
"skipEditingReceivedFiles": "Skip editing received files",
|
||||||
|
"uploadWithoutPromptingUploadForm": "Always upload without prompting the upload form when sharing files with the app.",
|
||||||
|
"authenticatingDots": "Authenticating...",
|
||||||
|
"@authenticatingDots": {
|
||||||
|
"description": "Message shown when the app is authenticating the user"
|
||||||
|
},
|
||||||
|
"persistingUserInformation": "Persisting user information...",
|
||||||
|
"fetchingUserInformation": "Fetching user information...",
|
||||||
|
"@fetchingUserInformation": {
|
||||||
|
"description": "Message shown when the app loads user data from the server"
|
||||||
|
},
|
||||||
|
"restoringSession": "Restoring session...",
|
||||||
|
"@restoringSession": {
|
||||||
|
"description": "Message shown when the user opens the app and the previous user is tried to be authenticated and logged in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
119
lib/main.dart
119
lib/main.dart
@@ -15,7 +15,6 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
|||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:intl/intl_standalone.dart';
|
import 'package:intl/intl_standalone.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:mock_server/mock_server.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
@@ -28,7 +27,6 @@ import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
|||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
@@ -36,11 +34,8 @@ import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'
|
|||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/model/share_intent_queue.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 'package:paperless_mobile/routes/navigation_keys.dart';
|
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||||
import 'package:paperless_mobile/routes/routes.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||||
@@ -50,12 +45,10 @@ import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
|||||||
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/shells/provider_shell_route.dart';
|
import 'package:paperless_mobile/routes/typed/shells/provider_shell_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/shells/scaffold_shell_route.dart';
|
import 'package:paperless_mobile/routes/typed/shells/scaffold_shell_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/checking_login_route.dart';
|
import 'package:paperless_mobile/routes/typed/top_level/add_account_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/logging_out_route.dart';
|
import 'package:paperless_mobile/routes/typed/top_level/logging_out_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/switching_accounts_route.dart';
|
|
||||||
import 'package:paperless_mobile/routes/typed/top_level/verify_identity_route.dart';
|
|
||||||
import 'package:paperless_mobile/theme.dart';
|
import 'package:paperless_mobile/theme.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -157,9 +150,8 @@ void main() async {
|
|||||||
apiFactory,
|
apiFactory,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
connectivityStatusService,
|
connectivityStatusService,
|
||||||
|
localNotificationService,
|
||||||
);
|
);
|
||||||
await authenticationCubit.restoreSessionState();
|
|
||||||
await ShareIntentQueue.instance.initialize();
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -208,11 +200,14 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
FlutterNativeSplash.remove();
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
_setOptimalDisplayMode();
|
_setOptimalDisplayMode();
|
||||||
}
|
}
|
||||||
initializeDateFormatting();
|
initializeDateFormatting();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
context.read<AuthenticationCubit>().restoreSession();
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Activates the highest supported refresh rate on the device.
|
/// Activates the highest supported refresh rate on the device.
|
||||||
@@ -236,20 +231,71 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
debugLogDiagnostics: kDebugMode,
|
debugLogDiagnostics: kDebugMode,
|
||||||
initialLocation: "/login",
|
initialLocation: "/login",
|
||||||
routes: [
|
routes: [
|
||||||
$loginRoute,
|
|
||||||
$verifyIdentityRoute,
|
|
||||||
$switchingAccountsRoute,
|
|
||||||
$logginOutRoute,
|
|
||||||
$checkingLoginRoute,
|
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
|
pageBuilder: (context, state, child) {
|
||||||
|
return MaterialPage(
|
||||||
|
child: BlocListener<AuthenticationCubit, AuthenticationState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
switch (state) {
|
||||||
|
case UnauthenticatedState(
|
||||||
|
redirectToAccountSelection: var shouldRedirect
|
||||||
|
):
|
||||||
|
if (shouldRedirect) {
|
||||||
|
const LoginToExistingAccountRoute().go(context);
|
||||||
|
} else {
|
||||||
|
const LoginRoute().go(context);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case RestoringSessionState():
|
||||||
|
const RestoringSessionRoute().go(context);
|
||||||
|
break;
|
||||||
|
case VerifyIdentityState(userId: var userId):
|
||||||
|
VerifyIdentityRoute(userId: userId).go(context);
|
||||||
|
break;
|
||||||
|
case SwitchingAccountsState():
|
||||||
|
const SwitchingAccountsRoute().push(context);
|
||||||
|
break;
|
||||||
|
case AuthenticatedState():
|
||||||
|
const LandingRoute().go(context);
|
||||||
|
break;
|
||||||
|
case AuthenticatingState state:
|
||||||
|
AuthenticatingRoute(state.currentStage.name).push(context);
|
||||||
|
break;
|
||||||
|
case LoggingOutState():
|
||||||
|
const LoggingOutRoute().go(context);
|
||||||
|
break;
|
||||||
|
case AuthenticationErrorState():
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
// LoginRoute(
|
||||||
|
// $extra: errorState.clientCertificate,
|
||||||
|
// password: errorState.password,
|
||||||
|
// serverUrl: errorState.serverUrl,
|
||||||
|
// username: errorState.username,
|
||||||
|
// ).go(context);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
navigatorKey: rootNavigatorKey,
|
navigatorKey: rootNavigatorKey,
|
||||||
|
routes: [
|
||||||
|
$loginRoute,
|
||||||
|
$loggingOutRoute,
|
||||||
|
$addAccountRoute,
|
||||||
|
ShellRoute(
|
||||||
|
navigatorKey: outerShellNavigatorKey,
|
||||||
builder: ProviderShellRoute(widget.apiFactory).build,
|
builder: ProviderShellRoute(widget.apiFactory).build,
|
||||||
routes: [
|
routes: [
|
||||||
$settingsRoute,
|
$settingsRoute,
|
||||||
$savedViewsRoute,
|
$savedViewsRoute,
|
||||||
$uploadQueueRoute,
|
$uploadQueueRoute,
|
||||||
StatefulShellRoute(
|
StatefulShellRoute(
|
||||||
navigatorContainerBuilder: (context, navigationShell, children) {
|
navigatorContainerBuilder:
|
||||||
|
(context, navigationShell, children) {
|
||||||
return children[navigationShell.currentIndex];
|
return children[navigationShell.currentIndex];
|
||||||
},
|
},
|
||||||
builder: const ScaffoldShellRoute().builder,
|
builder: const ScaffoldShellRoute().builder,
|
||||||
@@ -279,45 +325,13 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<AuthenticationCubit, AuthenticationState>(
|
return GlobalSettingsBuilder(
|
||||||
listener: (context, state) {
|
|
||||||
switch (state) {
|
|
||||||
case UnauthenticatedState():
|
|
||||||
_router.goNamed(R.login);
|
|
||||||
break;
|
|
||||||
case RequiresLocalAuthenticationState():
|
|
||||||
_router.goNamed(R.verifyIdentity);
|
|
||||||
break;
|
|
||||||
case SwitchingAccountsState():
|
|
||||||
final userId = context.read<LocalUserAccount>().id;
|
|
||||||
context
|
|
||||||
.read<LocalNotificationService>()
|
|
||||||
.cancelUserNotifications(userId);
|
|
||||||
_router.goNamed(R.switchingAccounts);
|
|
||||||
break;
|
|
||||||
case AuthenticatedState():
|
|
||||||
_router.goNamed(R.landing);
|
|
||||||
break;
|
|
||||||
case CheckingLoginState():
|
|
||||||
_router.goNamed(R.checkingLogin);
|
|
||||||
break;
|
|
||||||
case LogginOutState():
|
|
||||||
final userId = context.read<LocalUserAccount>().id;
|
|
||||||
context
|
|
||||||
.read<LocalNotificationService>()
|
|
||||||
.cancelUserNotifications(userId);
|
|
||||||
_router.goNamed(R.loggingOut);
|
|
||||||
break;
|
|
||||||
case AuthenticationErrorState():
|
|
||||||
_router.goNamed(R.login);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: GlobalSettingsBuilder(
|
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
@@ -345,7 +359,6 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
final outerShellNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final landingNavigatorKey = GlobalKey<NavigatorState>();
|
final landingNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final documentsNavigatorKey = GlobalKey<NavigatorState>();
|
final documentsNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final scannerNavigatorKey = GlobalKey<NavigatorState>();
|
final scannerNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ class R {
|
|||||||
const R._();
|
const R._();
|
||||||
static const landing = "landing";
|
static const landing = "landing";
|
||||||
static const login = "login";
|
static const login = "login";
|
||||||
|
static const loginToExistingAccount = 'loginToExistingAccount';
|
||||||
static const documents = "documents";
|
static const documents = "documents";
|
||||||
static const verifyIdentity = "verifyIdentity";
|
static const verifyIdentity = "verifyIdentity";
|
||||||
static const switchingAccounts = "switchingAccounts";
|
static const switchingAccount = "switchingAccount";
|
||||||
static const savedView = "savedView";
|
static const savedView = "savedView";
|
||||||
static const createSavedView = "createSavedView";
|
static const createSavedView = "createSavedView";
|
||||||
static const editSavedView = "editSavedView";
|
static const editSavedView = "editSavedView";
|
||||||
@@ -21,6 +22,8 @@ class R {
|
|||||||
static const linkedDocuments = "linkedDocuments";
|
static const linkedDocuments = "linkedDocuments";
|
||||||
static const bulkEditDocuments = "bulkEditDocuments";
|
static const bulkEditDocuments = "bulkEditDocuments";
|
||||||
static const uploadQueue = "uploadQueue";
|
static const uploadQueue = "uploadQueue";
|
||||||
static const checkingLogin = "checkingLogin";
|
static const authenticating = "authenticating";
|
||||||
static const loggingOut = "loggingOut";
|
static const loggingOut = "loggingOut";
|
||||||
|
static const restoringSession = "restoringSession";
|
||||||
|
static const addAccount = 'addAccount';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ class DocumentsRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DocumentDetailsRoute extends GoRouteData {
|
class DocumentDetailsRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
|
|
||||||
final bool isLabelClickable;
|
final bool isLabelClickable;
|
||||||
final DocumentModel $extra;
|
final DocumentModel $extra;
|
||||||
@@ -86,7 +87,8 @@ class DocumentDetailsRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EditDocumentRoute extends GoRouteData {
|
class EditDocumentRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
|
|
||||||
final DocumentModel $extra;
|
final DocumentModel $extra;
|
||||||
|
|
||||||
@@ -114,7 +116,8 @@ class EditDocumentRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DocumentPreviewRoute extends GoRouteData {
|
class DocumentPreviewRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
|
|
||||||
final DocumentModel $extra;
|
final DocumentModel $extra;
|
||||||
final String? title;
|
final String? title;
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ class LabelsRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EditLabelRoute extends GoRouteData {
|
class EditLabelRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
|
|
||||||
final Label $extra;
|
final Label $extra;
|
||||||
|
|
||||||
@@ -67,7 +68,8 @@ class EditLabelRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CreateLabelRoute extends GoRouteData {
|
class CreateLabelRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
final LabelType $extra;
|
final LabelType $extra;
|
||||||
final String? name;
|
final String? name;
|
||||||
|
|
||||||
@@ -88,7 +90,8 @@ class CreateLabelRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LinkedDocumentsRoute extends GoRouteData {
|
class LinkedDocumentsRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
final DocumentFilter $extra;
|
final DocumentFilter $extra;
|
||||||
|
|
||||||
const LinkedDocumentsRoute(this.$extra);
|
const LinkedDocumentsRoute(this.$extra);
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ class ScannerRoute extends GoRouteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DocumentUploadRoute extends GoRouteData {
|
class DocumentUploadRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
final FutureOr<Uint8List> $extra;
|
final FutureOr<Uint8List> $extra;
|
||||||
final String? title;
|
final String? title;
|
||||||
final String? filename;
|
final String? filename;
|
||||||
@@ -72,6 +73,7 @@ class DocumentUploadRoute extends GoRouteData {
|
|||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
),
|
),
|
||||||
child: DocumentUploadPreparationPage(
|
child: DocumentUploadPreparationPage(
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ part 'upload_queue_route.g.dart';
|
|||||||
name: R.uploadQueue,
|
name: R.uploadQueue,
|
||||||
)
|
)
|
||||||
class UploadQueueRoute extends GoRouteData {
|
class UploadQueueRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey =
|
||||||
|
outerShellNavigatorKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
|||||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/home_shell_widget.dart';
|
import 'package:paperless_mobile/features/home/view/home_shell_widget.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/view/widgets/upload_queue_shell.dart';
|
import 'package:paperless_mobile/features/sharing/view/widgets/event_listener_shell.dart';
|
||||||
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ import 'package:provider/provider.dart';
|
|||||||
// )
|
// )
|
||||||
class ProviderShellRoute extends ShellRouteData {
|
class ProviderShellRoute extends ShellRouteData {
|
||||||
final PaperlessApiFactory apiFactory;
|
final PaperlessApiFactory apiFactory;
|
||||||
static final GlobalKey<NavigatorState> $navigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $navigatorKey = outerShellNavigatorKey;
|
||||||
|
|
||||||
const ProviderShellRoute(this.apiFactory);
|
const ProviderShellRoute(this.apiFactory);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class ProviderShellRoute extends ShellRouteData {
|
|||||||
child: ChangeNotifierProvider(
|
child: ChangeNotifierProvider(
|
||||||
create: (context) => ConsumptionChangeNotifier()
|
create: (context) => ConsumptionChangeNotifier()
|
||||||
..loadFromConsumptionDirectory(userId: currentUserId),
|
..loadFromConsumptionDirectory(userId: currentUserId),
|
||||||
child: UploadQueueShell(child: navigator),
|
child: EventListenerShell(child: navigator),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
80
lib/routes/typed/top_level/add_account_route.dart
Normal file
80
lib/routes/typed/top_level/add_account_route.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/view/add_account_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/dialogs/switch_account_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||||
|
import 'package:paperless_mobile/routes/routes.dart';
|
||||||
|
|
||||||
|
part 'add_account_route.g.dart';
|
||||||
|
|
||||||
|
@TypedGoRoute<AddAccountRoute>(
|
||||||
|
path: '/add-account',
|
||||||
|
name: R.addAccount,
|
||||||
|
)
|
||||||
|
class AddAccountRoute extends GoRouteData {
|
||||||
|
const AddAccountRoute();
|
||||||
|
|
||||||
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return AddAccountPage(
|
||||||
|
titleText: S.of(context)!.addAccount,
|
||||||
|
onSubmit:
|
||||||
|
(context, username, password, serverUrl, clientCertificate) async {
|
||||||
|
try {
|
||||||
|
final userId = await context.read<AuthenticationCubit>().addAccount(
|
||||||
|
credentials: LoginFormCredentials(
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
serverUrl: serverUrl,
|
||||||
|
enableBiometricAuthentication: false,
|
||||||
|
locale: Localizations.localeOf(context).languageCode,
|
||||||
|
);
|
||||||
|
final shoudSwitch = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const SwitchAccountDialog(),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (shoudSwitch) {
|
||||||
|
await context.read<AuthenticationCubit>().switchAccount(userId);
|
||||||
|
} else {
|
||||||
|
while (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
// context.pop();
|
||||||
|
} on PaperlessFormValidationException catch (exception, stackTrace) {
|
||||||
|
if (exception.hasUnspecificErrorMessage()) {
|
||||||
|
showLocalizedError(context, exception.unspecificErrorMessage()!);
|
||||||
|
// context.pop();
|
||||||
|
} else {
|
||||||
|
showGenericError(
|
||||||
|
context,
|
||||||
|
exception.validationMessages.values.first,
|
||||||
|
stackTrace,
|
||||||
|
); //TODO: Check if we can show error message directly on field here.
|
||||||
|
}
|
||||||
|
} on InfoMessageException catch (error) {
|
||||||
|
showInfoMessage(context, error);
|
||||||
|
// context.pop();
|
||||||
|
} catch (unknownError, stackTrace) {
|
||||||
|
showGenericError(context, unknownError.toString(), stackTrace);
|
||||||
|
// context.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitText: S.of(context)!.addAccount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:paperless_mobile/routes/routes.dart';
|
|
||||||
|
|
||||||
part 'checking_login_route.g.dart';
|
|
||||||
|
|
||||||
@TypedGoRoute<CheckingLoginRoute>(
|
|
||||||
path: "/checking-login",
|
|
||||||
name: R.checkingLogin,
|
|
||||||
)
|
|
||||||
class CheckingLoginRoute extends GoRouteData {
|
|
||||||
const CheckingLoginRoute();
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text("Logging in..."),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,12 @@ import 'package:paperless_mobile/routes/routes.dart';
|
|||||||
|
|
||||||
part 'logging_out_route.g.dart';
|
part 'logging_out_route.g.dart';
|
||||||
|
|
||||||
@TypedGoRoute<LogginOutRoute>(
|
@TypedGoRoute<LoggingOutRoute>(
|
||||||
path: "/logging-out",
|
path: "/logging-out",
|
||||||
name: R.loggingOut,
|
name: R.loggingOut,
|
||||||
)
|
)
|
||||||
class LogginOutRoute extends GoRouteData {
|
class LoggingOutRoute extends GoRouteData {
|
||||||
const LogginOutRoute();
|
const LoggingOutRoute();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/src/widgets/framework.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/view/login_to_existing_account_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/view/verify_identity_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/view/widgets/login_transition_page.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||||
import 'package:paperless_mobile/routes/routes.dart';
|
import 'package:paperless_mobile/routes/routes.dart';
|
||||||
|
|
||||||
part 'login_route.g.dart';
|
part 'login_route.g.dart';
|
||||||
@@ -12,12 +20,51 @@ part 'login_route.g.dart';
|
|||||||
@TypedGoRoute<LoginRoute>(
|
@TypedGoRoute<LoginRoute>(
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: R.login,
|
name: R.login,
|
||||||
|
routes: [
|
||||||
|
TypedGoRoute<SwitchingAccountsRoute>(
|
||||||
|
path: "switching-account",
|
||||||
|
name: R.switchingAccount,
|
||||||
|
),
|
||||||
|
TypedGoRoute<AuthenticatingRoute>(
|
||||||
|
path: 'authenticating',
|
||||||
|
name: R.authenticating,
|
||||||
|
),
|
||||||
|
TypedGoRoute<VerifyIdentityRoute>(
|
||||||
|
path: 'verify-identity',
|
||||||
|
name: R.verifyIdentity,
|
||||||
|
),
|
||||||
|
TypedGoRoute<LoginToExistingAccountRoute>(
|
||||||
|
path: 'existing',
|
||||||
|
name: R.loginToExistingAccount,
|
||||||
|
),
|
||||||
|
TypedGoRoute<RestoringSessionRoute>(
|
||||||
|
path: 'restoring-session',
|
||||||
|
name: R.restoringSession,
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
class LoginRoute extends GoRouteData {
|
class LoginRoute extends GoRouteData {
|
||||||
const LoginRoute();
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
final String? serverUrl;
|
||||||
|
final String? username;
|
||||||
|
final String? password;
|
||||||
|
final ClientCertificate? $extra;
|
||||||
|
|
||||||
|
const LoginRoute({
|
||||||
|
this.serverUrl,
|
||||||
|
this.username,
|
||||||
|
this.password,
|
||||||
|
this.$extra,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
return const LoginPage();
|
return LoginPage(
|
||||||
|
initialServerUrl: serverUrl,
|
||||||
|
initialUsername: username,
|
||||||
|
initialPassword: password,
|
||||||
|
initialClientCertificate: $extra,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,3 +75,77 @@ class LoginRoute extends GoRouteData {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SwitchingAccountsRoute extends GoRouteData {
|
||||||
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
|
||||||
|
const SwitchingAccountsRoute();
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return LoginTransitionPage(
|
||||||
|
text: S.of(context)!.switchingAccountsPleaseWait,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticatingRoute extends GoRouteData {
|
||||||
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
|
||||||
|
final String checkLoginStageName;
|
||||||
|
const AuthenticatingRoute(this.checkLoginStageName);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
final stage = AuthenticatingStage.values.byName(checkLoginStageName);
|
||||||
|
final text = switch (stage) {
|
||||||
|
AuthenticatingStage.authenticating => S.of(context)!.authenticatingDots,
|
||||||
|
AuthenticatingStage.persistingLocalUserData =>
|
||||||
|
S.of(context)!.persistingUserInformation,
|
||||||
|
AuthenticatingStage.fetchingUserInformation =>
|
||||||
|
S.of(context)!.fetchingUserInformation,
|
||||||
|
};
|
||||||
|
|
||||||
|
return LoginTransitionPage(text: text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerifyIdentityRoute extends GoRouteData {
|
||||||
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
|
||||||
|
final String userId;
|
||||||
|
const VerifyIdentityRoute({required this.userId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return VerifyIdentityPage(userId: userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginToExistingAccountRoute extends GoRouteData {
|
||||||
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
|
||||||
|
const LoginToExistingAccountRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<String?> redirect(BuildContext context, GoRouterState state) {
|
||||||
|
if (Hive.localUserAccountBox.isEmpty) {
|
||||||
|
return "/login";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return const LoginToExistingAccountPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestoringSessionRoute extends GoRouteData {
|
||||||
|
static final $parentNavigatorKey = rootNavigatorKey;
|
||||||
|
|
||||||
|
const RestoringSessionRoute();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
return LoginTransitionPage(text: S.of(context)!.restoringSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ part 'settings_route.g.dart';
|
|||||||
name: R.settings,
|
name: R.settings,
|
||||||
)
|
)
|
||||||
class SettingsRoute extends GoRouteData {
|
class SettingsRoute extends GoRouteData {
|
||||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
static final GlobalKey<NavigatorState> $parentNavigatorKey = outerShellNavigatorKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/view/pages/switching_accounts_page.dart';
|
|
||||||
import 'package:paperless_mobile/routes/routes.dart';
|
|
||||||
|
|
||||||
part 'switching_accounts_route.g.dart';
|
|
||||||
|
|
||||||
@TypedGoRoute<SwitchingAccountsRoute>(
|
|
||||||
path: '/switching-accounts',
|
|
||||||
name: R.switchingAccounts,
|
|
||||||
)
|
|
||||||
class SwitchingAccountsRoute extends GoRouteData {
|
|
||||||
const SwitchingAccountsRoute();
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
|
||||||
return const SwitchingAccountsPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
|
|
||||||
import 'package:paperless_mobile/routes/routes.dart';
|
|
||||||
|
|
||||||
part 'verify_identity_route.g.dart';
|
|
||||||
|
|
||||||
@TypedGoRoute<VerifyIdentityRoute>(
|
|
||||||
path: '/verify-identity',
|
|
||||||
name: R.verifyIdentity,
|
|
||||||
)
|
|
||||||
class VerifyIdentityRoute extends GoRouteData {
|
|
||||||
const VerifyIdentityRoute();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
|
||||||
return const VerifyIdentityPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,8 @@ class PaperlessFormValidationException implements Exception {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool canParse(Map<String, dynamic> json) {
|
static bool canParse(Map<String, dynamic> json) {
|
||||||
return json.values.every((element) => element is String);
|
return json.values
|
||||||
|
.every((element) => element is String || element is List);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory PaperlessFormValidationException.fromJson(Map<String, dynamic> json) {
|
factory PaperlessFormValidationException.fromJson(Map<String, dynamic> json) {
|
||||||
|
|||||||
@@ -20,13 +20,15 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
|
|||||||
"password": password,
|
"password": password,
|
||||||
},
|
},
|
||||||
options: Options(
|
options: Options(
|
||||||
|
sendTimeout: const Duration(seconds: 5),
|
||||||
|
receiveTimeout: const Duration(seconds: 5),
|
||||||
followRedirects: false,
|
followRedirects: false,
|
||||||
headers: {
|
headers: {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
},
|
},
|
||||||
validateStatus: (status) {
|
// validateStatus: (status) {
|
||||||
return status! == 200;
|
// return status! == 200;
|
||||||
},
|
// },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return response.data['token'];
|
return response.data['token'];
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ abstract class PaperlessDocumentsApi {
|
|||||||
DocumentModel document,
|
DocumentModel document,
|
||||||
String localFilePath, {
|
String localFilePath, {
|
||||||
bool original = false,
|
bool original = false,
|
||||||
void Function(double)? onProgressChanged,
|
void Function(double progress)? onProgressChanged,
|
||||||
});
|
});
|
||||||
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user