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