Feat: Update scanner persistence, more migrations and bugfixes

This commit is contained in:
Anton Stubenbord
2023-09-28 17:14:27 +02:00
parent 18ab657932
commit 653344c9ee
55 changed files with 887 additions and 442 deletions

View File

@@ -39,8 +39,6 @@ class DioHttpErrorInterceptor extends Interceptor {
),
);
}
} else {
return handler.next(err);
}
}
}

View File

@@ -0,0 +1,12 @@
import 'package:paperless_api/paperless_api.dart';
class InfoMessageException implements Exception {
final ErrorCode code;
final String? message;
final StackTrace? stackTrace;
InfoMessageException({
required this.code,
this.message,
this.stackTrace,
});
}

View File

@@ -43,6 +43,7 @@ Future<void> pushSavedViewDetailsRoute(
context.read(),
context.read(),
LocalUserAppState.current,
context.read(),
savedView: savedView,
),
child: SavedViewDetailsPage(

View File

@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/dio_offline_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/dio_unauthorized_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
@@ -37,6 +38,7 @@ class SessionManager extends ValueNotifier<Dio> {
...interceptors,
DioUnauthorizedInterceptor(),
DioHttpErrorInterceptor(),
DioOfflineInterceptor(),
PrettyDioLogger(
compact: true,
responseBody: false,

View File

@@ -7,6 +7,7 @@ import 'package:paperless_mobile/core/interceptor/server_reachability_error_inte
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:rxdart/subjects.dart';
abstract class ConnectivityStatusService {
Future<bool> isConnectedToInternet();
@@ -20,14 +21,19 @@ abstract class ConnectivityStatusService {
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
final Connectivity _connectivity;
final BehaviorSubject<bool> _connectivityState$ = BehaviorSubject();
ConnectivityStatusServiceImpl(this._connectivity);
ConnectivityStatusServiceImpl(this._connectivity) {
_connectivityState$.addStream(
_connectivity.onConnectivityChanged
.map(_hasActiveInternetConnection)
.asBroadcastStream(),
);
}
@override
Stream<bool> connectivityChanges() {
return _connectivity.onConnectivityChanged
.map(_hasActiveInternetConnection)
.asBroadcastStream();
return _connectivityState$.asBroadcastStream();
}
@override
@@ -98,3 +104,31 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
return ReachabilityStatus.notReachable;
}
}
class ConnectivityStatusServiceMock implements ConnectivityStatusService {
final bool isConnected;
ConnectivityStatusServiceMock(this.isConnected);
@override
Stream<bool> connectivityChanges() {
return Stream.value(isConnected);
}
@override
Future<bool> isConnectedToInternet() async {
return isConnected;
}
@override
Future<ReachabilityStatus> isPaperlessServerReachable(String serverAddress,
[ClientCertificate? clientCertificate]) async {
return isConnected
? ReachabilityStatus.reachable
: ReachabilityStatus.notReachable;
}
@override
Future<bool> isServerReachable(String serverAddress) async {
return isConnected;
}
}

View File

@@ -25,7 +25,7 @@ class FileService {
case PaperlessDirectoryType.temporary:
return temporaryDirectory;
case PaperlessDirectoryType.scans:
return scanDirectory;
return temporaryScansDirectory;
case PaperlessDirectoryType.download:
return downloadsDirectory;
}
@@ -52,8 +52,7 @@ class FileService {
} else if (Platform.isIOS) {
final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/documents');
dir.createSync();
return dir;
return dir.create(recursive: true);
} else {
throw UnsupportedError("Platform not supported.");
}
@@ -72,33 +71,22 @@ class FileService {
} else if (Platform.isIOS) {
final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/downloads');
dir.createSync();
return dir;
return dir.create(recursive: true);
} else {
throw UnsupportedError("Platform not supported.");
}
}
static Future<Directory?> get scanDirectory async {
if (Platform.isAndroid) {
final scanDir = await getExternalStorageDirectories(
type: StorageDirectory.dcim,
);
return scanDir!.first;
} else if (Platform.isIOS) {
final appDir = await getApplicationDocumentsDirectory();
final dir = Directory('${appDir.path}/scans');
dir.createSync();
return dir;
} else {
throw UnsupportedError("Platform not supported.");
}
static Future<Directory> get temporaryScansDirectory async {
final tempDir = await temporaryDirectory;
final scansDir = Directory('${tempDir.path}/scans');
return scansDir.create(recursive: true);
}
static Future<void> clearUserData() async {
final scanDir = await scanDirectory;
final scanDir = await temporaryScansDirectory;
final tempDir = await temporaryDirectory;
await scanDir?.delete(recursive: true);
await scanDir.delete(recursive: true);
await tempDir.delete(recursive: true);
}

View File

@@ -75,5 +75,6 @@ String translateError(BuildContext context, ErrorCode code) {
ErrorCode.loadTasksError => S.of(context)!.couldNotLoadTasks,
ErrorCode.userNotFound => S.of(context)!.userNotFound,
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
ErrorCode.userAlreadyExists => S.of(context)!.userAlreadyExists,
};
}

View File

@@ -1,3 +1,4 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -20,6 +21,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/document_previe
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.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/message_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
@@ -199,6 +201,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
context.read(),
context.read(),
context.read(),
context.read(),
documentId: state.document.id,
),
child: Padding(
@@ -322,28 +325,45 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return BottomAppBar(
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
final isConnected = connectivityState.isConnected;
final currentUser = context.watch<LocalUserAccount>();
final canDelete =
isConnected && currentUser.paperlessUser.canDeleteDocuments;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed:
canDelete ? () => _onDelete(state.document) : null,
).paddedSymmetrically(horizontal: 4),
DocumentDownloadButton(
document: state.document,
enabled: isConnected,
ConnectivityAwareActionWrapper(
disabled: !currentUser.paperlessUser.canDeleteDocuments,
offlineBuilder: (context, child) {
return const IconButton(
icon: Icon(Icons.delete),
onPressed: null,
).paddedSymmetrically(horizontal: 4);
},
child: IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed: () => _onDelete(state.document),
).paddedSymmetrically(horizontal: 4),
),
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) =>
const DocumentDownloadButton(
document: null,
enabled: false,
),
child: DocumentDownloadButton(
document: state.document,
),
),
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => const IconButton(
icon: Icon(Icons.open_in_new),
onPressed: null,
),
child: IconButton(
tooltip: S.of(context)!.openInSystemViewer,
icon: const Icon(Icons.open_in_new),
onPressed: _onOpenFileInSystemViewer,
).paddedOnly(right: 4.0),
),
IconButton(
tooltip: S.of(context)!.openInSystemViewer,
icon: const Icon(Icons.open_in_new),
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
).paddedOnly(right: 4.0),
DocumentShareButton(document: state.document),
IconButton(
tooltip: S.of(context)!.print,

View File

@@ -11,6 +11,7 @@ import 'package:paperless_mobile/features/document_details/cubit/document_detail
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
import 'package:paperless_mobile/features/settings/model/file_download_type.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/message_helpers.dart';
import 'package:paperless_mobile/helpers/permission_helpers.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -34,19 +35,25 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
@override
Widget build(BuildContext context) {
return IconButton(
tooltip: S.of(context)!.shareTooltip,
icon: _isDownloadPending
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(),
)
: const Icon(Icons.share),
onPressed: widget.document != null && widget.enabled
? () => _onShare(widget.document!)
: null,
).paddedOnly(right: 4);
return ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => const IconButton(
icon: Icon(Icons.share),
onPressed: null,
),
child: IconButton(
tooltip: S.of(context)!.shareTooltip,
icon: _isDownloadPending
? const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(),
)
: const Icon(Icons.share),
onPressed: widget.document != null && widget.enabled
? () => _onShare(widget.document!)
: null,
).paddedOnly(right: 4),
);
}
Future<void> _onShare(DocumentModel document) async {

View File

@@ -1,43 +1,71 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:rxdart/rxdart.dart';
class DocumentScannerCubit extends Cubit<List<File>> {
part 'document_scanner_state.dart';
class DocumentScannerCubit extends Cubit<DocumentScannerState> {
final LocalNotificationService _notificationService;
DocumentScannerCubit(this._notificationService) : super(const []);
DocumentScannerCubit(this._notificationService)
: super(const InitialDocumentScannerState());
void addScan(File file) => emit([...state, file]);
void removeScan(int fileIndex) {
try {
state[fileIndex].deleteSync();
final scans = [...state];
scans.removeAt(fileIndex);
emit(scans);
} catch (_) {
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
}
Future<void> initialize() async {
debugPrint("Restoring scans...");
emit(const RestoringDocumentScannerState());
final tempDir = await FileService.temporaryScansDirectory;
final allFiles = tempDir.list().whereType<File>();
final scans =
await allFiles.where((event) => event.path.endsWith(".jpeg")).toList();
debugPrint("Restored ${scans.length} scans.");
emit(
scans.isEmpty
? const InitialDocumentScannerState()
: LoadedDocumentScannerState(scans: scans),
);
}
void reset() {
void addScan(File file) async {
emit(LoadedDocumentScannerState(
scans: [...state.scans, file],
));
}
Future<void> removeScan(File file) async {
try {
for (final doc in state) {
doc.deleteSync();
if (kDebugMode) {
log('[ScannerCubit]: Removed ${doc.path}');
}
}
await file.delete();
} catch (error, stackTrace) {
throw InfoMessageException(
code: ErrorCode.scanRemoveFailed,
message: error.toString(),
stackTrace: stackTrace,
);
}
final scans = state.scans..remove(file);
emit(
scans.isEmpty
? const InitialDocumentScannerState()
: LoadedDocumentScannerState(scans: scans),
);
}
Future<void> reset() async {
try {
Future.wait([
for (final file in state.scans) file.delete(),
]);
imageCache.clear();
emit([]);
} catch (_) {
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
} finally {
emit(const InitialDocumentScannerState());
}
}

View File

@@ -0,0 +1,30 @@
part of 'document_scanner_cubit.dart';
sealed class DocumentScannerState {
final List<File> scans;
const DocumentScannerState({
this.scans = const [],
});
}
class InitialDocumentScannerState extends DocumentScannerState {
const InitialDocumentScannerState();
}
class RestoringDocumentScannerState extends DocumentScannerState {
const RestoringDocumentScannerState({super.scans});
}
class LoadedDocumentScannerState extends DocumentScannerState {
const LoadedDocumentScannerState({super.scans});
}
class ErrorDocumentScannerState extends DocumentScannerState {
final String message;
const ErrorDocumentScannerState({
required this.message,
super.scans,
});
}

View File

@@ -10,7 +10,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.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/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/global/constants.dart';
@@ -25,6 +24,7 @@ import 'package:paperless_mobile/features/document_upload/view/document_upload_p
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.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/message_helpers.dart';
import 'package:paperless_mobile/helpers/permission_helpers.dart';
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
@@ -52,66 +52,54 @@ class _ScannerPageState extends State<ScannerPage>
@override
Widget build(BuildContext context) {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) {
return BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
return SafeArea(
top: true,
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton(
heroTag: "fab_document_edit",
onPressed: () => _openDocumentScanner(context),
child: const Icon(Icons.add_a_photo_outlined),
),
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: SliverSearchBar(
titleText: S.of(context)!.scanner,
),
),
SliverOverlapAbsorber(
handle: actionsHandle,
sliver: SliverPinnedHeader(
child: _buildActions(connectedState.isConnected),
),
),
],
body: BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
if (state.isEmpty) {
return SizedBox.expand(
child: Center(
child: _buildEmptyState(
connectedState.isConnected,
state,
),
),
);
} else {
return _buildImageGrid(state);
}
},
),
),
return SafeArea(
top: true,
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton(
heroTag: "fab_document_edit",
onPressed: () => _openDocumentScanner(context),
child: const Icon(Icons.add_a_photo_outlined),
),
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: SliverSearchBar(
titleText: S.of(context)!.scanner,
),
);
},
);
},
),
SliverOverlapAbsorber(
handle: actionsHandle,
sliver: SliverPinnedHeader(
child: _buildActions(),
),
),
],
body: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
builder: (context, state) {
return switch (state) {
InitialDocumentScannerState() => _buildEmptyState(),
RestoringDocumentScannerState() => Center(
child: Text("Restoring..."),
),
LoadedDocumentScannerState() => _buildImageGrid(state.scans),
ErrorDocumentScannerState() => Placeholder(),
};
},
),
),
),
);
}
Widget _buildActions(bool isConnected) {
Widget _buildActions() {
return ColoredBox(
color: Theme.of(context).colorScheme.background,
child: SizedBox(
height: kTextTabBarHeight,
child: BlocBuilder<DocumentScannerCubit, List<File>>(
child: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
builder: (context, state) {
return RawScrollbar(
padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
@@ -130,12 +118,12 @@ class _ScannerPageState extends State<ScannerPage>
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isNotEmpty
onPressed: state.scans.isNotEmpty
? () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes: _assembleFileBytes(
state,
state.scans,
forcePdf: true,
).then((file) => file.bytes),
),
@@ -150,19 +138,32 @@ class _ScannerPageState extends State<ScannerPage>
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isEmpty ? null : () => _reset(context),
onPressed:
state.scans.isEmpty ? null : () => _reset(context),
icon: const Icon(Icons.delete_sweep_outlined),
),
SizedBox(width: 8),
TextButton.icon(
label: Text(S.of(context)!.upload),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) {
return TextButton.icon(
label: Text(S.of(context)!.upload),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: null,
icon: const Icon(Icons.upload_outlined),
);
},
disabled: state.scans.isEmpty,
child: TextButton.icon(
label: Text(S.of(context)!.upload),
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: () =>
_onPrepareDocumentUpload(context, state.scans),
icon: const Icon(Icons.upload_outlined),
),
onPressed: state.isEmpty || !isConnected
? null
: () => _onPrepareDocumentUpload(context),
icon: const Icon(Icons.upload_outlined),
),
SizedBox(width: 8),
TextButton.icon(
@@ -170,7 +171,7 @@ class _ScannerPageState extends State<ScannerPage>
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
),
onPressed: state.isEmpty ? null : _onSaveToFile,
onPressed: state.scans.isEmpty ? null : _onSaveToFile,
icon: const Icon(Icons.save_alt_outlined),
),
SizedBox(width: 12),
@@ -192,7 +193,7 @@ class _ScannerPageState extends State<ScannerPage>
final cubit = context.read<DocumentScannerCubit>();
final file = await _assembleFileBytes(
forcePdf: true,
context.read<DocumentScannerCubit>().state,
context.read<DocumentScannerCubit>().state.scans,
);
try {
final globalSettings =
@@ -249,9 +250,9 @@ class _ScannerPageState extends State<ScannerPage>
context.read<DocumentScannerCubit>().addScan(file);
}
void _onPrepareDocumentUpload(BuildContext context) async {
void _onPrepareDocumentUpload(BuildContext context, List<File> scans) async {
final file = await _assembleFileBytes(
context.read<DocumentScannerCubit>().state,
scans,
forcePdf: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.enforceSinglePagePdfUpload,
@@ -269,10 +270,7 @@ class _ScannerPageState extends State<ScannerPage>
}
}
Widget _buildEmptyState(bool isConnected, List<File> scans) {
if (scans.isNotEmpty) {
return _buildImageGrid(scans);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
@@ -288,9 +286,15 @@ class _ScannerPageState extends State<ScannerPage>
onPressed: () => _openDocumentScanner(context),
),
Text(S.of(context)!.or),
TextButton(
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
onPressed: isConnected ? _onUploadFromFilesystem : null,
ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => TextButton(
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
onPressed: null,
),
child: TextButton(
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
onPressed: _onUploadFromFilesystem,
),
),
],
),
@@ -318,7 +322,9 @@ class _ScannerPageState extends State<ScannerPage>
file: scans[index],
onDelete: () async {
try {
context.read<DocumentScannerCubit>().removeScan(index);
context
.read<DocumentScannerCubit>()
.removeScan(scans[index]);
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}

View File

@@ -4,6 +4,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.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/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -119,4 +120,9 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState>
@override
Future<void> onFilterUpdated(DocumentFilter filter) async {}
@override
// TODO: implement connectivityStatusService
ConnectivityStatusService get connectivityStatusService =>
throw UnimplementedError();
}

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
part 'document_upload_state.dart';
@@ -13,12 +13,12 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final PaperlessDocumentsApi _documentApi;
final LabelRepository _labelRepository;
final Connectivity _connectivity;
final ConnectivityStatusService _connectivityStatusService;
DocumentUploadCubit(
this._labelRepository,
this._documentApi,
this._connectivity,
this._connectivityStatusService,
) : super(const DocumentUploadState()) {
_labelRepository.addListener(
this,

View File

@@ -7,6 +7,7 @@ import 'package:paperless_api/paperless_api.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/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -20,6 +21,8 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override
final ConnectivityStatusService connectivityStatusService;
@override
final DocumentChangedNotifier notifier;
@@ -31,6 +34,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
this.notifier,
this._labelRepository,
this._userState,
this.connectivityStatusService,
) : super(DocumentsState(
filter: _userState.currentDocumentFilter,
viewType: _userState.documentsPageViewType,

View File

@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
@@ -333,12 +334,16 @@ class _DocumentsPageState extends State<DocumentsPage> {
slivers: <Widget>[
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: savedViewsHandle),
BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) =>
previous.filter != current.filter,
builder: (context, state) {
return SliverToBoxAdapter(
child: SavedViewsWidget(
SliverToBoxAdapter(
child: BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) =>
previous.filter != current.filter,
builder: (context, state) {
final currentUser = context.watch<LocalUserAccount>();
if (!currentUser.paperlessUser.canViewSavedViews) {
return const SizedBox.shrink();
}
return SavedViewsWidget(
controller: _savedViewsExpansionController,
onViewSelected: (view) {
final cubit = context.read<DocumentsCubit>();
@@ -372,9 +377,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
},
filter: state.filter,
),
);
},
);
},
),
),
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {

View File

@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
@@ -28,17 +29,17 @@ class DocumentPreview extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: isClickable
? () {
DocumentPreviewRoute($extra: document).push(context);
}
: null,
child: HeroMode(
enabled: enableHero,
child: Hero(
tag: "thumb_${document.id}",
child: _buildPreview(context),
return ConnectivityAwareActionWrapper(
child: GestureDetector(
onTap: isClickable
? () => DocumentPreviewRoute($extra: document).push(context)
: null,
child: HeroMode(
enabled: enableHero,
child: Hero(
tag: "thumb_${document.id}",
child: _buildPreview(context),
),
),
),
);

View File

@@ -6,6 +6,7 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_chip.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
class SavedViewsWidget extends StatefulWidget {
@@ -146,15 +147,17 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
final isSelected =
(widget.filter.selectedView ?? -1) ==
view.id;
return SavedViewChip(
view: view,
onViewSelected: widget.onViewSelected,
selected: isSelected,
hasChanged: isSelected &&
view.toDocumentFilter() !=
widget.filter,
onUpdateView: widget.onUpdateView,
onDeleteView: widget.onDeleteView,
return ConnectivityAwareActionWrapper(
child: SavedViewChip(
view: view,
onViewSelected: widget.onViewSelected,
selected: isSelected,
hasChanged: isSelected &&
view.toDocumentFilter() !=
widget.filter,
onUpdateView: widget.onUpdateView,
onDeleteView: widget.onDeleteView,
),
);
},
separatorBuilder: (context, index) =>
@@ -178,12 +181,14 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
alignment: Alignment.centerRight,
child: Tooltip(
message: S.of(context)!.createFromCurrentFilter,
child: TextButton.icon(
onPressed: () {
CreateSavedViewRoute(widget.filter).push(context);
},
icon: const Icon(Icons.add),
label: Text(S.of(context)!.newView),
child: ConnectivityAwareActionWrapper(
child: TextButton.icon(
onPressed: () {
CreateSavedViewRoute(widget.filter).push(context);
},
icon: const Icon(Icons.add),
label: Text(S.of(context)!.newView),
),
),
).padded(4),
),

View File

@@ -6,6 +6,7 @@ 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/widgets/search/document_filter_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
enum DateRangeSelection { before, after }

View File

@@ -5,6 +5,7 @@ import 'package:paperless_mobile/core/translation/sort_field_localization_mapper
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
class SortDocumentsButton extends StatelessWidget {
final bool enabled;
@@ -20,55 +21,65 @@ class SortDocumentsButton extends StatelessWidget {
if (state.filter.sortField == null) {
return const SizedBox.shrink();
}
print(state.filter.sortField);
return TextButton.icon(
icon: Icon(state.filter.sortOrder == SortOrder.ascending
? Icons.arrow_upward
: Icons.arrow_downward),
label: Text(translateSortField(context, state.filter.sortField)),
onPressed: enabled
? () {
showModalBottomSheet(
elevation: 2,
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
builder: (_) => BlocProvider<DocumentsCubit>.value(
value: context.read<DocumentsCubit>(),
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LabelCubit(context.read()),
),
],
child: SortFieldSelectionBottomSheet(
initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) {
return context
.read<DocumentsCubit>()
.updateCurrentFilter(
(filter) => filter.copyWith(
sortField: field,
sortOrder: order,
),
);
},
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
final icon = Icon(state.filter.sortOrder == SortOrder.ascending
? Icons.arrow_upward
: Icons.arrow_downward);
final label = Text(translateSortField(context, state.filter.sortField));
return ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) {
return TextButton.icon(
icon: icon,
label: label,
onPressed: null,
);
},
child: TextButton.icon(
icon: icon,
label: label,
onPressed: enabled
? () {
showModalBottomSheet(
elevation: 2,
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
),
);
}
: null,
builder: (_) => BlocProvider<DocumentsCubit>.value(
value: context.read<DocumentsCubit>(),
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LabelCubit(context.read()),
),
],
child: SortFieldSelectionBottomSheet(
initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) {
return context
.read<DocumentsCubit>()
.updateCurrentFilter(
(filter) => filter.copyWith(
sortField: field,
sortOrder: order,
),
);
},
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
),
),
),
);
}
: null,
),
);
},
);

View File

@@ -160,11 +160,13 @@ class HomeShellWidget extends StatelessWidget {
Hive.box<LocalUserAppState>(
HiveBoxes.localUserAppState)
.get(currentUserId)!,
context.read(),
)..initialize(),
),
Provider(
create: (context) =>
DocumentScannerCubit(context.read()),
DocumentScannerCubit(context.read())
..initialize(),
),
Provider(
create: (context) {
@@ -173,6 +175,7 @@ class HomeShellWidget extends StatelessWidget {
context.read(),
context.read(),
context.read(),
context.read(),
);
if (currentLocalUser
.paperlessUser.canViewDocuments &&

View File

@@ -7,6 +7,7 @@ import 'package:paperless_api/paperless_api.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_state.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
@@ -18,7 +19,8 @@ class InboxCubit extends HydratedCubit<InboxState>
final LabelRepository _labelRepository;
final PaperlessDocumentsApi _documentsApi;
@override
final ConnectivityStatusService connectivityStatusService;
@override
final DocumentChangedNotifier notifier;
@@ -32,6 +34,7 @@ class InboxCubit extends HydratedCubit<InboxState>
this._statsApi,
this._labelRepository,
this.notifier,
this.connectivityStatusService,
) : super(InboxState(
labels: _labelRepository.state,
)) {

View File

@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.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/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
@@ -17,6 +18,7 @@ import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.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/message_helpers.dart';
class InboxPage extends StatefulWidget {
@@ -74,45 +76,50 @@ class _InboxPageState extends State<InboxPage>
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
return Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
if (!state.hasLoaded || state.documents.isEmpty || !canEditDocument) {
return const SizedBox.shrink();
}
return FloatingActionButton.extended(
extendedPadding: _showExtendedFab
? null
: const EdgeInsets.symmetric(horizontal: 16),
heroTag: "inbox_page_fab",
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
child: child,
),
);
},
child: _showExtendedFab
? Row(
children: [
const Icon(Icons.done_all),
Text(S.of(context)!.allSeen),
],
)
: const Icon(Icons.done_all),
),
onPressed: state.hasLoaded && state.documents.isNotEmpty
? () => _onMarkAllAsSeen(
state.documents,
state.inboxTags,
)
: null,
);
},
floatingActionButton: ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => const SizedBox.shrink(),
child: BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
if (!state.hasLoaded ||
state.documents.isEmpty ||
!canEditDocument) {
return const SizedBox.shrink();
}
return FloatingActionButton.extended(
extendedPadding: _showExtendedFab
? null
: const EdgeInsets.symmetric(horizontal: 16),
heroTag: "inbox_page_fab",
label: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axis: Axis.horizontal,
child: child,
),
);
},
child: _showExtendedFab
? Row(
children: [
const Icon(Icons.done_all),
Text(S.of(context)!.allSeen),
],
)
: const Icon(Icons.done_all),
),
onPressed: state.hasLoaded && state.documents.isNotEmpty
? () => _onMarkAllAsSeen(
state.documents,
state.inboxTags,
)
: null,
);
},
),
),
body: SafeArea(
top: true,
@@ -268,6 +275,12 @@ class _InboxPageState extends State<InboxPage>
showSnackBar(context, S.of(context)!.missingPermissions);
return false;
}
final isConnectedToInternet =
await context.read<ConnectivityStatusService>().isConnectedToInternet();
if (!isConnectedToInternet) {
showSnackBar(context, S.of(context)!.youAreCurrentlyOffline);
return false;
}
try {
final removedTags = await context.read<InboxCubit>().removeFromInbox(doc);
showSnackBar(

View File

@@ -1,10 +1,8 @@
import 'package:collection/collection.dart';
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/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -16,6 +14,7 @@ import 'package:paperless_mobile/features/inbox/cubit/inbox_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
class InboxItemPlaceholder extends StatelessWidget {
@@ -228,7 +227,9 @@ class _InboxItemState extends State<InboxItem> {
),
LimitedBox(
maxHeight: 56,
child: _buildActions(context),
child: ConnectivityAwareActionWrapper(
child: _buildActions(context),
),
),
],
).paddedOnly(left: 8, top: 8, bottom: 8),

View File

@@ -13,6 +13,7 @@ import 'package:paperless_mobile/features/document_search/view/sliver_search_bar
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
class LabelsPage extends StatefulWidget {
@@ -66,36 +67,37 @@ class _LabelsPageState extends State<LabelsPage>
.getValue()!
.loggedInUserId;
final user = box.get(currentUserId)!.paperlessUser;
final fabLabel = [
S.of(context)!.addCorrespondent,
S.of(context)!.addDocumentType,
S.of(context)!.addTag,
S.of(context)!.addStoragePath,
][_currentIndex];
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) {
return SafeArea(
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton.extended(
heroTag: "inbox_page_fab",
label: Text(
[
S.of(context)!.addCorrespondent,
S.of(context)!.addDocumentType,
S.of(context)!.addTag,
S.of(context)!.addStoragePath,
floatingActionButton: ConnectivityAwareActionWrapper(
offlineBuilder: (context, child) => const SizedBox.shrink(),
child: FloatingActionButton.extended(
heroTag: "inbox_page_fab",
label: Text(fabLabel),
icon: Icon(Icons.add),
onPressed: [
if (user.canViewCorrespondents)
() => CreateLabelRoute(LabelType.correspondent)
.push(context),
if (user.canViewDocumentTypes)
() => CreateLabelRoute(LabelType.documentType)
.push(context),
if (user.canViewTags)
() => CreateLabelRoute(LabelType.tag).push(context),
if (user.canViewStoragePaths)
() => CreateLabelRoute(LabelType.storagePath)
.push(context),
][_currentIndex],
),
icon: Icon(Icons.add),
onPressed: [
if (user.canViewCorrespondents)
() => CreateLabelRoute(LabelType.correspondent)
.push(context),
if (user.canViewDocumentTypes)
() => CreateLabelRoute(LabelType.documentType)
.push(context),
if (user.canViewTags)
() => CreateLabelRoute(LabelType.tag).push(context),
if (user.canViewStoragePaths)
() => CreateLabelRoute(LabelType.storagePath)
.push(context),
][_currentIndex],
),
body: NestedScrollView(
floatHeaderSlivers: true,

View File

@@ -44,7 +44,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
if (!connectivityState.isConnected) {
return const OfflineWidget();
return const SliverFillRemaining(child: OfflineWidget());
}
final sortedLabels = labels.values.toList()..sort();
if (labels.isEmpty) {

View File

@@ -22,6 +22,7 @@ class LandingPage extends StatefulWidget {
class _LandingPageState extends State<LandingPage> {
final _searchBarHandle = SliverOverlapAbsorberHandle();
@override
Widget build(BuildContext context) {
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
@@ -121,7 +122,6 @@ class _LandingPageState extends State<LandingPage> {
Widget _buildStatisticsCard(BuildContext context) {
final currentUser = context.read<LocalUserAccount>().paperlessUser;
return ExpansionCard(
initiallyExpanded: false,
title: Text(

View File

@@ -3,6 +3,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.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/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -14,7 +15,8 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@override
final ConnectivityStatusService connectivityStatusService;
@override
final DocumentChangedNotifier notifier;
@@ -25,6 +27,7 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
this.api,
this.notifier,
this._labelRepository,
this.connectivityStatusService,
) : super(LinkedDocumentsState(filter: filter)) {
updateFilter(filter: filter);
_labelRepository.addListener(

View File

@@ -1,9 +1,11 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.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_extensions.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
@@ -12,25 +14,28 @@ 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/user_credentials.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.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/service/connectivity_status_service.dart';
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/services/authentication_service.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
part 'authentication_cubit.freezed.dart';
part 'authentication_state.dart';
class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessApiFactory _apiFactory;
final SessionManager _sessionManager;
final ConnectivityStatusService _connectivityService;
AuthenticationCubit(
this._localAuthService,
this._apiFactory,
this._sessionManager,
) : super(const AuthenticationState.unauthenticated());
this._connectivityService,
) : super(const UnauthenticatedState());
Future<void> login({
required LoginFormCredentials credentials,
@@ -51,8 +56,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
_sessionManager,
);
final apiVersion = await _getApiVersion(_sessionManager.client);
// Mark logged in user as currently active user.
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
@@ -60,7 +63,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await globalSettings.save();
emit(
AuthenticationState.authenticated(
AuthenticatedState(
localUserId: localUserId,
),
);
@@ -72,11 +75,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async {
emit(const AuthenticationState.switchingAccounts());
emit(const SwitchingAccountsState());
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.loggedInUserId == localUserId) {
emit(AuthenticationState.authenticated(localUserId: localUserId));
emit(AuthenticatedState(localUserId: localUserId));
return;
}
final userAccountBox =
@@ -125,7 +128,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
apiVersion,
);
emit(AuthenticationState.authenticated(
emit(AuthenticatedState(
localUserId: localUserId,
));
});
@@ -182,7 +185,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"There is nothing to restore.",
);
// If there is nothing to restore, we can quit here.
emit(const AuthenticationState.unauthenticated());
emit(const UnauthenticatedState());
return;
}
final localUserAccountBox =
@@ -203,7 +206,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final localAuthSuccess =
await _localAuthService.authenticateLocalUser(authenticationMesage);
if (!localAuthSuccess) {
emit(const AuthenticationState.requriresLocalAuthentication());
emit(const RequiresLocalAuthenticationState());
_debugPrintMessage(
"restoreSessionState",
"User could not be authenticated.",
@@ -239,14 +242,17 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"User should be authenticated but no authentication information was found.",
);
}
_debugPrintMessage(
"restoreSessionState",
"Authentication credentials successfully retrieved.",
);
_debugPrintMessage(
"restoreSessionState",
"Updating current session state...",
);
_sessionManager.updateSettings(
clientCertificate: authentication.clientCertificate,
authToken: authentication.token,
@@ -256,18 +262,32 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"restoreSessionState",
"Current session state successfully updated.",
);
final hasInternetConnection =
await _connectivityService.isConnectedToInternet();
if (hasInternetConnection) {
_debugPrintMessage(
"restoreSessionMState",
"Updating server user...",
);
final apiVersion = await _getApiVersion(_sessionManager.client);
await _updateRemoteUser(
_sessionManager,
localUserAccount,
apiVersion,
);
_debugPrintMessage(
"restoreSessionMState",
"Successfully updated server user.",
);
} else {
_debugPrintMessage(
"restoreSessionMState",
"Skipping update of server user (no internet connection).",
);
}
emit(AuthenticatedState(localUserId: localUserId));
final apiVersion = await _getApiVersion(_sessionManager.client);
await _updateRemoteUser(
_sessionManager,
localUserAccount,
apiVersion,
);
emit(
AuthenticationState.authenticated(
localUserId: localUserId,
),
);
_debugPrintMessage(
"restoreSessionState",
"Session was successfully restored.",
@@ -285,7 +305,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
globalSettings.loggedInUserId = null;
await globalSettings.save();
emit(const AuthenticationState.unauthenticated());
emit(const UnauthenticatedState());
_debugPrintMessage(
"logout",
"User successfully logged out.",
@@ -353,7 +373,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"_addUser",
"An error occurred! The user $localUserId already exists.",
);
throw Exception("User already exists!");
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
}
final apiVersion = await _getApiVersion(sessionManager.client);
_debugPrintMessage(

View File

@@ -1,19 +1,32 @@
part of 'authentication_cubit.dart';
@freezed
class AuthenticationState with _$AuthenticationState {
const AuthenticationState._();
sealed class AuthenticationState {
const AuthenticationState();
const factory AuthenticationState.unauthenticated() = _Unauthenticated;
const factory AuthenticationState.requriresLocalAuthentication() =
_RequiresLocalAuthentication;
const factory AuthenticationState.authenticated({
required String localUserId,
}) = _Authenticated;
const factory AuthenticationState.switchingAccounts() = _SwitchingAccounts;
bool get isAuthenticated => maybeWhen(
authenticated: (_) => true,
orElse: () => false,
);
bool get isAuthenticated =>
switch (this) { AuthenticatedState() => true, _ => false };
}
class UnauthenticatedState extends AuthenticationState {
const UnauthenticatedState();
}
class RequiresLocalAuthenticationState extends AuthenticationState {
const RequiresLocalAuthenticationState();
}
class AuthenticatedState extends AuthenticationState {
final String localUserId;
const AuthenticatedState({
required this.localUserId,
});
}
class SwitchingAccountsState extends AuthenticationState {
const SwitchingAccountsState();
}
class AuthenticationErrorState extends AuthenticationState {
const AuthenticationErrorState();
}

View File

@@ -8,6 +8,7 @@ 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/model/info_message_exception.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/model/client_certificate_form_model.dart';
@@ -153,6 +154,8 @@ class _AddAccountPageState extends State<AddAccountPage> {
showErrorMessage(context, error);
} on ServerMessageException catch (error) {
showLocalizedError(context, error.message);
} on InfoMessageException catch (error) {
showInfoMessage(context, error);
} catch (error) {
showGenericError(context, error);
}

View File

@@ -52,10 +52,11 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
Text(
S.of(context)!.loginRequiredPermissionsHint,
style: Theme.of(context).textTheme.bodySmall?.apply(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.6)),
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(0.6),
),
).padded(16),
],
),
@@ -64,11 +65,16 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: () async {
setState(() => _isLoginLoading = true);
await widget.onSubmit();
setState(() => _isLoginLoading = false);
},
onPressed: !_isLoginLoading
? () async {
setState(() => _isLoginLoading = true);
try {
await widget.onSubmit();
} finally {
setState(() => _isLoginLoading = false);
}
}
: null,
child: Text(S.of(context)!.signIn),
)
],

View File

@@ -2,6 +2,8 @@ import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:rxdart/streams.dart';
import 'paged_documents_state.dart';
@@ -11,13 +13,16 @@ import 'paged_documents_state.dart';
///
mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
on BlocBase<State> {
ConnectivityStatusService get connectivityStatusService;
PaperlessDocumentsApi get api;
DocumentChangedNotifier get notifier;
Future<void> onFilterUpdated(DocumentFilter filter);
Future<void> loadMore() async {
if (state.isLastPageLoaded) {
final hasConnection =
await connectivityStatusService.isConnectedToInternet();
if (state.isLastPageLoaded || !hasConnection) {
return;
}
emit(state.copyWithPaged(isLoading: true));
@@ -47,6 +52,32 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
Future<void> updateFilter({
final DocumentFilter filter = const DocumentFilter(),
}) async {
final hasConnection =
await connectivityStatusService.isConnectedToInternet();
if (!hasConnection) {
// Just filter currently loaded documents
final filteredDocuments = state.value
.expand((page) => page.results)
.where((doc) => filter.matches(doc))
.toList();
emit(state.copyWithPaged(isLoading: true));
emit(
state.copyWithPaged(
filter: filter,
value: [
PagedSearchResult(
results: filteredDocuments,
count: filteredDocuments.length,
next: null,
previous: null,
)
],
hasLoaded: true,
),
);
return;
}
try {
emit(state.copyWithPaged(isLoading: true));
final result = await api.findAll(filter.copyWith(page: 1));

View File

@@ -3,6 +3,7 @@ import 'package:paperless_api/paperless_api.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/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -15,7 +16,8 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override
final ConnectivityStatusService connectivityStatusService;
@override
final DocumentChangedNotifier notifier;
@@ -27,7 +29,8 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
this.api,
this.notifier,
this._labelRepository,
this._userState, {
this._userState,
this.connectivityStatusService, {
required this.savedView,
int initialCount = 25,
}) : super(

View File

@@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
part 'saved_view_preview_state.dart';
part 'saved_view_preview_cubit.freezed.dart';
@@ -8,11 +9,21 @@ part 'saved_view_preview_cubit.freezed.dart';
class SavedViewPreviewCubit extends Cubit<SavedViewPreviewState> {
final PaperlessDocumentsApi _api;
final SavedView view;
SavedViewPreviewCubit(this._api, this.view)
: super(const SavedViewPreviewState.initial());
final ConnectivityStatusService _connectivityStatusService;
SavedViewPreviewCubit(
this._api,
this._connectivityStatusService, {
required this.view,
}) : super(const InitialSavedViewPreviewState());
Future<void> initialize() async {
emit(const SavedViewPreviewState.loading());
final isConnected =
await _connectivityStatusService.isConnectedToInternet();
if (!isConnected) {
emit(const OfflineSavedViewPreviewState());
return;
}
emit(const LoadingSavedViewPreviewState());
try {
final documents = await _api.findAll(
view.toDocumentFilter().copyWith(
@@ -20,9 +31,9 @@ class SavedViewPreviewCubit extends Cubit<SavedViewPreviewState> {
pageSize: 5,
),
);
emit(SavedViewPreviewState.loaded(documents: documents.results));
emit(LoadedSavedViewPreviewState(documents: documents.results));
} catch (e) {
emit(const SavedViewPreviewState.error());
emit(const ErrorSavedViewPreviewState());
}
}
}

View File

@@ -1,11 +1,29 @@
part of 'saved_view_preview_cubit.dart';
@freezed
class SavedViewPreviewState with _$SavedViewPreviewState {
const factory SavedViewPreviewState.initial() = _Initial;
const factory SavedViewPreviewState.loading() = _Loading;
const factory SavedViewPreviewState.loaded({
required List<DocumentModel> documents,
}) = _Loaded;
const factory SavedViewPreviewState.error() = _Error;
sealed class SavedViewPreviewState {
const SavedViewPreviewState();
}
class InitialSavedViewPreviewState extends SavedViewPreviewState {
const InitialSavedViewPreviewState();
}
class LoadingSavedViewPreviewState extends SavedViewPreviewState {
const LoadingSavedViewPreviewState();
}
class LoadedSavedViewPreviewState extends SavedViewPreviewState {
final List<DocumentModel> documents;
const LoadedSavedViewPreviewState({
required this.documents,
});
}
class ErrorSavedViewPreviewState extends SavedViewPreviewState {
const ErrorSavedViewPreviewState();
}
class OfflineSavedViewPreviewState extends SavedViewPreviewState {
const OfflineSavedViewPreviewState();
}

View File

@@ -22,8 +22,11 @@ class SavedViewPreview extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (context) =>
SavedViewPreviewCubit(context.read(), savedView)..initialize(),
create: (context) => SavedViewPreviewCubit(
context.read(),
context.read(),
view: savedView,
)..initialize(),
builder: (context, child) {
return ExpansionCard(
initiallyExpanded: expanded,
@@ -33,34 +36,40 @@ class SavedViewPreview extends StatelessWidget {
children: [
BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
builder: (context, state) {
return state.maybeWhen(
loaded: (documents) {
if (documents.isEmpty) {
return Text(S.of(context)!.noDocumentsFound).padded();
} else {
return Column(
children: [
for (final document in documents)
DocumentListItem(
document: document,
isLabelClickable: false,
isSelected: false,
isSelectionActive: false,
onTap: (document) {
DocumentDetailsRoute($extra: document)
.push(context);
},
onSelected: null,
),
],
);
}
},
error: () => Text(S.of(context)!.couldNotLoadSavedViews),
orElse: () => const Center(
child: CircularProgressIndicator(),
).paddedOnly(top: 8, bottom: 24),
);
return switch (state) {
LoadedSavedViewPreviewState(documents: var documents) =>
Builder(
builder: (context) {
if (documents.isEmpty) {
return Text(S.of(context)!.noDocumentsFound)
.padded();
} else {
return Column(
children: [
for (final document in documents)
DocumentListItem(
document: document,
isLabelClickable: false,
isSelected: false,
isSelectionActive: false,
onTap: (document) {
DocumentDetailsRoute($extra: document)
.push(context);
},
onSelected: null,
),
],
);
}
},
),
ErrorSavedViewPreviewState() =>
Text(S.of(context)!.couldNotLoadSavedViews).padded(16),
OfflineSavedViewPreviewState() =>
Text(S.of(context)!.youAreCurrentlyOffline).padded(16),
_ => const CircularProgressIndicator()
.paddedOnly(top: 8, bottom: 24),
};
},
),
Row(

View File

@@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart';
import 'package:paperless_api/paperless_api.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/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
@@ -10,7 +11,8 @@ part 'similar_documents_state.dart';
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
with DocumentPagingBlocMixin {
final int documentId;
@override
final ConnectivityStatusService connectivityStatusService;
@override
final PaperlessDocumentsApi api;
@@ -22,7 +24,8 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
SimilarDocumentsCubit(
this.api,
this.notifier,
this._labelRepository, {
this._labelRepository,
this.connectivityStatusService, {
required this.documentId,
}) : super(const SimilarDocumentsState(filter: DocumentFilter())) {
notifier.addListener(

View File

@@ -0,0 +1,63 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
typedef OfflineBuilder = Widget Function(BuildContext context, Widget? child);
class ConnectivityAwareActionWrapper extends StatelessWidget {
final OfflineBuilder offlineBuilder;
final Widget child;
final bool disabled;
static Widget disabledBuilder(BuildContext context, Widget? child) {
return ColorFiltered(
colorFilter: const ColorFilter.matrix([
0.2126, 0.7152, 0.0722, 0, 0, //
0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 1, 0,
]),
child: child,
);
}
///
/// Wrapper widget which is used to disable an actionable [child]
/// (like buttons, chips etc.) which require a connection to the internet.
///
///
const ConnectivityAwareActionWrapper({
super.key,
this.offlineBuilder = ConnectivityAwareActionWrapper.disabledBuilder,
required this.child,
this.disabled = false,
});
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: context.read<ConnectivityStatusService>().connectivityChanges(),
builder: (context, snapshot) {
final disableButton =
!snapshot.hasData || snapshot.data == false || disabled;
if (disableButton) {
return GestureDetector(
onTap: () {
HapticFeedback.heavyImpact();
showSnackBar(context, S.of(context)!.youAreCurrentlyOffline);
},
child: AbsorbPointer(
child: offlineBuilder(context, child),
),
);
}
return child;
},
);
}
}

View File

@@ -2,6 +2,7 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
class SnackBarActionConfig {
@@ -108,3 +109,15 @@ void showErrorMessage(
time: DateTime.now(),
);
}
void showInfoMessage(
BuildContext context,
InfoMessageException error, [
StackTrace? stackTrace,
]) {
showSnackBar(
context,
translateError(context, error.code),
details: error.message,
);
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Alle anzeigen",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "Dieser Nutzer existiert bereits.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -969,5 +969,9 @@
"showAll": "Show all",
"@showAll": {
"description": "Button label shown on a saved view preview to open this view in the documents page"
},
"userAlreadyExists": "This user already exists.",
"@userAlreadyExists": {
"description": "Error message shown when the user tries to add an already existing account."
}
}

View File

@@ -6,7 +6,6 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
@@ -29,16 +28,17 @@ 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_impl.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/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
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/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.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/typed/branches/documents_route.dart';
import 'package:paperless_mobile/routes/typed/branches/inbox_route.dart';
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
@@ -103,17 +103,19 @@ void main() async {
await findSystemLocale();
packageInfo = await PackageInfo.fromPlatform();
if (Platform.isAndroid) {
androidInfo = await DeviceInfoPlugin().androidInfo;
}
if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo;
}
final connectivity = Connectivity();
final localAuthentication = LocalAuthentication();
final connectivityStatusService =
ConnectivityStatusServiceImpl(connectivity);
final localAuthService = LocalAuthenticationService(localAuthentication);
final connectivityStatusService = ConnectivityStatusServiceImpl(
Connectivity(),
);
final localAuthService = LocalAuthenticationService(
LocalAuthentication(),
);
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
@@ -145,8 +147,12 @@ void main() async {
});
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
final authenticationCubit =
AuthenticationCubit(localAuthService, apiFactory, sessionManager);
final authenticationCubit = AuthenticationCubit(
localAuthService,
apiFactory,
sessionManager,
connectivityStatusService,
);
await authenticationCubit.restoreSessionState();
runApp(
@@ -154,7 +160,6 @@ void main() async {
providers: [
ChangeNotifierProvider.value(value: sessionManager),
Provider<LocalAuthenticationService>.value(value: localAuthService),
Provider<Connectivity>.value(value: connectivity),
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService),
Provider<LocalNotificationService>.value(
@@ -171,6 +176,7 @@ void main() async {
),
);
}, (error, stack) {
// Catches all unexpected/uncaught errors and prints them to the console.
String message = switch (error) {
PaperlessApiException e => e.details ?? error.toString(),
ServerMessageException e => e.message,
@@ -271,12 +277,22 @@ class _GoRouterShellState extends State<GoRouterShell> {
Widget build(BuildContext context) {
return BlocListener<AuthenticationCubit, AuthenticationState>(
listener: (context, state) {
state.when(
unauthenticated: () => _router.goNamed(R.login),
requriresLocalAuthentication: () => _router.goNamed(R.verifyIdentity),
switchingAccounts: () => _router.goNamed(R.switchingAccounts),
authenticated: (localUserId) => _router.goNamed(R.landing),
);
switch (state) {
case UnauthenticatedState():
const LoginRoute().go(context);
break;
case RequiresLocalAuthenticationState():
const VerifyIdentityRoute().go(context);
break;
case SwitchingAccountsState():
const SwitchingAccountsRoute().go(context);
break;
case AuthenticatedState():
const LandingRoute().go(context);
break;
case AuthenticationErrorState():
}
},
child: GlobalSettingsBuilder(
builder: (context, settings) {

View File

@@ -100,6 +100,7 @@ class LinkedDocumentsRoute extends GoRouteData {
context.read(),
context.read(),
context.read(),
context.read(),
),
child: const LinkedDocumentsPage(),
);

View File

@@ -1,13 +1,13 @@
library mock_server;
export 'response_delay_generator.dart';
export 'response_delay_factory.dart';
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:mock_server/english_words.dart';
import 'package:mock_server/response_delay_generator.dart';
import 'package:mock_server/response_delay_factory.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart' as shelf_router;
@@ -22,15 +22,17 @@ class LocalMockApiServer {
static get baseUrl => 'http://$host:$port/';
final DelayGenerator _delayGenerator;
final ResponseDelayFactory _delayGenerator;
late shelf_router.Router app;
Future<Map<String, dynamic>> loadFixture(String name) async {
var fixture = await rootBundle.loadString('packages/mock_server/fixtures/$name.json');
var fixture =
await rootBundle.loadString('packages/mock_server/fixtures/$name.json');
return json.decode(fixture);
}
LocalMockApiServer([this._delayGenerator = const ZeroDelayGenerator()]) {
LocalMockApiServer(
[this._delayGenerator = const ZeroResponseDelayFactory()]) {
app = shelf_router.Router();
Map<String, dynamic> createdTags = {};
@@ -44,7 +46,8 @@ class LocalMockApiServer {
log.info('Responding to /api/token/');
var body = await req.bodyJsonMap();
if (body?['username'] == 'admin' && body?['password'] == 'test') {
return JsonMockResponse.ok({'token': 'testToken'}, _delayGenerator.nextDelay());
return JsonMockResponse.ok(
{'token': 'testToken'}, _delayGenerator.nextDelay());
} else {
return Response.unauthorized('Unauthorized');
}
@@ -149,9 +152,13 @@ class LocalMockApiServer {
app.delete('/api/tags/<tagId>/', (Request req, String tagId) async {
log.info('Responding to PUT /api/tags/<tagId>/');
(createdTags['results'] as List<dynamic>).removeWhere((element) => element['id'] == tagId);
(createdTags['results'] as List<dynamic>)
.removeWhere((element) => element['id'] == tagId);
return Response(204,
body: null, headers: {'Content-Type': 'application/json'}, encoding: null, context: null);
body: null,
headers: {'Content-Type': 'application/json'},
encoding: null,
context: null);
});
app.get('/api/storage_paths/', (Request req) async {
@@ -180,7 +187,8 @@ class LocalMockApiServer {
app.get('/api/documents/<docId>/thumb/', (Request req, String docId) async {
log.info('Responding to /api/documents/<docId>/thumb/');
var thumb = await rootBundle.load('packages/mock_server/fixtures/lorem-ipsum.png');
var thumb = await rootBundle
.load('packages/mock_server/fixtures/lorem-ipsum.png');
try {
var resp = Response.ok(
http.ByteStream.fromBytes(thumb.buffer.asInt8List()),
@@ -192,14 +200,16 @@ class LocalMockApiServer {
}
});
app.get('/api/documents/<docId>/metadata/', (Request req, String docId) async {
app.get('/api/documents/<docId>/metadata/',
(Request req, String docId) async {
log.info('Responding to /api/documents/<docId>/metadata/');
var data = await loadFixture('metadata');
return JsonMockResponse.ok(data, _delayGenerator.nextDelay());
});
//This is not yet used in the app
app.get('/api/documents/<docId>/suggestions/', (Request req, String docId) async {
app.get('/api/documents/<docId>/suggestions/',
(Request req, String docId) async {
log.info('Responding to /api/documents/<docId>/suggestions/');
var data = await loadFixture('suggestions');
return JsonMockResponse.ok(data, _delayGenerator.nextDelay());
@@ -235,7 +245,10 @@ class LocalMockApiServer {
final term = req.url.queryParameters["term"] ?? '';
final limit = int.parse(req.url.queryParameters["limit"] ?? '5');
return JsonMockResponse.ok(
mostFrequentWords.where((element) => element.startsWith(term)).take(limit).toList(),
mostFrequentWords
.where((element) => element.startsWith(term))
.take(limit)
.toList(),
_delayGenerator.nextDelay(),
);
});

View File

@@ -1,10 +1,10 @@
import 'dart:math';
abstract interface class DelayGenerator {
abstract interface class ResponseDelayFactory {
Duration nextDelay();
}
class RandomDelayGenerator implements DelayGenerator {
class RandomResponseDelayFactory implements ResponseDelayFactory {
/// Minimum allowed response delay
final Duration minDelay;
@@ -12,7 +12,7 @@ class RandomDelayGenerator implements DelayGenerator {
final Duration maxDelay;
final Random _random = Random();
RandomDelayGenerator(this.minDelay, this.maxDelay);
RandomResponseDelayFactory(this.minDelay, this.maxDelay);
@override
Duration nextDelay() {
@@ -25,10 +25,10 @@ class RandomDelayGenerator implements DelayGenerator {
}
}
class ConstantDelayGenerator implements DelayGenerator {
class ConstantResponseDelayFactory implements ResponseDelayFactory {
final Duration delay;
const ConstantDelayGenerator(this.delay);
const ConstantResponseDelayFactory(this.delay);
@override
Duration nextDelay() {
@@ -36,8 +36,8 @@ class ConstantDelayGenerator implements DelayGenerator {
}
}
class ZeroDelayGenerator implements DelayGenerator {
const ZeroDelayGenerator();
class ZeroResponseDelayFactory implements ResponseDelayFactory {
const ZeroResponseDelayFactory();
@override
Duration nextDelay() {

View File

@@ -51,8 +51,8 @@ class PagedSearchResult<T> extends Equatable {
const PagedSearchResult({
required this.count,
required this.next,
required this.previous,
this.next,
this.previous,
required this.results,
});

View File

@@ -67,5 +67,6 @@ enum ErrorCode {
uiSettingsLoadFailed,
loadTasksError,
userNotFound,
userAlreadyExists,
updateSavedViewError;
}