mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 03:15:49 -06:00
Feat: Update scanner persistence, more migrations and bugfixes
This commit is contained in:
@@ -39,8 +39,6 @@ class DioHttpErrorInterceptor extends Interceptor {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return handler.next(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
lib/core/model/info_message_exception.dart
Normal file
12
lib/core/model/info_message_exception.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ Future<void> pushSavedViewDetailsRoute(
|
|||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
LocalUserAppState.current,
|
LocalUserAppState.current,
|
||||||
|
context.read(),
|
||||||
savedView: savedView,
|
savedView: savedView,
|
||||||
),
|
),
|
||||||
child: SavedViewDetailsPage(
|
child: SavedViewDetailsPage(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.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/dio_unauthorized_interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
@@ -37,6 +38,7 @@ class SessionManager extends ValueNotifier<Dio> {
|
|||||||
...interceptors,
|
...interceptors,
|
||||||
DioUnauthorizedInterceptor(),
|
DioUnauthorizedInterceptor(),
|
||||||
DioHttpErrorInterceptor(),
|
DioHttpErrorInterceptor(),
|
||||||
|
DioOfflineInterceptor(),
|
||||||
PrettyDioLogger(
|
PrettyDioLogger(
|
||||||
compact: true,
|
compact: true,
|
||||||
responseBody: false,
|
responseBody: false,
|
||||||
|
|||||||
@@ -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/core/security/session_manager.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/reachability_status.dart';
|
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
|
||||||
|
import 'package:rxdart/subjects.dart';
|
||||||
|
|
||||||
abstract class ConnectivityStatusService {
|
abstract class ConnectivityStatusService {
|
||||||
Future<bool> isConnectedToInternet();
|
Future<bool> isConnectedToInternet();
|
||||||
@@ -20,14 +21,19 @@ abstract class ConnectivityStatusService {
|
|||||||
|
|
||||||
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||||
final Connectivity _connectivity;
|
final Connectivity _connectivity;
|
||||||
|
final BehaviorSubject<bool> _connectivityState$ = BehaviorSubject();
|
||||||
|
|
||||||
ConnectivityStatusServiceImpl(this._connectivity);
|
ConnectivityStatusServiceImpl(this._connectivity) {
|
||||||
|
_connectivityState$.addStream(
|
||||||
|
_connectivity.onConnectivityChanged
|
||||||
|
.map(_hasActiveInternetConnection)
|
||||||
|
.asBroadcastStream(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<bool> connectivityChanges() {
|
Stream<bool> connectivityChanges() {
|
||||||
return _connectivity.onConnectivityChanged
|
return _connectivityState$.asBroadcastStream();
|
||||||
.map(_hasActiveInternetConnection)
|
|
||||||
.asBroadcastStream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -98,3 +104,31 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
|||||||
return ReachabilityStatus.notReachable;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class FileService {
|
|||||||
case PaperlessDirectoryType.temporary:
|
case PaperlessDirectoryType.temporary:
|
||||||
return temporaryDirectory;
|
return temporaryDirectory;
|
||||||
case PaperlessDirectoryType.scans:
|
case PaperlessDirectoryType.scans:
|
||||||
return scanDirectory;
|
return temporaryScansDirectory;
|
||||||
case PaperlessDirectoryType.download:
|
case PaperlessDirectoryType.download:
|
||||||
return downloadsDirectory;
|
return downloadsDirectory;
|
||||||
}
|
}
|
||||||
@@ -52,8 +52,7 @@ class FileService {
|
|||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
final dir = Directory('${appDir.path}/documents');
|
final dir = Directory('${appDir.path}/documents');
|
||||||
dir.createSync();
|
return dir.create(recursive: true);
|
||||||
return dir;
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedError("Platform not supported.");
|
throw UnsupportedError("Platform not supported.");
|
||||||
}
|
}
|
||||||
@@ -72,33 +71,22 @@ class FileService {
|
|||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
final dir = Directory('${appDir.path}/downloads');
|
final dir = Directory('${appDir.path}/downloads');
|
||||||
dir.createSync();
|
return dir.create(recursive: true);
|
||||||
return dir;
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedError("Platform not supported.");
|
throw UnsupportedError("Platform not supported.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Directory?> get scanDirectory async {
|
static Future<Directory> get temporaryScansDirectory async {
|
||||||
if (Platform.isAndroid) {
|
final tempDir = await temporaryDirectory;
|
||||||
final scanDir = await getExternalStorageDirectories(
|
final scansDir = Directory('${tempDir.path}/scans');
|
||||||
type: StorageDirectory.dcim,
|
return scansDir.create(recursive: true);
|
||||||
);
|
|
||||||
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<void> clearUserData() async {
|
static Future<void> clearUserData() async {
|
||||||
final scanDir = await scanDirectory;
|
final scanDir = await temporaryScansDirectory;
|
||||||
final tempDir = await temporaryDirectory;
|
final tempDir = await temporaryDirectory;
|
||||||
await scanDir?.delete(recursive: true);
|
await scanDir.delete(recursive: true);
|
||||||
await tempDir.delete(recursive: true);
|
await tempDir.delete(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,5 +75,6 @@ String translateError(BuildContext context, ErrorCode code) {
|
|||||||
ErrorCode.loadTasksError => S.of(context)!.couldNotLoadTasks,
|
ErrorCode.loadTasksError => S.of(context)!.couldNotLoadTasks,
|
||||||
ErrorCode.userNotFound => S.of(context)!.userNotFound,
|
ErrorCode.userNotFound => S.of(context)!.userNotFound,
|
||||||
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
|
ErrorCode.updateSavedViewError => S.of(context)!.couldNotUpdateSavedView,
|
||||||
|
ErrorCode.userAlreadyExists => S.of(context)!.userAlreadyExists,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@@ -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/cubit/similar_documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/similar_documents/view/similar_documents_view.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/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/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.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(),
|
||||||
context.read(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
documentId: state.document.id,
|
documentId: state.document.id,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -322,28 +325,45 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
final isConnected = connectivityState.isConnected;
|
|
||||||
final currentUser = context.watch<LocalUserAccount>();
|
final currentUser = context.watch<LocalUserAccount>();
|
||||||
final canDelete =
|
|
||||||
isConnected && currentUser.paperlessUser.canDeleteDocuments;
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
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,
|
tooltip: S.of(context)!.deleteDocumentTooltip,
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed:
|
onPressed: () => _onDelete(state.document),
|
||||||
canDelete ? () => _onDelete(state.document) : null,
|
|
||||||
).paddedSymmetrically(horizontal: 4),
|
).paddedSymmetrically(horizontal: 4),
|
||||||
DocumentDownloadButton(
|
|
||||||
document: state.document,
|
|
||||||
enabled: isConnected,
|
|
||||||
),
|
),
|
||||||
IconButton(
|
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,
|
tooltip: S.of(context)!.openInSystemViewer,
|
||||||
icon: const Icon(Icons.open_in_new),
|
icon: const Icon(Icons.open_in_new),
|
||||||
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
|
onPressed: _onOpenFileInSystemViewer,
|
||||||
).paddedOnly(right: 4.0),
|
).paddedOnly(right: 4.0),
|
||||||
|
),
|
||||||
DocumentShareButton(document: state.document),
|
DocumentShareButton(document: state.document),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: S.of(context)!.print,
|
tooltip: S.of(context)!.print,
|
||||||
|
|||||||
@@ -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/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/file_download_type.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/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/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
@@ -34,7 +35,12 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) => const IconButton(
|
||||||
|
icon: Icon(Icons.share),
|
||||||
|
onPressed: null,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
tooltip: S.of(context)!.shareTooltip,
|
tooltip: S.of(context)!.shareTooltip,
|
||||||
icon: _isDownloadPending
|
icon: _isDownloadPending
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
@@ -46,7 +52,8 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
|
|||||||
onPressed: widget.document != null && widget.enabled
|
onPressed: widget.document != null && widget.enabled
|
||||||
? () => _onShare(widget.document!)
|
? () => _onShare(widget.document!)
|
||||||
: null,
|
: null,
|
||||||
).paddedOnly(right: 4);
|
).paddedOnly(right: 4),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onShare(DocumentModel document) async {
|
Future<void> _onShare(DocumentModel document) async {
|
||||||
|
|||||||
@@ -1,43 +1,71 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.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:paperless_api/paperless_api.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/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:rxdart/rxdart.dart';
|
||||||
|
|
||||||
class DocumentScannerCubit extends Cubit<List<File>> {
|
part 'document_scanner_state.dart';
|
||||||
|
|
||||||
|
class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||||
final LocalNotificationService _notificationService;
|
final LocalNotificationService _notificationService;
|
||||||
|
|
||||||
DocumentScannerCubit(this._notificationService) : super(const []);
|
DocumentScannerCubit(this._notificationService)
|
||||||
|
: super(const InitialDocumentScannerState());
|
||||||
|
|
||||||
void addScan(File file) => emit([...state, file]);
|
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 removeScan(int fileIndex) {
|
void addScan(File file) async {
|
||||||
|
emit(LoadedDocumentScannerState(
|
||||||
|
scans: [...state.scans, file],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeScan(File file) async {
|
||||||
try {
|
try {
|
||||||
state[fileIndex].deleteSync();
|
await file.delete();
|
||||||
final scans = [...state];
|
} catch (error, stackTrace) {
|
||||||
scans.removeAt(fileIndex);
|
throw InfoMessageException(
|
||||||
emit(scans);
|
code: ErrorCode.scanRemoveFailed,
|
||||||
} catch (_) {
|
message: error.toString(),
|
||||||
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
final scans = state.scans..remove(file);
|
||||||
|
emit(
|
||||||
|
scans.isEmpty
|
||||||
|
? const InitialDocumentScannerState()
|
||||||
|
: LoadedDocumentScannerState(scans: scans),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
Future<void> reset() async {
|
||||||
try {
|
try {
|
||||||
for (final doc in state) {
|
Future.wait([
|
||||||
doc.deleteSync();
|
for (final file in state.scans) file.delete(),
|
||||||
if (kDebugMode) {
|
]);
|
||||||
log('[ScannerCubit]: Removed ${doc.path}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageCache.clear();
|
imageCache.clear();
|
||||||
emit([]);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
|
||||||
|
} finally {
|
||||||
|
emit(const InitialDocumentScannerState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
lib/features/document_scan/cubit/document_scanner_state.dart
Normal file
30
lib/features/document_scan/cubit/document_scanner_state.dart
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import 'package:flutter_bloc/flutter_bloc.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/constants.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/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';
|
||||||
@@ -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/documents/view/pages/document_view.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.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/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/message_helpers.dart';
|
||||||
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
||||||
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
||||||
@@ -52,10 +52,6 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
|
||||||
builder: (context, connectedState) {
|
|
||||||
return BlocBuilder<DocumentScannerCubit, List<File>>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
top: true,
|
top: true,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
@@ -77,41 +73,33 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
SliverOverlapAbsorber(
|
SliverOverlapAbsorber(
|
||||||
handle: actionsHandle,
|
handle: actionsHandle,
|
||||||
sliver: SliverPinnedHeader(
|
sliver: SliverPinnedHeader(
|
||||||
child: _buildActions(connectedState.isConnected),
|
child: _buildActions(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
body: BlocBuilder<DocumentScannerCubit, List<File>>(
|
body: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isEmpty) {
|
return switch (state) {
|
||||||
return SizedBox.expand(
|
InitialDocumentScannerState() => _buildEmptyState(),
|
||||||
child: Center(
|
RestoringDocumentScannerState() => Center(
|
||||||
child: _buildEmptyState(
|
child: Text("Restoring..."),
|
||||||
connectedState.isConnected,
|
|
||||||
state,
|
|
||||||
),
|
),
|
||||||
),
|
LoadedDocumentScannerState() => _buildImageGrid(state.scans),
|
||||||
);
|
ErrorDocumentScannerState() => Placeholder(),
|
||||||
} else {
|
};
|
||||||
return _buildImageGrid(state);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActions(bool isConnected) {
|
Widget _buildActions() {
|
||||||
return ColoredBox(
|
return ColoredBox(
|
||||||
color: Theme.of(context).colorScheme.background,
|
color: Theme.of(context).colorScheme.background,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: kTextTabBarHeight,
|
height: kTextTabBarHeight,
|
||||||
child: BlocBuilder<DocumentScannerCubit, List<File>>(
|
child: BlocBuilder<DocumentScannerCubit, DocumentScannerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return RawScrollbar(
|
return RawScrollbar(
|
||||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||||
@@ -130,12 +118,12 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
),
|
),
|
||||||
onPressed: state.isNotEmpty
|
onPressed: state.scans.isNotEmpty
|
||||||
? () => Navigator.of(context).push(
|
? () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => DocumentView(
|
builder: (context) => DocumentView(
|
||||||
documentBytes: _assembleFileBytes(
|
documentBytes: _assembleFileBytes(
|
||||||
state,
|
state.scans,
|
||||||
forcePdf: true,
|
forcePdf: true,
|
||||||
).then((file) => file.bytes),
|
).then((file) => file.bytes),
|
||||||
),
|
),
|
||||||
@@ -150,19 +138,32 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
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),
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
TextButton.icon(
|
ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) {
|
||||||
|
return TextButton.icon(
|
||||||
label: Text(S.of(context)!.upload),
|
label: Text(S.of(context)!.upload),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
||||||
),
|
),
|
||||||
onPressed: state.isEmpty || !isConnected
|
onPressed: null,
|
||||||
? null
|
|
||||||
: () => _onPrepareDocumentUpload(context),
|
|
||||||
icon: const Icon(Icons.upload_outlined),
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
@@ -170,7 +171,7 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.fromLTRB(5, 10, 5, 10),
|
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),
|
icon: const Icon(Icons.save_alt_outlined),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
@@ -192,7 +193,7 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
final cubit = context.read<DocumentScannerCubit>();
|
final cubit = context.read<DocumentScannerCubit>();
|
||||||
final file = await _assembleFileBytes(
|
final file = await _assembleFileBytes(
|
||||||
forcePdf: true,
|
forcePdf: true,
|
||||||
context.read<DocumentScannerCubit>().state,
|
context.read<DocumentScannerCubit>().state.scans,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
final globalSettings =
|
final globalSettings =
|
||||||
@@ -249,9 +250,9 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
context.read<DocumentScannerCubit>().addScan(file);
|
context.read<DocumentScannerCubit>().addScan(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPrepareDocumentUpload(BuildContext context) async {
|
void _onPrepareDocumentUpload(BuildContext context, List<File> scans) async {
|
||||||
final file = await _assembleFileBytes(
|
final file = await _assembleFileBytes(
|
||||||
context.read<DocumentScannerCubit>().state,
|
scans,
|
||||||
forcePdf: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
forcePdf: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
|
||||||
.getValue()!
|
.getValue()!
|
||||||
.enforceSinglePagePdfUpload,
|
.enforceSinglePagePdfUpload,
|
||||||
@@ -269,10 +270,7 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(bool isConnected, List<File> scans) {
|
Widget _buildEmptyState() {
|
||||||
if (scans.isNotEmpty) {
|
|
||||||
return _buildImageGrid(scans);
|
|
||||||
}
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@@ -288,9 +286,15 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
onPressed: () => _openDocumentScanner(context),
|
onPressed: () => _openDocumentScanner(context),
|
||||||
),
|
),
|
||||||
Text(S.of(context)!.or),
|
Text(S.of(context)!.or),
|
||||||
TextButton(
|
ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) => TextButton(
|
||||||
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
|
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
|
||||||
onPressed: isConnected ? _onUploadFromFilesystem : null,
|
onPressed: null,
|
||||||
|
),
|
||||||
|
child: TextButton(
|
||||||
|
child: Text(S.of(context)!.uploadADocumentFromThisDevice),
|
||||||
|
onPressed: _onUploadFromFilesystem,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -318,7 +322,9 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
file: scans[index],
|
file: scans[index],
|
||||||
onDelete: () async {
|
onDelete: () async {
|
||||||
try {
|
try {
|
||||||
context.read<DocumentScannerCubit>().removeScan(index);
|
context
|
||||||
|
.read<DocumentScannerCubit>()
|
||||||
|
.removeScan(scans[index]);
|
||||||
} on PaperlessApiException catch (error, stackTrace) {
|
} on PaperlessApiException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
import 'package:paperless_api/paperless_api.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/database/tables/local_user_app_state.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/features/paged_document_view/cubit/document_paging_bloc_mixin.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/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
@@ -119,4 +120,9 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
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';
|
||||||
|
|
||||||
part 'document_upload_state.dart';
|
part 'document_upload_state.dart';
|
||||||
|
|
||||||
@@ -13,12 +13,12 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
final PaperlessDocumentsApi _documentApi;
|
final PaperlessDocumentsApi _documentApi;
|
||||||
|
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
final Connectivity _connectivity;
|
final ConnectivityStatusService _connectivityStatusService;
|
||||||
|
|
||||||
DocumentUploadCubit(
|
DocumentUploadCubit(
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this._documentApi,
|
this._documentApi,
|
||||||
this._connectivity,
|
this._connectivityStatusService,
|
||||||
) : super(const DocumentUploadState()) {
|
) : super(const DocumentUploadState()) {
|
||||||
_labelRepository.addListener(
|
_labelRepository.addListener(
|
||||||
this,
|
this,
|
||||||
|
|||||||
@@ -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/database/tables/local_user_app_state.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/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/document_paging_bloc_mixin.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
@@ -20,6 +21,8 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
@@ -31,6 +34,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
|||||||
this.notifier,
|
this.notifier,
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this._userState,
|
this._userState,
|
||||||
|
this.connectivityStatusService,
|
||||||
) : super(DocumentsState(
|
) : super(DocumentsState(
|
||||||
filter: _userState.currentDocumentFilter,
|
filter: _userState.currentDocumentFilter,
|
||||||
viewType: _userState.documentsPageViewType,
|
viewType: _userState.documentsPageViewType,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter/services.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/bloc/connectivity_cubit.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/extensions/flutter_extensions.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_search/view/sliver_search_bar.dart';
|
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||||
@@ -333,12 +334,16 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverOverlapInjector(handle: searchBarHandle),
|
SliverOverlapInjector(handle: searchBarHandle),
|
||||||
SliverOverlapInjector(handle: savedViewsHandle),
|
SliverOverlapInjector(handle: savedViewsHandle),
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
SliverToBoxAdapter(
|
||||||
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
buildWhen: (previous, current) =>
|
buildWhen: (previous, current) =>
|
||||||
previous.filter != current.filter,
|
previous.filter != current.filter,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SliverToBoxAdapter(
|
final currentUser = context.watch<LocalUserAccount>();
|
||||||
child: SavedViewsWidget(
|
if (!currentUser.paperlessUser.canViewSavedViews) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return SavedViewsWidget(
|
||||||
controller: _savedViewsExpansionController,
|
controller: _savedViewsExpansionController,
|
||||||
onViewSelected: (view) {
|
onViewSelected: (view) {
|
||||||
final cubit = context.read<DocumentsCubit>();
|
final cubit = context.read<DocumentsCubit>();
|
||||||
@@ -372,10 +377,10 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
filter: state.filter,
|
filter: state.filter,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
if (state.hasLoaded && state.documents.isEmpty) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:paperless_api/paperless_api.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:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
@@ -28,11 +29,10 @@ class DocumentPreview extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return ConnectivityAwareActionWrapper(
|
||||||
|
child: GestureDetector(
|
||||||
onTap: isClickable
|
onTap: isClickable
|
||||||
? () {
|
? () => DocumentPreviewRoute($extra: document).push(context)
|
||||||
DocumentPreviewRoute($extra: document).push(context);
|
|
||||||
}
|
|
||||||
: null,
|
: null,
|
||||||
child: HeroMode(
|
child: HeroMode(
|
||||||
enabled: enableHero,
|
enabled: enableHero,
|
||||||
@@ -41,6 +41,7 @@ class DocumentPreview extends StatelessWidget {
|
|||||||
child: _buildPreview(context),
|
child: _buildPreview(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/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/features/saved_view/cubit/saved_view_cubit.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/routes/typed/branches/saved_views_route.dart';
|
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||||
|
|
||||||
class SavedViewsWidget extends StatefulWidget {
|
class SavedViewsWidget extends StatefulWidget {
|
||||||
@@ -146,7 +147,8 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
|||||||
final isSelected =
|
final isSelected =
|
||||||
(widget.filter.selectedView ?? -1) ==
|
(widget.filter.selectedView ?? -1) ==
|
||||||
view.id;
|
view.id;
|
||||||
return SavedViewChip(
|
return ConnectivityAwareActionWrapper(
|
||||||
|
child: SavedViewChip(
|
||||||
view: view,
|
view: view,
|
||||||
onViewSelected: widget.onViewSelected,
|
onViewSelected: widget.onViewSelected,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
@@ -155,6 +157,7 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
|||||||
widget.filter,
|
widget.filter,
|
||||||
onUpdateView: widget.onUpdateView,
|
onUpdateView: widget.onUpdateView,
|
||||||
onDeleteView: widget.onDeleteView,
|
onDeleteView: widget.onDeleteView,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) =>
|
separatorBuilder: (context, index) =>
|
||||||
@@ -178,6 +181,7 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: S.of(context)!.createFromCurrentFilter,
|
message: S.of(context)!.createFromCurrentFilter,
|
||||||
|
child: ConnectivityAwareActionWrapper(
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
CreateSavedViewRoute(widget.filter).push(context);
|
CreateSavedViewRoute(widget.filter).push(context);
|
||||||
@@ -185,6 +189,7 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
|||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: Text(S.of(context)!.newView),
|
label: Text(S.of(context)!.newView),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
).padded(4),
|
).padded(4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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/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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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/cubit/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.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/features/labels/cubit/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/connectivity_aware_action_wrapper.dart';
|
||||||
|
|
||||||
class SortDocumentsButton extends StatelessWidget {
|
class SortDocumentsButton extends StatelessWidget {
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
@@ -20,12 +21,21 @@ class SortDocumentsButton extends StatelessWidget {
|
|||||||
if (state.filter.sortField == null) {
|
if (state.filter.sortField == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
print(state.filter.sortField);
|
final icon = Icon(state.filter.sortOrder == SortOrder.ascending
|
||||||
return TextButton.icon(
|
|
||||||
icon: Icon(state.filter.sortOrder == SortOrder.ascending
|
|
||||||
? Icons.arrow_upward
|
? Icons.arrow_upward
|
||||||
: Icons.arrow_downward),
|
: Icons.arrow_downward);
|
||||||
label: Text(translateSortField(context, state.filter.sortField)),
|
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
|
onPressed: enabled
|
||||||
? () {
|
? () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
@@ -69,6 +79,7 @@ class SortDocumentsButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -160,11 +160,13 @@ class HomeShellWidget extends StatelessWidget {
|
|||||||
Hive.box<LocalUserAppState>(
|
Hive.box<LocalUserAppState>(
|
||||||
HiveBoxes.localUserAppState)
|
HiveBoxes.localUserAppState)
|
||||||
.get(currentUserId)!,
|
.get(currentUserId)!,
|
||||||
|
context.read(),
|
||||||
)..initialize(),
|
)..initialize(),
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
DocumentScannerCubit(context.read()),
|
DocumentScannerCubit(context.read())
|
||||||
|
..initialize(),
|
||||||
),
|
),
|
||||||
Provider(
|
Provider(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
@@ -173,6 +175,7 @@ class HomeShellWidget extends StatelessWidget {
|
|||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
);
|
);
|
||||||
if (currentLocalUser
|
if (currentLocalUser
|
||||||
.paperlessUser.canViewDocuments &&
|
.paperlessUser.canViewDocuments &&
|
||||||
|
|||||||
@@ -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/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/repository/label_repository_state.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/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.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 LabelRepository _labelRepository;
|
||||||
|
|
||||||
final PaperlessDocumentsApi _documentsApi;
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
this._statsApi,
|
this._statsApi,
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
|
this.connectivityStatusService,
|
||||||
) : super(InboxState(
|
) : super(InboxState(
|
||||||
labels: _labelRepository.state,
|
labels: _labelRepository.state,
|
||||||
)) {
|
)) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:intl/intl.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/exception/server_message_exception.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_cancel_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.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/inbox/view/widgets/inbox_item.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/view/document_paging_view_mixin.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/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/message_helpers.dart';
|
||||||
|
|
||||||
class InboxPage extends StatefulWidget {
|
class InboxPage extends StatefulWidget {
|
||||||
@@ -74,9 +76,13 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
|
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
|
floatingActionButton: ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) => const SizedBox.shrink(),
|
||||||
|
child: BlocBuilder<InboxCubit, InboxState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (!state.hasLoaded || state.documents.isEmpty || !canEditDocument) {
|
if (!state.hasLoaded ||
|
||||||
|
state.documents.isEmpty ||
|
||||||
|
!canEditDocument) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return FloatingActionButton.extended(
|
return FloatingActionButton.extended(
|
||||||
@@ -114,6 +120,7 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: true,
|
top: true,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
@@ -268,6 +275,12 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
showSnackBar(context, S.of(context)!.missingPermissions);
|
showSnackBar(context, S.of(context)!.missingPermissions);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
final isConnectedToInternet =
|
||||||
|
await context.read<ConnectivityStatusService>().isConnectedToInternet();
|
||||||
|
if (!isConnectedToInternet) {
|
||||||
|
showSnackBar(context, S.of(context)!.youAreCurrentlyOffline);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final removedTags = await context.read<InboxCubit>().removeFromInbox(doc);
|
final removedTags = await context.read<InboxCubit>().removeFromInbox(doc);
|
||||||
showSnackBar(
|
showSnackBar(
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.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: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/core/widgets/shimmer_placeholder.dart';
|
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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/tags/view/widgets/tags_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_text.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/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';
|
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||||
|
|
||||||
class InboxItemPlaceholder extends StatelessWidget {
|
class InboxItemPlaceholder extends StatelessWidget {
|
||||||
@@ -228,8 +227,10 @@ class _InboxItemState extends State<InboxItem> {
|
|||||||
),
|
),
|
||||||
LimitedBox(
|
LimitedBox(
|
||||||
maxHeight: 56,
|
maxHeight: 56,
|
||||||
|
child: ConnectivityAwareActionWrapper(
|
||||||
child: _buildActions(context),
|
child: _buildActions(context),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).paddedOnly(left: 8, top: 8, bottom: 8),
|
).paddedOnly(left: 8, top: 8, bottom: 8),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/cubit/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.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/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';
|
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||||
|
|
||||||
class LabelsPage extends StatefulWidget {
|
class LabelsPage extends StatefulWidget {
|
||||||
@@ -66,22 +67,22 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
.getValue()!
|
.getValue()!
|
||||||
.loggedInUserId;
|
.loggedInUserId;
|
||||||
final user = box.get(currentUserId)!.paperlessUser;
|
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>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectedState) {
|
builder: (context, connectedState) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
drawer: const AppDrawer(),
|
drawer: const AppDrawer(),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: ConnectivityAwareActionWrapper(
|
||||||
|
offlineBuilder: (context, child) => const SizedBox.shrink(),
|
||||||
|
child: FloatingActionButton.extended(
|
||||||
heroTag: "inbox_page_fab",
|
heroTag: "inbox_page_fab",
|
||||||
label: Text(
|
label: Text(fabLabel),
|
||||||
[
|
|
||||||
S.of(context)!.addCorrespondent,
|
|
||||||
S.of(context)!.addDocumentType,
|
|
||||||
S.of(context)!.addTag,
|
|
||||||
S.of(context)!.addStoragePath,
|
|
||||||
][_currentIndex],
|
|
||||||
),
|
|
||||||
icon: Icon(Icons.add),
|
icon: Icon(Icons.add),
|
||||||
onPressed: [
|
onPressed: [
|
||||||
if (user.canViewCorrespondents)
|
if (user.canViewCorrespondents)
|
||||||
@@ -97,6 +98,7 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
.push(context),
|
.push(context),
|
||||||
][_currentIndex],
|
][_currentIndex],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
floatHeaderSlivers: true,
|
floatHeaderSlivers: true,
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
|
|||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
if (!connectivityState.isConnected) {
|
if (!connectivityState.isConnected) {
|
||||||
return const OfflineWidget();
|
return const SliverFillRemaining(child: OfflineWidget());
|
||||||
}
|
}
|
||||||
final sortedLabels = labels.values.toList()..sort();
|
final sortedLabels = labels.values.toList()..sort();
|
||||||
if (labels.isEmpty) {
|
if (labels.isEmpty) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class LandingPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _LandingPageState extends State<LandingPage> {
|
class _LandingPageState extends State<LandingPage> {
|
||||||
final _searchBarHandle = SliverOverlapAbsorberHandle();
|
final _searchBarHandle = SliverOverlapAbsorberHandle();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||||
@@ -121,7 +122,6 @@ class _LandingPageState extends State<LandingPage> {
|
|||||||
|
|
||||||
Widget _buildStatisticsCard(BuildContext context) {
|
Widget _buildStatisticsCard(BuildContext context) {
|
||||||
final currentUser = context.read<LocalUserAccount>().paperlessUser;
|
final currentUser = context.read<LocalUserAccount>().paperlessUser;
|
||||||
|
|
||||||
return ExpansionCard(
|
return ExpansionCard(
|
||||||
initiallyExpanded: false,
|
initiallyExpanded: false,
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:json_annotation/json_annotation.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/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/features/paged_document_view/cubit/paged_documents_state.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/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
@@ -14,7 +15,8 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
|
|||||||
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 LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
|
|||||||
this.api,
|
this.api,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
|
this.connectivityStatusService,
|
||||||
) : super(LinkedDocumentsState(filter: filter)) {
|
) : super(LinkedDocumentsState(filter: filter)) {
|
||||||
updateFilter(filter: filter);
|
updateFilter(filter: filter);
|
||||||
_labelRepository.addListener(
|
_labelRepository.addListener(
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.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';
|
||||||
@@ -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/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/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/features/login/model/client_certificate.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/model/login_form_credentials.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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
part 'authentication_cubit.freezed.dart';
|
|
||||||
part 'authentication_state.dart';
|
part 'authentication_state.dart';
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
AuthenticationCubit(
|
AuthenticationCubit(
|
||||||
this._localAuthService,
|
this._localAuthService,
|
||||||
this._apiFactory,
|
this._apiFactory,
|
||||||
this._sessionManager,
|
this._sessionManager,
|
||||||
) : super(const AuthenticationState.unauthenticated());
|
this._connectivityService,
|
||||||
|
) : super(const UnauthenticatedState());
|
||||||
|
|
||||||
Future<void> login({
|
Future<void> login({
|
||||||
required LoginFormCredentials credentials,
|
required LoginFormCredentials credentials,
|
||||||
@@ -51,8 +56,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
_sessionManager,
|
_sessionManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
final apiVersion = await _getApiVersion(_sessionManager.client);
|
|
||||||
|
|
||||||
// Mark logged in user as currently active user.
|
// Mark logged in user as currently active user.
|
||||||
final globalSettings =
|
final globalSettings =
|
||||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||||
@@ -60,7 +63,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
await globalSettings.save();
|
await globalSettings.save();
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
AuthenticationState.authenticated(
|
AuthenticatedState(
|
||||||
localUserId: localUserId,
|
localUserId: localUserId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -72,11 +75,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
|
|
||||||
/// 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 AuthenticationState.switchingAccounts());
|
emit(const SwitchingAccountsState());
|
||||||
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(AuthenticationState.authenticated(localUserId: localUserId));
|
emit(AuthenticatedState(localUserId: localUserId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final userAccountBox =
|
final userAccountBox =
|
||||||
@@ -125,7 +128,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
apiVersion,
|
apiVersion,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(AuthenticationState.authenticated(
|
emit(AuthenticatedState(
|
||||||
localUserId: localUserId,
|
localUserId: localUserId,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@@ -182,7 +185,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"There is nothing to restore.",
|
"There is nothing to restore.",
|
||||||
);
|
);
|
||||||
// If there is nothing to restore, we can quit here.
|
// If there is nothing to restore, we can quit here.
|
||||||
emit(const AuthenticationState.unauthenticated());
|
emit(const UnauthenticatedState());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final localUserAccountBox =
|
final localUserAccountBox =
|
||||||
@@ -203,7 +206,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
final localAuthSuccess =
|
final localAuthSuccess =
|
||||||
await _localAuthService.authenticateLocalUser(authenticationMesage);
|
await _localAuthService.authenticateLocalUser(authenticationMesage);
|
||||||
if (!localAuthSuccess) {
|
if (!localAuthSuccess) {
|
||||||
emit(const AuthenticationState.requriresLocalAuthentication());
|
emit(const RequiresLocalAuthenticationState());
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"User could not be authenticated.",
|
"User could not be authenticated.",
|
||||||
@@ -239,14 +242,17 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"User should be authenticated but no authentication information was found.",
|
"User should be authenticated but no authentication information was found.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"Authentication credentials successfully retrieved.",
|
"Authentication credentials successfully retrieved.",
|
||||||
);
|
);
|
||||||
|
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"Updating current session state...",
|
"Updating current session state...",
|
||||||
);
|
);
|
||||||
|
|
||||||
_sessionManager.updateSettings(
|
_sessionManager.updateSettings(
|
||||||
clientCertificate: authentication.clientCertificate,
|
clientCertificate: authentication.clientCertificate,
|
||||||
authToken: authentication.token,
|
authToken: authentication.token,
|
||||||
@@ -256,18 +262,32 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"Current session state successfully updated.",
|
"Current session state successfully updated.",
|
||||||
);
|
);
|
||||||
|
final hasInternetConnection =
|
||||||
|
await _connectivityService.isConnectedToInternet();
|
||||||
|
if (hasInternetConnection) {
|
||||||
|
_debugPrintMessage(
|
||||||
|
"restoreSessionMState",
|
||||||
|
"Updating server user...",
|
||||||
|
);
|
||||||
final apiVersion = await _getApiVersion(_sessionManager.client);
|
final apiVersion = await _getApiVersion(_sessionManager.client);
|
||||||
await _updateRemoteUser(
|
await _updateRemoteUser(
|
||||||
_sessionManager,
|
_sessionManager,
|
||||||
localUserAccount,
|
localUserAccount,
|
||||||
apiVersion,
|
apiVersion,
|
||||||
);
|
);
|
||||||
emit(
|
_debugPrintMessage(
|
||||||
AuthenticationState.authenticated(
|
"restoreSessionMState",
|
||||||
localUserId: localUserId,
|
"Successfully updated server user.",
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
_debugPrintMessage(
|
||||||
|
"restoreSessionMState",
|
||||||
|
"Skipping update of server user (no internet connection).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(AuthenticatedState(localUserId: localUserId));
|
||||||
|
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"restoreSessionState",
|
"restoreSessionState",
|
||||||
"Session was successfully restored.",
|
"Session was successfully restored.",
|
||||||
@@ -285,7 +305,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
globalSettings.loggedInUserId = null;
|
globalSettings.loggedInUserId = null;
|
||||||
await globalSettings.save();
|
await globalSettings.save();
|
||||||
|
|
||||||
emit(const AuthenticationState.unauthenticated());
|
emit(const UnauthenticatedState());
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
"logout",
|
"logout",
|
||||||
"User successfully logged out.",
|
"User successfully logged out.",
|
||||||
@@ -353,7 +373,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
|||||||
"_addUser",
|
"_addUser",
|
||||||
"An error occurred! The user $localUserId already exists.",
|
"An error occurred! The user $localUserId already exists.",
|
||||||
);
|
);
|
||||||
throw Exception("User already exists!");
|
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
|
||||||
}
|
}
|
||||||
final apiVersion = await _getApiVersion(sessionManager.client);
|
final apiVersion = await _getApiVersion(sessionManager.client);
|
||||||
_debugPrintMessage(
|
_debugPrintMessage(
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
part of 'authentication_cubit.dart';
|
part of 'authentication_cubit.dart';
|
||||||
|
|
||||||
@freezed
|
sealed class AuthenticationState {
|
||||||
class AuthenticationState with _$AuthenticationState {
|
const AuthenticationState();
|
||||||
const AuthenticationState._();
|
|
||||||
|
|
||||||
const factory AuthenticationState.unauthenticated() = _Unauthenticated;
|
bool get isAuthenticated =>
|
||||||
const factory AuthenticationState.requriresLocalAuthentication() =
|
switch (this) { AuthenticatedState() => true, _ => false };
|
||||||
_RequiresLocalAuthentication;
|
}
|
||||||
const factory AuthenticationState.authenticated({
|
|
||||||
required String localUserId,
|
class UnauthenticatedState extends AuthenticationState {
|
||||||
}) = _Authenticated;
|
const UnauthenticatedState();
|
||||||
const factory AuthenticationState.switchingAccounts() = _SwitchingAccounts;
|
}
|
||||||
|
|
||||||
bool get isAuthenticated => maybeWhen(
|
class RequiresLocalAuthenticationState extends AuthenticationState {
|
||||||
authenticated: (_) => true,
|
const RequiresLocalAuthenticationState();
|
||||||
orElse: () => false,
|
}
|
||||||
);
|
|
||||||
|
class AuthenticatedState extends AuthenticationState {
|
||||||
|
final String localUserId;
|
||||||
|
|
||||||
|
const AuthenticatedState({
|
||||||
|
required this.localUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SwitchingAccountsState extends AuthenticationState {
|
||||||
|
const SwitchingAccountsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationErrorState extends AuthenticationState {
|
||||||
|
const AuthenticationErrorState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/config/hive/hive_config.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/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/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/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';
|
||||||
@@ -153,6 +154,8 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
|||||||
showErrorMessage(context, error);
|
showErrorMessage(context, error);
|
||||||
} on ServerMessageException catch (error) {
|
} on ServerMessageException catch (error) {
|
||||||
showLocalizedError(context, error.message);
|
showLocalizedError(context, error.message);
|
||||||
|
} on InfoMessageException catch (error) {
|
||||||
|
showInfoMessage(context, error);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showGenericError(context, error);
|
showGenericError(context, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
|
|||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onBackground
|
.onBackground
|
||||||
.withOpacity(0.6)),
|
.withOpacity(0.6),
|
||||||
|
),
|
||||||
).padded(16),
|
).padded(16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -64,11 +65,16 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () async {
|
onPressed: !_isLoginLoading
|
||||||
|
? () async {
|
||||||
setState(() => _isLoginLoading = true);
|
setState(() => _isLoginLoading = true);
|
||||||
|
try {
|
||||||
await widget.onSubmit();
|
await widget.onSubmit();
|
||||||
|
} finally {
|
||||||
setState(() => _isLoginLoading = false);
|
setState(() => _isLoginLoading = false);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
child: Text(S.of(context)!.signIn),
|
child: Text(S.of(context)!.signIn),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:collection/collection.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/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:rxdart/streams.dart';
|
||||||
|
|
||||||
import 'paged_documents_state.dart';
|
import 'paged_documents_state.dart';
|
||||||
|
|
||||||
@@ -11,13 +13,16 @@ import 'paged_documents_state.dart';
|
|||||||
///
|
///
|
||||||
mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
||||||
on BlocBase<State> {
|
on BlocBase<State> {
|
||||||
|
ConnectivityStatusService get connectivityStatusService;
|
||||||
PaperlessDocumentsApi get api;
|
PaperlessDocumentsApi get api;
|
||||||
DocumentChangedNotifier get notifier;
|
DocumentChangedNotifier get notifier;
|
||||||
|
|
||||||
Future<void> onFilterUpdated(DocumentFilter filter);
|
Future<void> onFilterUpdated(DocumentFilter filter);
|
||||||
|
|
||||||
Future<void> loadMore() async {
|
Future<void> loadMore() async {
|
||||||
if (state.isLastPageLoaded) {
|
final hasConnection =
|
||||||
|
await connectivityStatusService.isConnectedToInternet();
|
||||||
|
if (state.isLastPageLoaded || !hasConnection) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit(state.copyWithPaged(isLoading: true));
|
emit(state.copyWithPaged(isLoading: true));
|
||||||
@@ -47,6 +52,32 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
|
|||||||
Future<void> updateFilter({
|
Future<void> updateFilter({
|
||||||
final DocumentFilter filter = const DocumentFilter(),
|
final DocumentFilter filter = const DocumentFilter(),
|
||||||
}) async {
|
}) 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 {
|
try {
|
||||||
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));
|
||||||
|
|||||||
@@ -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/database/tables/local_user_app_state.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/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/paged_documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.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';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
@@ -15,7 +16,8 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
|
|||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
final LabelRepository _labelRepository;
|
final LabelRepository _labelRepository;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
@override
|
@override
|
||||||
final DocumentChangedNotifier notifier;
|
final DocumentChangedNotifier notifier;
|
||||||
|
|
||||||
@@ -27,7 +29,8 @@ class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
|
|||||||
this.api,
|
this.api,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
this._labelRepository,
|
this._labelRepository,
|
||||||
this._userState, {
|
this._userState,
|
||||||
|
this.connectivityStatusService, {
|
||||||
required this.savedView,
|
required this.savedView,
|
||||||
int initialCount = 25,
|
int initialCount = 25,
|
||||||
}) : super(
|
}) : super(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.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_state.dart';
|
||||||
part 'saved_view_preview_cubit.freezed.dart';
|
part 'saved_view_preview_cubit.freezed.dart';
|
||||||
@@ -8,11 +9,21 @@ part 'saved_view_preview_cubit.freezed.dart';
|
|||||||
class SavedViewPreviewCubit extends Cubit<SavedViewPreviewState> {
|
class SavedViewPreviewCubit extends Cubit<SavedViewPreviewState> {
|
||||||
final PaperlessDocumentsApi _api;
|
final PaperlessDocumentsApi _api;
|
||||||
final SavedView view;
|
final SavedView view;
|
||||||
SavedViewPreviewCubit(this._api, this.view)
|
final ConnectivityStatusService _connectivityStatusService;
|
||||||
: super(const SavedViewPreviewState.initial());
|
SavedViewPreviewCubit(
|
||||||
|
this._api,
|
||||||
|
this._connectivityStatusService, {
|
||||||
|
required this.view,
|
||||||
|
}) : super(const InitialSavedViewPreviewState());
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
emit(const SavedViewPreviewState.loading());
|
final isConnected =
|
||||||
|
await _connectivityStatusService.isConnectedToInternet();
|
||||||
|
if (!isConnected) {
|
||||||
|
emit(const OfflineSavedViewPreviewState());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(const LoadingSavedViewPreviewState());
|
||||||
try {
|
try {
|
||||||
final documents = await _api.findAll(
|
final documents = await _api.findAll(
|
||||||
view.toDocumentFilter().copyWith(
|
view.toDocumentFilter().copyWith(
|
||||||
@@ -20,9 +31,9 @@ class SavedViewPreviewCubit extends Cubit<SavedViewPreviewState> {
|
|||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
emit(SavedViewPreviewState.loaded(documents: documents.results));
|
emit(LoadedSavedViewPreviewState(documents: documents.results));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(const SavedViewPreviewState.error());
|
emit(const ErrorSavedViewPreviewState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
part of 'saved_view_preview_cubit.dart';
|
part of 'saved_view_preview_cubit.dart';
|
||||||
|
|
||||||
@freezed
|
sealed class SavedViewPreviewState {
|
||||||
class SavedViewPreviewState with _$SavedViewPreviewState {
|
const SavedViewPreviewState();
|
||||||
const factory SavedViewPreviewState.initial() = _Initial;
|
}
|
||||||
const factory SavedViewPreviewState.loading() = _Loading;
|
|
||||||
const factory SavedViewPreviewState.loaded({
|
class InitialSavedViewPreviewState extends SavedViewPreviewState {
|
||||||
required List<DocumentModel> documents,
|
const InitialSavedViewPreviewState();
|
||||||
}) = _Loaded;
|
}
|
||||||
const factory SavedViewPreviewState.error() = _Error;
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ class SavedViewPreview extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Provider(
|
return Provider(
|
||||||
create: (context) =>
|
create: (context) => SavedViewPreviewCubit(
|
||||||
SavedViewPreviewCubit(context.read(), savedView)..initialize(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
|
view: savedView,
|
||||||
|
)..initialize(),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return ExpansionCard(
|
return ExpansionCard(
|
||||||
initiallyExpanded: expanded,
|
initiallyExpanded: expanded,
|
||||||
@@ -33,10 +36,13 @@ class SavedViewPreview extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
|
BlocBuilder<SavedViewPreviewCubit, SavedViewPreviewState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.maybeWhen(
|
return switch (state) {
|
||||||
loaded: (documents) {
|
LoadedSavedViewPreviewState(documents: var documents) =>
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
if (documents.isEmpty) {
|
if (documents.isEmpty) {
|
||||||
return Text(S.of(context)!.noDocumentsFound).padded();
|
return Text(S.of(context)!.noDocumentsFound)
|
||||||
|
.padded();
|
||||||
} else {
|
} else {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -56,11 +62,14 @@ class SavedViewPreview extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => Text(S.of(context)!.couldNotLoadSavedViews),
|
),
|
||||||
orElse: () => const Center(
|
ErrorSavedViewPreviewState() =>
|
||||||
child: CircularProgressIndicator(),
|
Text(S.of(context)!.couldNotLoadSavedViews).padded(16),
|
||||||
).paddedOnly(top: 8, bottom: 24),
|
OfflineSavedViewPreviewState() =>
|
||||||
);
|
Text(S.of(context)!.youAreCurrentlyOffline).padded(16),
|
||||||
|
_ => const CircularProgressIndicator()
|
||||||
|
.paddedOnly(top: 8, bottom: 24),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:bloc/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/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/features/paged_document_view/cubit/document_paging_bloc_mixin.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/paged_document_view/cubit/paged_documents_state.dart';
|
||||||
|
|
||||||
@@ -10,7 +11,8 @@ part 'similar_documents_state.dart';
|
|||||||
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
||||||
with DocumentPagingBlocMixin {
|
with DocumentPagingBlocMixin {
|
||||||
final int documentId;
|
final int documentId;
|
||||||
|
@override
|
||||||
|
final ConnectivityStatusService connectivityStatusService;
|
||||||
@override
|
@override
|
||||||
final PaperlessDocumentsApi api;
|
final PaperlessDocumentsApi api;
|
||||||
|
|
||||||
@@ -22,7 +24,8 @@ class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
|
|||||||
SimilarDocumentsCubit(
|
SimilarDocumentsCubit(
|
||||||
this.api,
|
this.api,
|
||||||
this.notifier,
|
this.notifier,
|
||||||
this._labelRepository, {
|
this._labelRepository,
|
||||||
|
this.connectivityStatusService, {
|
||||||
required this.documentId,
|
required this.documentId,
|
||||||
}) : super(const SimilarDocumentsState(filter: DocumentFilter())) {
|
}) : super(const SimilarDocumentsState(filter: DocumentFilter())) {
|
||||||
notifier.addListener(
|
notifier.addListener(
|
||||||
|
|||||||
63
lib/helpers/connectivity_aware_action_wrapper.dart
Normal file
63
lib/helpers/connectivity_aware_action_wrapper.dart
Normal 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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'dart:developer';
|
|||||||
|
|
||||||
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/core/model/info_message_exception.dart';
|
||||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||||
|
|
||||||
class SnackBarActionConfig {
|
class SnackBarActionConfig {
|
||||||
@@ -108,3 +109,15 @@ void showErrorMessage(
|
|||||||
time: DateTime.now(),
|
time: DateTime.now(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showInfoMessage(
|
||||||
|
BuildContext context,
|
||||||
|
InfoMessageException error, [
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
]) {
|
||||||
|
showSnackBar(
|
||||||
|
context,
|
||||||
|
translateError(context, error.code),
|
||||||
|
details: error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Alle anzeigen",
|
"showAll": "Alle anzeigen",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,5 +969,9 @@
|
|||||||
"showAll": "Show all",
|
"showAll": "Show all",
|
||||||
"@showAll": {
|
"@showAll": {
|
||||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import 'package:device_info_plus/device_info_plus.dart';
|
|||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.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.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';
|
||||||
|
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/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/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.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';
|
||||||
@@ -103,17 +103,19 @@ void main() async {
|
|||||||
|
|
||||||
await findSystemLocale();
|
await findSystemLocale();
|
||||||
packageInfo = await PackageInfo.fromPlatform();
|
packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
androidInfo = await DeviceInfoPlugin().androidInfo;
|
androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
}
|
}
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
iosInfo = await DeviceInfoPlugin().iosInfo;
|
iosInfo = await DeviceInfoPlugin().iosInfo;
|
||||||
}
|
}
|
||||||
final connectivity = Connectivity();
|
final connectivityStatusService = ConnectivityStatusServiceImpl(
|
||||||
final localAuthentication = LocalAuthentication();
|
Connectivity(),
|
||||||
final connectivityStatusService =
|
);
|
||||||
ConnectivityStatusServiceImpl(connectivity);
|
final localAuthService = LocalAuthenticationService(
|
||||||
final localAuthService = LocalAuthenticationService(localAuthentication);
|
LocalAuthentication(),
|
||||||
|
);
|
||||||
|
|
||||||
HydratedBloc.storage = await HydratedStorage.build(
|
HydratedBloc.storage = await HydratedStorage.build(
|
||||||
storageDirectory: await getApplicationDocumentsDirectory(),
|
storageDirectory: await getApplicationDocumentsDirectory(),
|
||||||
@@ -145,8 +147,12 @@ void main() async {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
|
final apiFactory = PaperlessApiFactoryImpl(sessionManager);
|
||||||
final authenticationCubit =
|
final authenticationCubit = AuthenticationCubit(
|
||||||
AuthenticationCubit(localAuthService, apiFactory, sessionManager);
|
localAuthService,
|
||||||
|
apiFactory,
|
||||||
|
sessionManager,
|
||||||
|
connectivityStatusService,
|
||||||
|
);
|
||||||
await authenticationCubit.restoreSessionState();
|
await authenticationCubit.restoreSessionState();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
@@ -154,7 +160,6 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider.value(value: sessionManager),
|
ChangeNotifierProvider.value(value: sessionManager),
|
||||||
Provider<LocalAuthenticationService>.value(value: localAuthService),
|
Provider<LocalAuthenticationService>.value(value: localAuthService),
|
||||||
Provider<Connectivity>.value(value: connectivity),
|
|
||||||
Provider<ConnectivityStatusService>.value(
|
Provider<ConnectivityStatusService>.value(
|
||||||
value: connectivityStatusService),
|
value: connectivityStatusService),
|
||||||
Provider<LocalNotificationService>.value(
|
Provider<LocalNotificationService>.value(
|
||||||
@@ -171,6 +176,7 @@ void main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, (error, stack) {
|
}, (error, stack) {
|
||||||
|
// Catches all unexpected/uncaught errors and prints them to the console.
|
||||||
String message = switch (error) {
|
String message = switch (error) {
|
||||||
PaperlessApiException e => e.details ?? error.toString(),
|
PaperlessApiException e => e.details ?? error.toString(),
|
||||||
ServerMessageException e => e.message,
|
ServerMessageException e => e.message,
|
||||||
@@ -271,12 +277,22 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<AuthenticationCubit, AuthenticationState>(
|
return BlocListener<AuthenticationCubit, AuthenticationState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
state.when(
|
switch (state) {
|
||||||
unauthenticated: () => _router.goNamed(R.login),
|
case UnauthenticatedState():
|
||||||
requriresLocalAuthentication: () => _router.goNamed(R.verifyIdentity),
|
const LoginRoute().go(context);
|
||||||
switchingAccounts: () => _router.goNamed(R.switchingAccounts),
|
break;
|
||||||
authenticated: (localUserId) => _router.goNamed(R.landing),
|
|
||||||
);
|
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(
|
child: GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ class LinkedDocumentsRoute extends GoRouteData {
|
|||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
|
context.read(),
|
||||||
),
|
),
|
||||||
child: const LinkedDocumentsPage(),
|
child: const LinkedDocumentsPage(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
library mock_server;
|
library mock_server;
|
||||||
|
|
||||||
export 'response_delay_generator.dart';
|
export 'response_delay_factory.dart';
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:mock_server/english_words.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.dart';
|
||||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||||
import 'package:shelf_router/shelf_router.dart' as shelf_router;
|
import 'package:shelf_router/shelf_router.dart' as shelf_router;
|
||||||
@@ -22,15 +22,17 @@ class LocalMockApiServer {
|
|||||||
|
|
||||||
static get baseUrl => 'http://$host:$port/';
|
static get baseUrl => 'http://$host:$port/';
|
||||||
|
|
||||||
final DelayGenerator _delayGenerator;
|
final ResponseDelayFactory _delayGenerator;
|
||||||
|
|
||||||
late shelf_router.Router app;
|
late shelf_router.Router app;
|
||||||
Future<Map<String, dynamic>> loadFixture(String name) async {
|
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);
|
return json.decode(fixture);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalMockApiServer([this._delayGenerator = const ZeroDelayGenerator()]) {
|
LocalMockApiServer(
|
||||||
|
[this._delayGenerator = const ZeroResponseDelayFactory()]) {
|
||||||
app = shelf_router.Router();
|
app = shelf_router.Router();
|
||||||
|
|
||||||
Map<String, dynamic> createdTags = {};
|
Map<String, dynamic> createdTags = {};
|
||||||
@@ -44,7 +46,8 @@ class LocalMockApiServer {
|
|||||||
log.info('Responding to /api/token/');
|
log.info('Responding to /api/token/');
|
||||||
var body = await req.bodyJsonMap();
|
var body = await req.bodyJsonMap();
|
||||||
if (body?['username'] == 'admin' && body?['password'] == 'test') {
|
if (body?['username'] == 'admin' && body?['password'] == 'test') {
|
||||||
return JsonMockResponse.ok({'token': 'testToken'}, _delayGenerator.nextDelay());
|
return JsonMockResponse.ok(
|
||||||
|
{'token': 'testToken'}, _delayGenerator.nextDelay());
|
||||||
} else {
|
} else {
|
||||||
return Response.unauthorized('Unauthorized');
|
return Response.unauthorized('Unauthorized');
|
||||||
}
|
}
|
||||||
@@ -149,9 +152,13 @@ class LocalMockApiServer {
|
|||||||
|
|
||||||
app.delete('/api/tags/<tagId>/', (Request req, String tagId) async {
|
app.delete('/api/tags/<tagId>/', (Request req, String tagId) async {
|
||||||
log.info('Responding to PUT /api/tags/<tagId>/');
|
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,
|
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 {
|
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 {
|
app.get('/api/documents/<docId>/thumb/', (Request req, String docId) async {
|
||||||
log.info('Responding to /api/documents/<docId>/thumb/');
|
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 {
|
try {
|
||||||
var resp = Response.ok(
|
var resp = Response.ok(
|
||||||
http.ByteStream.fromBytes(thumb.buffer.asInt8List()),
|
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/');
|
log.info('Responding to /api/documents/<docId>/metadata/');
|
||||||
var data = await loadFixture('metadata');
|
var data = await loadFixture('metadata');
|
||||||
return JsonMockResponse.ok(data, _delayGenerator.nextDelay());
|
return JsonMockResponse.ok(data, _delayGenerator.nextDelay());
|
||||||
});
|
});
|
||||||
|
|
||||||
//This is not yet used in the app
|
//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/');
|
log.info('Responding to /api/documents/<docId>/suggestions/');
|
||||||
var data = await loadFixture('suggestions');
|
var data = await loadFixture('suggestions');
|
||||||
return JsonMockResponse.ok(data, _delayGenerator.nextDelay());
|
return JsonMockResponse.ok(data, _delayGenerator.nextDelay());
|
||||||
@@ -235,7 +245,10 @@ class LocalMockApiServer {
|
|||||||
final term = req.url.queryParameters["term"] ?? '';
|
final term = req.url.queryParameters["term"] ?? '';
|
||||||
final limit = int.parse(req.url.queryParameters["limit"] ?? '5');
|
final limit = int.parse(req.url.queryParameters["limit"] ?? '5');
|
||||||
return JsonMockResponse.ok(
|
return JsonMockResponse.ok(
|
||||||
mostFrequentWords.where((element) => element.startsWith(term)).take(limit).toList(),
|
mostFrequentWords
|
||||||
|
.where((element) => element.startsWith(term))
|
||||||
|
.take(limit)
|
||||||
|
.toList(),
|
||||||
_delayGenerator.nextDelay(),
|
_delayGenerator.nextDelay(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
abstract interface class DelayGenerator {
|
abstract interface class ResponseDelayFactory {
|
||||||
Duration nextDelay();
|
Duration nextDelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
class RandomDelayGenerator implements DelayGenerator {
|
class RandomResponseDelayFactory implements ResponseDelayFactory {
|
||||||
/// Minimum allowed response delay
|
/// Minimum allowed response delay
|
||||||
final Duration minDelay;
|
final Duration minDelay;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ class RandomDelayGenerator implements DelayGenerator {
|
|||||||
final Duration maxDelay;
|
final Duration maxDelay;
|
||||||
|
|
||||||
final Random _random = Random();
|
final Random _random = Random();
|
||||||
RandomDelayGenerator(this.minDelay, this.maxDelay);
|
RandomResponseDelayFactory(this.minDelay, this.maxDelay);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Duration nextDelay() {
|
Duration nextDelay() {
|
||||||
@@ -25,10 +25,10 @@ class RandomDelayGenerator implements DelayGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConstantDelayGenerator implements DelayGenerator {
|
class ConstantResponseDelayFactory implements ResponseDelayFactory {
|
||||||
final Duration delay;
|
final Duration delay;
|
||||||
|
|
||||||
const ConstantDelayGenerator(this.delay);
|
const ConstantResponseDelayFactory(this.delay);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Duration nextDelay() {
|
Duration nextDelay() {
|
||||||
@@ -36,8 +36,8 @@ class ConstantDelayGenerator implements DelayGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ZeroDelayGenerator implements DelayGenerator {
|
class ZeroResponseDelayFactory implements ResponseDelayFactory {
|
||||||
const ZeroDelayGenerator();
|
const ZeroResponseDelayFactory();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Duration nextDelay() {
|
Duration nextDelay() {
|
||||||
@@ -51,8 +51,8 @@ class PagedSearchResult<T> extends Equatable {
|
|||||||
|
|
||||||
const PagedSearchResult({
|
const PagedSearchResult({
|
||||||
required this.count,
|
required this.count,
|
||||||
required this.next,
|
this.next,
|
||||||
required this.previous,
|
this.previous,
|
||||||
required this.results,
|
required this.results,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -67,5 +67,6 @@ enum ErrorCode {
|
|||||||
uiSettingsLoadFailed,
|
uiSettingsLoadFailed,
|
||||||
loadTasksError,
|
loadTasksError,
|
||||||
userNotFound,
|
userNotFound,
|
||||||
|
userAlreadyExists,
|
||||||
updateSavedViewError;
|
updateSavedViewError;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user