mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 20:07:48 -06:00
Hooked notifications to status changes on document upload - some refactorings
This commit is contained in:
@@ -85,7 +85,7 @@ To get a local copy up and running follow these simple steps.
|
|||||||
```sh
|
```sh
|
||||||
flutter pub get
|
flutter pub get
|
||||||
```
|
```
|
||||||
3. Build generated files (e.g. for injectable library)
|
3. Build generated files (for json_serializable etc.)
|
||||||
```sh
|
```sh
|
||||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
|
||||||
|
|
||||||
@prod
|
|
||||||
@test
|
|
||||||
@lazySingleton
|
|
||||||
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
||||||
DocumentStatusCubit() : super(null);
|
DocumentStatusCubit() : super(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
|
|
||||||
@prod
|
|
||||||
@test
|
|
||||||
@lazySingleton
|
|
||||||
class PaperlessServerInformationCubit
|
class PaperlessServerInformationCubit
|
||||||
extends Cubit<PaperlessServerInformationState> {
|
extends Cubit<PaperlessServerInformationState> {
|
||||||
final PaperlessServerStatsApi service;
|
final PaperlessServerStatsApi _api;
|
||||||
|
|
||||||
PaperlessServerInformationCubit(this.service)
|
PaperlessServerInformationCubit(this._api)
|
||||||
: super(PaperlessServerInformationState());
|
: super(PaperlessServerInformationState());
|
||||||
|
|
||||||
Future<void> updateInformtion() async {
|
Future<void> updateInformtion() async {
|
||||||
final information = await service.getServerInformation();
|
final information = await _api.getServerInformation();
|
||||||
emit(PaperlessServerInformationState(
|
emit(PaperlessServerInformationState(
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
information: information,
|
information: information,
|
||||||
|
|||||||
@@ -68,5 +68,9 @@ String translateError(BuildContext context, ErrorCode code) {
|
|||||||
return S.of(context).errorMessageUnsupportedFileFormat;
|
return S.of(context).errorMessageUnsupportedFileFormat;
|
||||||
case ErrorCode.missingClientCertificate:
|
case ErrorCode.missingClientCertificate:
|
||||||
return S.of(context).errorMessageMissingClientCertificate;
|
return S.of(context).errorMessageMissingClientCertificate;
|
||||||
|
case ErrorCode.suggestionsQueryError:
|
||||||
|
return S.of(context).errorMessageSuggestionsQueryError;
|
||||||
|
case ErrorCode.acknowledgeTasksError:
|
||||||
|
return S.of(context).errorMessageAcknowledgeTasksError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:dio/adapter.dart';
|
import 'package:dio/adapter.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||||
|
|
||||||
class AuthenticationAwareDioManager {
|
class SessionManager {
|
||||||
final Dio client;
|
final Dio client;
|
||||||
final List<Interceptor> interceptors;
|
final List<Interceptor> interceptors;
|
||||||
|
PaperlessServerInformationModel serverInformation;
|
||||||
|
|
||||||
AuthenticationAwareDioManager([this.interceptors = const []])
|
SessionManager([this.interceptors = const []])
|
||||||
: client = _initDio(interceptors);
|
: client = _initDio(interceptors),
|
||||||
|
serverInformation = PaperlessServerInformationModel();
|
||||||
|
|
||||||
static Dio _initDio(List<Interceptor> interceptors) {
|
static Dio _initDio(List<Interceptor> interceptors) {
|
||||||
//en- and decoded by utf8 by default
|
//en- and decoded by utf8 by default
|
||||||
@@ -19,8 +24,19 @@ class AuthenticationAwareDioManager {
|
|||||||
dio.options.responseType = ResponseType.json;
|
dio.options.responseType = ResponseType.json;
|
||||||
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
|
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
|
||||||
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||||
dio.interceptors.addAll(interceptors);
|
dio.interceptors.addAll([
|
||||||
dio.interceptors.add(RetryOnConnectionChangeInterceptor(dio: dio));
|
...interceptors,
|
||||||
|
DioHttpErrorInterceptor(),
|
||||||
|
PrettyDioLogger(
|
||||||
|
compact: true,
|
||||||
|
responseBody: false,
|
||||||
|
responseHeader: false,
|
||||||
|
request: false,
|
||||||
|
requestBody: false,
|
||||||
|
requestHeader: false,
|
||||||
|
),
|
||||||
|
RetryOnConnectionChangeInterceptor(dio: dio)
|
||||||
|
]);
|
||||||
return dio;
|
return dio;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +44,7 @@ class AuthenticationAwareDioManager {
|
|||||||
String? baseUrl,
|
String? baseUrl,
|
||||||
String? authToken,
|
String? authToken,
|
||||||
ClientCertificate? clientCertificate,
|
ClientCertificate? clientCertificate,
|
||||||
|
PaperlessServerInformationModel? serverInformation,
|
||||||
}) {
|
}) {
|
||||||
if (clientCertificate != null) {
|
if (clientCertificate != null) {
|
||||||
final context = SecurityContext()
|
final context = SecurityContext()
|
||||||
@@ -58,11 +75,16 @@ class AuthenticationAwareDioManager {
|
|||||||
if (authToken != null) {
|
if (authToken != null) {
|
||||||
client.options.headers.addAll({'Authorization': 'Token $authToken'});
|
client.options.headers.addAll({'Authorization': 'Token $authToken'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverInformation != null) {
|
||||||
|
this.serverInformation = serverInformation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetSettings() {
|
void resetSettings() {
|
||||||
client.httpClientAdapter = DefaultHttpClientAdapter();
|
client.httpClientAdapter = DefaultHttpClientAdapter();
|
||||||
client.options.baseUrl = '';
|
client.options.baseUrl = '';
|
||||||
client.options.headers.remove('Authorization');
|
client.options.headers.remove('Authorization');
|
||||||
|
serverInformation = PaperlessServerInformationModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
lib/extensions/hydrated_storage_extension.dart
Normal file
14
lib/extensions/hydrated_storage_extension.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||||
|
|
||||||
|
extension AddressableHydratedStorage on Storage {
|
||||||
|
ApplicationSettingsState get settings {
|
||||||
|
return ApplicationSettingsState.fromJson(read('ApplicationSettingsCubit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationState get authentication {
|
||||||
|
return AuthenticationState.fromJson(read('AuthenticationCubit'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,22 +8,40 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
final PaperlessDocumentsApi _api;
|
final PaperlessDocumentsApi _api;
|
||||||
|
|
||||||
DocumentDetailsCubit(this._api, DocumentModel initialDocument)
|
DocumentDetailsCubit(this._api, DocumentModel initialDocument)
|
||||||
: super(DocumentDetailsState(document: initialDocument));
|
: super(DocumentDetailsState(document: initialDocument)) {
|
||||||
|
loadSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> delete(DocumentModel document) async {
|
Future<void> delete(DocumentModel document) async {
|
||||||
await _api.delete(document);
|
await _api.delete(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadSuggestions() async {
|
||||||
|
final suggestions = await _api.findSuggestions(state.document);
|
||||||
|
emit(state.copyWith(suggestions: suggestions));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadFullContent() async {
|
||||||
|
final doc = await _api.find(state.document.id);
|
||||||
|
if (doc == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(state.copyWith(
|
||||||
|
isFullContentLoaded: true,
|
||||||
|
fullContent: doc.content,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> assignAsn(DocumentModel document) async {
|
Future<void> assignAsn(DocumentModel document) async {
|
||||||
if (document.archiveSerialNumber == null) {
|
if (document.archiveSerialNumber == null) {
|
||||||
final int asn = await _api.findNextAsn();
|
final int asn = await _api.findNextAsn();
|
||||||
final updatedDocument =
|
final updatedDocument =
|
||||||
await _api.update(document.copyWith(archiveSerialNumber: asn));
|
await _api.update(document.copyWith(archiveSerialNumber: asn));
|
||||||
emit(DocumentDetailsState(document: updatedDocument));
|
emit(state.copyWith(document: updatedDocument));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void replaceDocument(DocumentModel document) {
|
void replaceDocument(DocumentModel document) {
|
||||||
emit(DocumentDetailsState(document: document));
|
emit(state.copyWith(document: document));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,36 @@ part of 'document_details_cubit.dart';
|
|||||||
|
|
||||||
class DocumentDetailsState with EquatableMixin {
|
class DocumentDetailsState with EquatableMixin {
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
|
final bool isFullContentLoaded;
|
||||||
|
final String? fullContent;
|
||||||
|
final FieldSuggestions suggestions;
|
||||||
|
|
||||||
const DocumentDetailsState({
|
const DocumentDetailsState({
|
||||||
required this.document,
|
required this.document,
|
||||||
|
this.suggestions = const FieldSuggestions(),
|
||||||
|
this.isFullContentLoaded = false,
|
||||||
|
this.fullContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [document];
|
List<Object?> get props => [
|
||||||
|
document,
|
||||||
|
suggestions,
|
||||||
|
isFullContentLoaded,
|
||||||
|
fullContent,
|
||||||
|
];
|
||||||
|
|
||||||
|
DocumentDetailsState copyWith({
|
||||||
|
DocumentModel? document,
|
||||||
|
FieldSuggestions? suggestions,
|
||||||
|
bool? isFullContentLoaded,
|
||||||
|
String? fullContent,
|
||||||
|
}) {
|
||||||
|
return DocumentDetailsState(
|
||||||
|
document: document ?? this.document,
|
||||||
|
suggestions: suggestions ?? this.suggestions,
|
||||||
|
isFullContentLoaded: isFullContentLoaded ?? this.isFullContentLoaded,
|
||||||
|
fullContent: fullContent ?? this.fullContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
|
|||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:badges/badges.dart' as b;
|
||||||
|
|
||||||
class DocumentDetailsPage extends StatefulWidget {
|
class DocumentDetailsPage extends StatefulWidget {
|
||||||
final bool allowEdit;
|
final bool allowEdit;
|
||||||
@@ -63,9 +64,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
if (!connectivityState.isConnected) {
|
if (!connectivityState.isConnected) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
return FloatingActionButton(
|
return b.Badge(
|
||||||
child: const Icon(Icons.edit),
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
onPressed: () => _onEdit(state.document),
|
showBadge: state.suggestions.hasSuggestions,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.edit),
|
||||||
|
onPressed: () => _onEdit(state.document),
|
||||||
|
),
|
||||||
|
badgeContent: Text(
|
||||||
|
'${state.suggestions.suggestionsCount}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
badgeColor: Theme.of(context).colorScheme.error,
|
||||||
|
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -182,6 +195,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
_buildDocumentContentView(
|
_buildDocumentContentView(
|
||||||
state.document,
|
state.document,
|
||||||
widget.titleAndContentQueryString,
|
widget.titleAndContentQueryString,
|
||||||
|
state,
|
||||||
),
|
),
|
||||||
_buildDocumentMetaDataView(
|
_buildDocumentMetaDataView(
|
||||||
state.document,
|
state.document,
|
||||||
@@ -217,7 +231,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
cubit.replaceDocument(state.document);
|
cubit.replaceDocument(state.document);
|
||||||
},
|
},
|
||||||
child: const DocumentEditPage(),
|
child: DocumentEditPage(
|
||||||
|
suggestions: cubit.state.suggestions,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
maintainState: true,
|
maintainState: true,
|
||||||
@@ -303,14 +319,30 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentContentView(DocumentModel document, String? match) {
|
Widget _buildDocumentContentView(
|
||||||
return SingleChildScrollView(
|
DocumentModel document,
|
||||||
child: HighlightedText(
|
String? match,
|
||||||
text: document.content ?? "",
|
DocumentDetailsState state,
|
||||||
highlights: match == null ? [] : match.split(" "),
|
) {
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
return ListView(
|
||||||
caseSensitive: false,
|
children: [
|
||||||
),
|
HighlightedText(
|
||||||
|
text: (state.isFullContentLoaded
|
||||||
|
? state.fullContent
|
||||||
|
: document.content) ??
|
||||||
|
"",
|
||||||
|
highlights: match == null ? [] : match.split(" "),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
caseSensitive: false,
|
||||||
|
),
|
||||||
|
if (!state.isFullContentLoaded && (document.content ?? '').isNotEmpty)
|
||||||
|
TextButton(
|
||||||
|
child: Text("Show full content ..."),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<DocumentDetailsCubit>().loadFullContent();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
).paddedOnly(top: 8);
|
).paddedOnly(top: 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,17 +55,16 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> upload(
|
Future<String?> upload(
|
||||||
Uint8List bytes, {
|
Uint8List bytes, {
|
||||||
required String filename,
|
required String filename,
|
||||||
required String title,
|
required String title,
|
||||||
required void Function(DocumentModel document)? onConsumptionFinished,
|
|
||||||
int? documentType,
|
int? documentType,
|
||||||
int? correspondent,
|
int? correspondent,
|
||||||
Iterable<int> tags = const [],
|
Iterable<int> tags = const [],
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
}) async {
|
}) async {
|
||||||
await _documentApi.create(
|
return await _documentApi.create(
|
||||||
bytes,
|
bytes,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -74,11 +73,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
);
|
);
|
||||||
if (onConsumptionFinished != null) {
|
|
||||||
_documentApi
|
|
||||||
.waitForConsumptionFinished(filename, title)
|
|
||||||
.then((value) => onConsumptionFinished(value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -25,14 +25,12 @@ class DocumentUploadPreparationPage extends StatefulWidget {
|
|||||||
final String? title;
|
final String? title;
|
||||||
final String? filename;
|
final String? filename;
|
||||||
final String? fileExtension;
|
final String? fileExtension;
|
||||||
final void Function(DocumentModel)? onSuccessfullyConsumed;
|
|
||||||
|
|
||||||
const DocumentUploadPreparationPage({
|
const DocumentUploadPreparationPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.fileBytes,
|
required this.fileBytes,
|
||||||
this.title,
|
this.title,
|
||||||
this.filename,
|
this.filename,
|
||||||
this.onSuccessfullyConsumed,
|
|
||||||
this.fileExtension,
|
this.fileExtension,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -236,19 +234,18 @@ class _DocumentUploadPreparationPageState
|
|||||||
final correspondent =
|
final correspondent =
|
||||||
fv[DocumentModel.correspondentKey] as IdQueryParameter;
|
fv[DocumentModel.correspondentKey] as IdQueryParameter;
|
||||||
|
|
||||||
await cubit.upload(
|
final taskId = await cubit.upload(
|
||||||
widget.fileBytes,
|
widget.fileBytes,
|
||||||
filename:
|
filename:
|
||||||
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
|
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
|
||||||
title: title,
|
title: title,
|
||||||
onConsumptionFinished: widget.onSuccessfullyConsumed,
|
|
||||||
documentType: docType.id,
|
documentType: docType.id,
|
||||||
correspondent: correspondent.id,
|
correspondent: correspondent.id,
|
||||||
tags: tags.ids,
|
tags: tags.ids,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
);
|
);
|
||||||
showSnackBar(context, S.of(context).documentUploadSuccessText);
|
showSnackBar(context, S.of(context).documentUploadSuccessText);
|
||||||
Navigator.pop(context, true);
|
Navigator.pop(context, taskId);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} on PaperlessValidationErrors catch (errors) {
|
} on PaperlessValidationErrors catch (errors) {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
log("[DocumentsCubit] load");
|
log("[DocumentsCubit] load");
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
try {
|
try {
|
||||||
final result = await _api.find(state.filter);
|
final result = await _api.findAll(state.filter);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasLoaded: true,
|
hasLoaded: true,
|
||||||
@@ -67,11 +67,13 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
log("[DocumentsCubit] reload");
|
log("[DocumentsCubit] reload");
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
try {
|
try {
|
||||||
final result = await _api.find(state.filter.copyWith(page: 1));
|
final filter = state.filter.copyWith(page: 1);
|
||||||
|
final result = await _api.findAll(filter);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
hasLoaded: true,
|
hasLoaded: true,
|
||||||
value: [result],
|
value: [result],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
filter: filter,
|
||||||
));
|
));
|
||||||
} finally {
|
} finally {
|
||||||
emit(state.copyWith(isLoading: false));
|
emit(state.copyWith(isLoading: false));
|
||||||
@@ -81,7 +83,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
Future<void> _bulkReloadDocuments() async {
|
Future<void> _bulkReloadDocuments() async {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
try {
|
try {
|
||||||
final result = await _api.find(
|
final result = await _api.findAll(
|
||||||
state.filter.copyWith(
|
state.filter.copyWith(
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: state.documents.length,
|
pageSize: state.documents.length,
|
||||||
@@ -106,7 +108,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
|
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
|
||||||
try {
|
try {
|
||||||
final result = await _api.find(newFilter);
|
final result = await _api.findAll(newFilter);
|
||||||
emit(
|
emit(
|
||||||
DocumentsState(
|
DocumentsState(
|
||||||
hasLoaded: true,
|
hasLoaded: true,
|
||||||
@@ -129,7 +131,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
log("[DocumentsCubit] updateFilter");
|
log("[DocumentsCubit] updateFilter");
|
||||||
try {
|
try {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
final result = await _api.find(filter.copyWith(page: 1));
|
final result = await _api.findAll(filter.copyWith(page: 1));
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
DocumentsState(
|
DocumentsState(
|
||||||
@@ -163,7 +165,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
|
|
||||||
void toggleDocumentSelection(DocumentModel model) {
|
void toggleDocumentSelection(DocumentModel model) {
|
||||||
log("[DocumentsCubit] toggleSelection");
|
log("[DocumentsCubit] toggleSelection");
|
||||||
if (state.selection.contains(model)) {
|
if (state.selectedIds.contains(model.id)) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
selection: state.selection
|
selection: state.selection
|
||||||
@@ -183,6 +185,12 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
emit(state.copyWith(selection: []));
|
emit(state.copyWith(selection: []));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onChange(Change<DocumentsState> change) {
|
||||||
|
super.onChange(change);
|
||||||
|
log("[DocumentsCubit] state changed: ${change.currentState.selection.map((e) => e.id).join(",")} to ${change.nextState.selection.map((e) => e.id).join(",")}");
|
||||||
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
log("[DocumentsCubit] reset");
|
log("[DocumentsCubit] reset");
|
||||||
emit(const DocumentsState());
|
emit(const DocumentsState());
|
||||||
@@ -196,7 +204,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
|||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final results = await _api.find(filter.copyWith(page: 1));
|
final results = await _api.findAll(filter.copyWith(page: 1));
|
||||||
emit(
|
emit(
|
||||||
DocumentsState(
|
DocumentsState(
|
||||||
filter: filter,
|
filter: filter,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class DocumentsState extends Equatable {
|
|||||||
this.selectedSavedViewId,
|
this.selectedSavedViewId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
List<int> get selectedIds => selection.map((e) => e.id).toList();
|
||||||
|
|
||||||
int get currentPageNumber {
|
int get currentPageNumber {
|
||||||
return filter.page;
|
return filter.page;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
@@ -15,14 +16,18 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi
|
|||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
|
||||||
class DocumentEditPage extends StatefulWidget {
|
class DocumentEditPage extends StatefulWidget {
|
||||||
|
final FieldSuggestions suggestions;
|
||||||
const DocumentEditPage({
|
const DocumentEditPage({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
required this.suggestions,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -74,14 +79,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
_buildTitleFormField(state.document.title).padded(),
|
_buildTitleFormField(state.document.title).padded(),
|
||||||
_buildCreatedAtFormField(state.document.created).padded(),
|
_buildCreatedAtFormField(state.document.created).padded(),
|
||||||
_buildDocumentTypeFormField(
|
_buildDocumentTypeFormField(
|
||||||
state.document.documentType, state.documentTypes)
|
state.document.documentType,
|
||||||
.padded(),
|
state.documentTypes,
|
||||||
|
).padded(),
|
||||||
_buildCorrespondentFormField(
|
_buildCorrespondentFormField(
|
||||||
state.document.correspondent, state.correspondents)
|
state.document.correspondent,
|
||||||
.padded(),
|
state.correspondents,
|
||||||
|
).padded(),
|
||||||
_buildStoragePathFormField(
|
_buildStoragePathFormField(
|
||||||
state.document.storagePath, state.storagePaths)
|
state.document.storagePath,
|
||||||
.padded(),
|
state.storagePaths,
|
||||||
|
).padded(),
|
||||||
TagFormField(
|
TagFormField(
|
||||||
initialValue:
|
initialValue:
|
||||||
IdsTagsQuery.included(state.document.tags.toList()),
|
IdsTagsQuery.included(state.document.tags.toList()),
|
||||||
@@ -99,58 +107,101 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStoragePathFormField(
|
Widget _buildStoragePathFormField(
|
||||||
int? initialId, Map<int, StoragePath> options) {
|
int? initialId,
|
||||||
return LabelFormField<StoragePath>(
|
Map<int, StoragePath> options,
|
||||||
notAssignedSelectable: false,
|
) {
|
||||||
formBuilderState: _formKey.currentState,
|
return Column(
|
||||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
children: [
|
||||||
create: (context) => context
|
LabelFormField<StoragePath>(
|
||||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
notAssignedSelectable: false,
|
||||||
child: AddStoragePathPage(initalValue: initialValue),
|
formBuilderState: _formKey.currentState,
|
||||||
),
|
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||||
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
create: (context) => context.read<
|
||||||
labelOptions: options,
|
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||||
initialValue: IdQueryParameter.fromId(initialId),
|
child: AddStoragePathPage(initalValue: initialValue),
|
||||||
name: fkStoragePath,
|
),
|
||||||
prefixIcon: const Icon(Icons.folder_outlined),
|
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
||||||
|
labelOptions: options,
|
||||||
|
initialValue: IdQueryParameter.fromId(initialId),
|
||||||
|
name: fkStoragePath,
|
||||||
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
|
),
|
||||||
|
if (widget.suggestions.hasSuggestedStoragePaths)
|
||||||
|
_buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions: widget.suggestions.storagePaths,
|
||||||
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
|
label: Text(options[itemData]!.name),
|
||||||
|
onPressed: () => _formKey.currentState?.fields[fkStoragePath]
|
||||||
|
?.didChange((IdQueryParameter.fromId(itemData))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCorrespondentFormField(
|
Widget _buildCorrespondentFormField(
|
||||||
int? initialId, Map<int, Correspondent> options) {
|
int? initialId, Map<int, Correspondent> options) {
|
||||||
return LabelFormField<Correspondent>(
|
return Column(
|
||||||
notAssignedSelectable: false,
|
children: [
|
||||||
formBuilderState: _formKey.currentState,
|
LabelFormField<Correspondent>(
|
||||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
notAssignedSelectable: false,
|
||||||
create: (context) => context.read<
|
formBuilderState: _formKey.currentState,
|
||||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||||
child: AddCorrespondentPage(initialName: initialValue),
|
create: (context) => context.read<
|
||||||
),
|
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
child: AddCorrespondentPage(initialName: initialValue),
|
||||||
labelOptions: options,
|
),
|
||||||
initialValue: IdQueryParameter.fromId(initialId),
|
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||||
name: fkCorrespondent,
|
labelOptions: options,
|
||||||
prefixIcon: const Icon(Icons.person_outlined),
|
initialValue: IdQueryParameter.fromId(initialId),
|
||||||
|
name: fkCorrespondent,
|
||||||
|
prefixIcon: const Icon(Icons.person_outlined),
|
||||||
|
),
|
||||||
|
if (widget.suggestions.hasSuggestedCorrespondents)
|
||||||
|
_buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions: widget.suggestions.correspondents,
|
||||||
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
|
label: Text(options[itemData]!.name),
|
||||||
|
onPressed: () => _formKey.currentState?.fields[fkCorrespondent]
|
||||||
|
?.didChange((IdQueryParameter.fromId(itemData))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentTypeFormField(
|
Widget _buildDocumentTypeFormField(
|
||||||
int? initialId, Map<int, DocumentType> options) {
|
int? initialId,
|
||||||
return LabelFormField<DocumentType>(
|
Map<int, DocumentType> options,
|
||||||
notAssignedSelectable: false,
|
) {
|
||||||
formBuilderState: _formKey.currentState,
|
return Column(
|
||||||
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
children: [
|
||||||
create: (context) => context
|
LabelFormField<DocumentType>(
|
||||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
notAssignedSelectable: false,
|
||||||
child: AddDocumentTypePage(
|
formBuilderState: _formKey.currentState,
|
||||||
initialName: currentInput,
|
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
||||||
|
create: (context) => context.read<
|
||||||
|
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||||
|
child: AddDocumentTypePage(
|
||||||
|
initialName: currentInput,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
||||||
|
initialValue: IdQueryParameter.fromId(initialId),
|
||||||
|
labelOptions: options,
|
||||||
|
name: fkDocumentType,
|
||||||
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
),
|
),
|
||||||
),
|
if (widget.suggestions.hasSuggestedDocumentTypes)
|
||||||
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
_buildSuggestionsSkeleton<int>(
|
||||||
initialValue: IdQueryParameter.fromId(initialId),
|
suggestions: widget.suggestions.documentTypes,
|
||||||
labelOptions: options,
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
name: fkDocumentType,
|
label: Text(options[itemData]!.name),
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
|
||||||
|
?.didChange(IdQueryParameter.fromId(itemData)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,16 +249,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
|
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
|
||||||
return FormBuilderDateTimePicker(
|
return Column(
|
||||||
inputType: InputType.date,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
name: fkCreatedDate,
|
children: [
|
||||||
decoration: InputDecoration(
|
FormBuilderDateTimePicker(
|
||||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
inputType: InputType.date,
|
||||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
name: fkCreatedDate,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
initialValue: initialCreatedAtDate,
|
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||||
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
|
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||||
initialEntryMode: DatePickerEntryMode.calendar,
|
),
|
||||||
|
initialValue: initialCreatedAtDate,
|
||||||
|
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
|
||||||
|
initialEntryMode: DatePickerEntryMode.calendar,
|
||||||
|
),
|
||||||
|
if (widget.suggestions.hasSuggestedDates)
|
||||||
|
_buildSuggestionsSkeleton<DateTime>(
|
||||||
|
suggestions: widget.suggestions.dates,
|
||||||
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
|
label: Text(DateFormat.yMd().format(itemData)),
|
||||||
|
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
||||||
|
?.didChange(itemData),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSuggestionsSkeleton<T>({
|
||||||
|
required Iterable<T> suggestions,
|
||||||
|
required ItemBuilder<T> itemBuilder,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Suggestions: ",
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: suggestions.length,
|
||||||
|
itemBuilder: (context, index) =>
|
||||||
|
itemBuilder(context, suggestions.elementAt(index)),
|
||||||
|
separatorBuilder: (BuildContext context, int index) =>
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
|
import 'package:badges/badges.dart' as b;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
|
||||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
|
|
||||||
class DocumentFilterIntent {
|
class DocumentFilterIntent {
|
||||||
@@ -42,9 +45,11 @@ class DocumentsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentsPageState extends State<DocumentsPage> {
|
class _DocumentsPageState extends State<DocumentsPage> {
|
||||||
final _pagingController = PagingController<int, DocumentModel>(
|
final ScrollController _scrollController = ScrollController();
|
||||||
firstPageKey: 1,
|
double _offset = 0;
|
||||||
);
|
double _last = 0;
|
||||||
|
|
||||||
|
static const double _savedViewWidgetHeight = 78 + 16;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -55,12 +60,36 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
_pagingController.addPageRequestListener(_loadNewPage);
|
_scrollController
|
||||||
|
..addListener(_listenForScrollChanges)
|
||||||
|
..addListener(_listenForLoadDataTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenForLoadDataTrigger() {
|
||||||
|
final currState = context.read<DocumentsCubit>().state;
|
||||||
|
if (_scrollController.offset >=
|
||||||
|
_scrollController.position.maxScrollExtent &&
|
||||||
|
!currState.isLoading &&
|
||||||
|
!currState.isLastPageLoaded) {
|
||||||
|
_loadNewPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenForScrollChanges() {
|
||||||
|
final current = _scrollController.offset;
|
||||||
|
_offset += _last - current;
|
||||||
|
|
||||||
|
if (_offset <= -_savedViewWidgetHeight) _offset = -_savedViewWidgetHeight;
|
||||||
|
if (_offset >= 0) _offset = 0;
|
||||||
|
_last = current;
|
||||||
|
if (_offset <= 0 && _offset >= -_savedViewWidgetHeight) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pagingController.dispose();
|
_scrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +107,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
|
const linearProgressIndicatorHeight = 4.0;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: BlocProvider.value(
|
drawer: BlocProvider.value(
|
||||||
value: context.read<AuthenticationCubit>(),
|
value: context.read<AuthenticationCubit>(),
|
||||||
@@ -85,16 +115,65 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
appBar: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(
|
||||||
|
kToolbarHeight + linearProgressIndicatorHeight,
|
||||||
|
),
|
||||||
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return AppBar(
|
||||||
|
title: Text(
|
||||||
|
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
const SortDocumentsButton(),
|
||||||
|
BlocBuilder<ApplicationSettingsCubit,
|
||||||
|
ApplicationSettingsState>(
|
||||||
|
builder: (context, settingsState) => IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
settingsState.preferredViewType == ViewType.grid
|
||||||
|
? Icons.list
|
||||||
|
: Icons.grid_view_rounded,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// Reset saved view widget position as scroll offset will be reset anyway.
|
||||||
|
setState(() {
|
||||||
|
_offset = 0;
|
||||||
|
_last = 0;
|
||||||
|
});
|
||||||
|
final cubit =
|
||||||
|
context.read<ApplicationSettingsCubit>();
|
||||||
|
cubit.setViewType(
|
||||||
|
cubit.state.preferredViewType.toggle());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize:
|
||||||
|
const Size.fromHeight(linearProgressIndicatorHeight),
|
||||||
|
child: state.isLoading
|
||||||
|
? const LinearProgressIndicator()
|
||||||
|
: const SizedBox(height: 4.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||||
return Badge.count(
|
return b.Badge(
|
||||||
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
alignment: const AlignmentDirectional(44, -4),
|
showBadge: appliedFiltersCount > 0,
|
||||||
isLabelVisible: appliedFiltersCount > 0,
|
badgeContent: Text(
|
||||||
count: state.filter.appliedFiltersCount,
|
'$appliedFiltersCount',
|
||||||
backgroundColor: Colors.red,
|
style: const TextStyle(
|
||||||
textColor: Colors.white,
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
animationType: b.BadgeAnimationType.fade,
|
||||||
|
badgeColor: Theme.of(context).colorScheme.error,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
child: const Icon(Icons.filter_alt_outlined),
|
child: const Icon(Icons.filter_alt_outlined),
|
||||||
onPressed: _openDocumentFilter,
|
onPressed: _openDocumentFilter,
|
||||||
@@ -103,12 +182,71 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: _buildBody(connectivityState),
|
body: WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||||
|
context.read<DocumentsCubit>().resetSelection();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: _onRefresh,
|
||||||
|
notificationPredicate: (_) => connectivityState.isConnected,
|
||||||
|
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||||
|
builder: (context, taskState) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
_buildBody(connectivityState),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: _offset,
|
||||||
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.background,
|
||||||
|
child: SavedViewSelectionWidget(
|
||||||
|
height: _savedViewWidgetHeight,
|
||||||
|
currentFilter: state.filter,
|
||||||
|
enabled: state.selection.isEmpty &&
|
||||||
|
connectivityState.isConnected,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (taskState.task != null &&
|
||||||
|
taskState.isSuccess &&
|
||||||
|
!taskState.task!.acknowledged)
|
||||||
|
_buildNewDocumentAvailableButton(context),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Align _buildNewDocumentAvailableButton(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: FilledButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStatePropertyAll(Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
|
child: Text("New document available!"),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
||||||
|
context.read<DocumentsCubit>().reload();
|
||||||
|
},
|
||||||
|
).paddedOnly(bottom: 24, left: 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _openDocumentFilter() async {
|
void _openDocumentFilter() async {
|
||||||
final draggableSheetController = DraggableScrollableController();
|
final draggableSheetController = DraggableScrollableController();
|
||||||
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
|
||||||
@@ -160,88 +298,45 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatDocumentCount(int count) {
|
||||||
|
return count > 99 ? "99+" : count.toString();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBody(ConnectivityState connectivityState) {
|
Widget _buildBody(ConnectivityState connectivityState) {
|
||||||
final isConnected = connectivityState == ConnectivityState.connected;
|
final isConnected = connectivityState == ConnectivityState.connected;
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
buildWhen: (previous, current) => !const ListEquality()
|
buildWhen: (previous, current) =>
|
||||||
.equals(previous.documents, current.documents),
|
!const ListEquality()
|
||||||
|
.equals(previous.documents, current.documents) ||
|
||||||
|
previous.selectedIds != current.selectedIds,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Some ugly tricks to make it work with bloc, update pageController
|
// Some ugly tricks to make it work with bloc, update pageController
|
||||||
_pagingController.value = PagingState(
|
|
||||||
itemList: state.documents,
|
|
||||||
nextPageKey: state.nextPageNumber,
|
|
||||||
);
|
|
||||||
|
|
||||||
late Widget child;
|
|
||||||
switch (settings.preferredViewType) {
|
|
||||||
case ViewType.list:
|
|
||||||
child = DocumentListView(
|
|
||||||
state: state,
|
|
||||||
onTap: _openDetails,
|
|
||||||
onSelected: _onSelected,
|
|
||||||
pagingController: _pagingController,
|
|
||||||
hasInternetConnection: isConnected,
|
|
||||||
onTagSelected: _addTagToFilter,
|
|
||||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
|
||||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
|
||||||
onStoragePathSelected: _addStoragePathToFilter,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case ViewType.grid:
|
|
||||||
child = DocumentGridView(
|
|
||||||
state: state,
|
|
||||||
onTap: _openDetails,
|
|
||||||
onSelected: _onSelected,
|
|
||||||
pagingController: _pagingController,
|
|
||||||
hasInternetConnection: isConnected,
|
|
||||||
onTagSelected: _addTagToFilter,
|
|
||||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
|
||||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
|
||||||
onStoragePathSelected: _addStoragePathToFilter,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.hasLoaded && state.documents.isEmpty) {
|
if (state.hasLoaded && state.documents.isEmpty) {
|
||||||
child = SliverToBoxAdapter(
|
return DocumentsEmptyState(
|
||||||
child: DocumentsEmptyState(
|
state: state,
|
||||||
state: state,
|
onReset: () {
|
||||||
onReset: () {
|
context.read<DocumentsCubit>().resetFilter();
|
||||||
context.read<DocumentsCubit>().resetFilter();
|
context.read<DocumentsCubit>().unselectView();
|
||||||
context.read<DocumentsCubit>().unselectView();
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RefreshIndicator(
|
return AdaptiveDocumentsView(
|
||||||
onRefresh: _onRefresh,
|
viewType: settings.preferredViewType,
|
||||||
notificationPredicate: (_) => isConnected,
|
state: state,
|
||||||
child: CustomScrollView(
|
scrollController: _scrollController,
|
||||||
slivers: [
|
onTap: _openDetails,
|
||||||
DocumentsPageAppBar(
|
onSelected: _onSelected,
|
||||||
isOffline: connectivityState != ConnectivityState.connected,
|
hasInternetConnection: isConnected,
|
||||||
actions: [
|
onTagSelected: _addTagToFilter,
|
||||||
const SortDocumentsButton(),
|
onCorrespondentSelected: _addCorrespondentToFilter,
|
||||||
IconButton(
|
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
||||||
icon: Icon(
|
onStoragePathSelected: _addStoragePathToFilter,
|
||||||
settings.preferredViewType == ViewType.grid
|
pageLoadingWidget: const NewItemsLoadingWidget(),
|
||||||
? Icons.list
|
beforeItems: const SizedBox(height: _savedViewWidgetHeight),
|
||||||
: Icons.grid_view,
|
|
||||||
),
|
|
||||||
onPressed: () => context
|
|
||||||
.read<ApplicationSettingsCubit>()
|
|
||||||
.setViewType(
|
|
||||||
settings.preferredViewType.toggle(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -355,15 +450,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadNewPage(int pageKey) async {
|
Future<void> _loadNewPage() async {
|
||||||
final documentsCubit = context.read<DocumentsCubit>();
|
|
||||||
final pageCount = documentsCubit.state
|
|
||||||
.inferPageCount(pageSize: documentsCubit.state.filter.pageSize);
|
|
||||||
if (pageCount <= pageKey + 1) {
|
|
||||||
_pagingController.nextPageKey = null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await documentsCubit.loadMore();
|
await context.read<DocumentsCubit>().loadMore();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
@@ -376,8 +465,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
Future<void> _onRefresh() async {
|
Future<void> _onRefresh() async {
|
||||||
try {
|
try {
|
||||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||||
await context.read<DocumentsCubit>().reload();
|
context.read<DocumentsCubit>().reload();
|
||||||
await context.read<SavedViewCubit>().reload();
|
context.read<SavedViewCubit>().reload();
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
|
|
||||||
class DocumentGridView extends StatelessWidget {
|
|
||||||
final void Function(DocumentModel model) onTap;
|
|
||||||
final void Function(DocumentModel) onSelected;
|
|
||||||
final PagingController<int, DocumentModel> pagingController;
|
|
||||||
final DocumentsState state;
|
|
||||||
final bool hasInternetConnection;
|
|
||||||
final void Function(int tagId) onTagSelected;
|
|
||||||
final void Function(int correspondentId) onCorrespondentSelected;
|
|
||||||
final void Function(int correspondentId) onDocumentTypeSelected;
|
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
|
||||||
|
|
||||||
const DocumentGridView({
|
|
||||||
super.key,
|
|
||||||
required this.onTap,
|
|
||||||
required this.pagingController,
|
|
||||||
required this.state,
|
|
||||||
required this.onSelected,
|
|
||||||
required this.hasInternetConnection,
|
|
||||||
required this.onTagSelected,
|
|
||||||
required this.onCorrespondentSelected,
|
|
||||||
required this.onDocumentTypeSelected,
|
|
||||||
this.onStoragePathSelected,
|
|
||||||
});
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return PagedSliverGrid<int, DocumentModel>(
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 4,
|
|
||||||
crossAxisSpacing: 4,
|
|
||||||
childAspectRatio: 1 / 2,
|
|
||||||
),
|
|
||||||
pagingController: pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate(
|
|
||||||
itemBuilder: (context, item, index) {
|
|
||||||
return DocumentGridItem(
|
|
||||||
document: item,
|
|
||||||
onTap: onTap,
|
|
||||||
isSelected: state.selection.contains(item),
|
|
||||||
onSelected: onSelected,
|
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
|
||||||
isTagSelectedPredicate: (int tagId) {
|
|
||||||
return state.filter.tags is IdsTagsQuery
|
|
||||||
? (state.filter.tags as IdsTagsQuery)
|
|
||||||
.includedIds
|
|
||||||
.contains(tagId)
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
onTagSelected: onTagSelected,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
noItemsFoundIndicatorBuilder: (context) =>
|
|
||||||
const DocumentsListLoadingWidget(), //TODO: Replace with grid loading widget
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
final void Function(DocumentModel) onSelected;
|
final void Function(DocumentModel) onSelected;
|
||||||
final bool isAtLeastOneSelected;
|
final bool isAtLeastOneSelected;
|
||||||
final bool Function(int tagId) isTagSelectedPredicate;
|
final bool Function(int tagId) isTagSelectedPredicate;
|
||||||
final void Function(int tagId) onTagSelected;
|
final void Function(int tagId)? onTagSelected;
|
||||||
|
|
||||||
const DocumentGridItem({
|
const DocumentGridItem({
|
||||||
Key? key,
|
Key? key,
|
||||||
@@ -57,9 +57,11 @@ class DocumentGridItem extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CorrespondentWidget(
|
CorrespondentWidget(
|
||||||
correspondentId: document.correspondent),
|
correspondentId: document.correspondent,
|
||||||
|
),
|
||||||
DocumentTypeWidget(
|
DocumentTypeWidget(
|
||||||
documentTypeId: document.documentType),
|
documentTypeId: document.documentType,
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
document.title,
|
document.title,
|
||||||
maxLines: document.tags.isEmpty ? 3 : 2,
|
maxLines: document.tags.isEmpty ? 3 : 2,
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||||
|
|
||||||
|
class AdaptiveDocumentsView extends StatelessWidget {
|
||||||
|
final ViewType viewType;
|
||||||
|
final Widget beforeItems;
|
||||||
|
final void Function(DocumentModel) onTap;
|
||||||
|
final void Function(DocumentModel) onSelected;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final DocumentsState state;
|
||||||
|
final bool hasInternetConnection;
|
||||||
|
final bool isLabelClickable;
|
||||||
|
final void Function(int id)? onTagSelected;
|
||||||
|
final void Function(int? id)? onCorrespondentSelected;
|
||||||
|
final void Function(int? id)? onDocumentTypeSelected;
|
||||||
|
final void Function(int? id)? onStoragePathSelected;
|
||||||
|
final Widget pageLoadingWidget;
|
||||||
|
|
||||||
|
const AdaptiveDocumentsView({
|
||||||
|
super.key,
|
||||||
|
required this.onTap,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.state,
|
||||||
|
required this.onSelected,
|
||||||
|
required this.hasInternetConnection,
|
||||||
|
this.isLabelClickable = true,
|
||||||
|
this.onTagSelected,
|
||||||
|
this.onCorrespondentSelected,
|
||||||
|
this.onDocumentTypeSelected,
|
||||||
|
this.onStoragePathSelected,
|
||||||
|
required this.pageLoadingWidget,
|
||||||
|
required this.beforeItems,
|
||||||
|
required this.viewType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(child: beforeItems),
|
||||||
|
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SliverList _buildListView() {
|
||||||
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
childCount: state.documents.length,
|
||||||
|
(context, index) {
|
||||||
|
final document = state.documents.elementAt(index);
|
||||||
|
return LabelRepositoriesProvider(
|
||||||
|
child: DocumentListItem(
|
||||||
|
isLabelClickable: isLabelClickable,
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: state.selectedIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isAtLeastOneSelected: state.selection.isNotEmpty,
|
||||||
|
isTagSelectedPredicate: (int tagId) {
|
||||||
|
return state.filter.tags is IdsTagsQuery
|
||||||
|
? (state.filter.tags as IdsTagsQuery)
|
||||||
|
.includedIds
|
||||||
|
.contains(tagId)
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
onCorrespondentSelected: onCorrespondentSelected,
|
||||||
|
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||||
|
onStoragePathSelected: onStoragePathSelected,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildGridView() {
|
||||||
|
return SliverGrid.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 178,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 1 / 2,
|
||||||
|
),
|
||||||
|
itemCount: state.documents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (state.hasLoaded &&
|
||||||
|
state.isLoading &&
|
||||||
|
index == state.documents.length) {
|
||||||
|
return Center(child: pageLoadingWidget);
|
||||||
|
}
|
||||||
|
final document = state.documents.elementAt(index);
|
||||||
|
return DocumentGridItem(
|
||||||
|
document: document,
|
||||||
|
onTap: onTap,
|
||||||
|
isSelected: state.selectedIds.contains(document.id),
|
||||||
|
onSelected: onSelected,
|
||||||
|
isAtLeastOneSelected: state.selection.isNotEmpty,
|
||||||
|
isTagSelectedPredicate: (int tagId) {
|
||||||
|
return state.filter.tags is IdsTagsQuery
|
||||||
|
? (state.filter.tags as IdsTagsQuery)
|
||||||
|
.includedIds
|
||||||
|
.contains(tagId)
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
onTagSelected: onTagSelected,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
|
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
|
|
||||||
class DocumentListView extends StatelessWidget {
|
|
||||||
final void Function(DocumentModel) onTap;
|
|
||||||
final void Function(DocumentModel) onSelected;
|
|
||||||
|
|
||||||
final PagingController<int, DocumentModel> pagingController;
|
|
||||||
final DocumentsState state;
|
|
||||||
final bool hasInternetConnection;
|
|
||||||
final bool isLabelClickable;
|
|
||||||
final void Function(int id)? onTagSelected;
|
|
||||||
final void Function(int? id)? onCorrespondentSelected;
|
|
||||||
final void Function(int? id)? onDocumentTypeSelected;
|
|
||||||
final void Function(int? id)? onStoragePathSelected;
|
|
||||||
|
|
||||||
const DocumentListView({
|
|
||||||
super.key,
|
|
||||||
required this.onTap,
|
|
||||||
required this.pagingController,
|
|
||||||
required this.state,
|
|
||||||
required this.onSelected,
|
|
||||||
required this.hasInternetConnection,
|
|
||||||
this.isLabelClickable = true,
|
|
||||||
this.onTagSelected,
|
|
||||||
this.onCorrespondentSelected,
|
|
||||||
this.onDocumentTypeSelected,
|
|
||||||
this.onStoragePathSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return PagedSliverList<int, DocumentModel>(
|
|
||||||
pagingController: pagingController,
|
|
||||||
builderDelegate: PagedChildBuilderDelegate(
|
|
||||||
animateTransitions: true,
|
|
||||||
itemBuilder: (context, document, index) {
|
|
||||||
return LabelRepositoriesProvider(
|
|
||||||
child: DocumentListItem(
|
|
||||||
isLabelClickable: isLabelClickable,
|
|
||||||
document: document,
|
|
||||||
onTap: onTap,
|
|
||||||
isSelected: state.selection.contains(document),
|
|
||||||
onSelected: onSelected,
|
|
||||||
isAtLeastOneSelected: state.selection.isNotEmpty,
|
|
||||||
isTagSelectedPredicate: (int tagId) {
|
|
||||||
return state.filter.tags is IdsTagsQuery
|
|
||||||
? (state.filter.tags as IdsTagsQuery)
|
|
||||||
.includedIds
|
|
||||||
.contains(tagId)
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
onTagSelected: onTagSelected,
|
|
||||||
onCorrespondentSelected: onCorrespondentSelected,
|
|
||||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
|
||||||
onStoragePathSelected: onStoragePathSelected,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection
|
|
||||||
? const DocumentsListLoadingWidget()
|
|
||||||
: const OfflineWidget(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,7 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
trailing: Text("${document.id}"),
|
||||||
dense: true,
|
dense: true,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
onTap: () => _onTap(),
|
onTap: () => _onTap(),
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NewItemsLoadingWidget extends StatelessWidget {
|
||||||
|
const NewItemsLoadingWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,3 +138,19 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
|
|||||||
return count > 99 ? "99+" : count.toString();
|
return count > 99 ? "99+" : count.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ScrollListener extends ChangeNotifier {
|
||||||
|
double top = 0;
|
||||||
|
double _last = 0;
|
||||||
|
|
||||||
|
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
|
||||||
|
controller.addListener(() {
|
||||||
|
final current = controller.offset;
|
||||||
|
top += _last - current;
|
||||||
|
if (top <= -height) top = -height;
|
||||||
|
if (top >= 0) top = 0;
|
||||||
|
_last = current;
|
||||||
|
if (top <= 0 && top >= -height) notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,46 +47,6 @@ class _HomePageState extends State<HomePage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
LocalNotificationService.instance.notifyTaskChanged(
|
|
||||||
Task(
|
|
||||||
id: 100,
|
|
||||||
dateCreated: DateTime.now(),
|
|
||||||
dateDone: DateTime.now(),
|
|
||||||
taskFileName: "test_file.pdf",
|
|
||||||
status: TaskStatus.started,
|
|
||||||
taskId: "abc-def-123-456",
|
|
||||||
type: "file",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Future.delayed(const Duration(seconds: 5), () {
|
|
||||||
LocalNotificationService.instance.notifyTaskChanged(
|
|
||||||
Task(
|
|
||||||
id: 100,
|
|
||||||
dateCreated: DateTime.now(),
|
|
||||||
dateDone: DateTime.now(),
|
|
||||||
taskFileName: "test_file.pdf",
|
|
||||||
status: TaskStatus.pending,
|
|
||||||
taskId: "abc-def-123-456",
|
|
||||||
type: "file",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Future.delayed(const Duration(seconds: 10), () {
|
|
||||||
LocalNotificationService.instance.notifyTaskChanged(
|
|
||||||
Task(
|
|
||||||
id: 100,
|
|
||||||
acknowledged: false,
|
|
||||||
dateCreated: DateTime.now(),
|
|
||||||
dateDone: DateTime.now(),
|
|
||||||
relatedDocumentId: 180,
|
|
||||||
result: "New document successfully created.",
|
|
||||||
status: TaskStatus.success,
|
|
||||||
taskFileName: "test_file.pdf",
|
|
||||||
taskId: "abc-def-123-456",
|
|
||||||
type: "file",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
_initializeData(context);
|
_initializeData(context);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
_listenForReceivedFiles();
|
_listenForReceivedFiles();
|
||||||
@@ -195,7 +155,14 @@ class _HomePageState extends State<HomePage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
BlocListener<TaskStatusCubit, TaskStatusState>(
|
BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||||
listener: (context, state) {},
|
listener: (context, state) {
|
||||||
|
if (state.task != null) {
|
||||||
|
// Handle local notifications on task change (only when app is running for now).
|
||||||
|
context
|
||||||
|
.read<LocalNotificationService>()
|
||||||
|
.notifyTaskChanged(state.task!);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
@@ -28,7 +27,7 @@ class InboxCubit extends Cubit<InboxState> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
final inboxDocuments = await _documentsApi
|
final inboxDocuments = await _documentsApi
|
||||||
.find(DocumentFilter(
|
.findAll(DocumentFilter(
|
||||||
tags: AnyAssignedTagsQuery(tagIds: inboxTags),
|
tags: AnyAssignedTagsQuery(tagIds: inboxTags),
|
||||||
sortField: SortField.added,
|
sortField: SortField.added,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class TagsWidget extends StatefulWidget {
|
|||||||
final Iterable<int> tagIds;
|
final Iterable<int> tagIds;
|
||||||
final bool isMultiLine;
|
final bool isMultiLine;
|
||||||
final VoidCallback? afterTagTapped;
|
final VoidCallback? afterTagTapped;
|
||||||
final void Function(int tagId) onTagSelected;
|
final void Function(int tagId)? onTagSelected;
|
||||||
final bool isClickable;
|
final bool isClickable;
|
||||||
final bool Function(int id) isSelectedPredicate;
|
final bool Function(int id) isSelectedPredicate;
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class _TagsWidgetState extends State<TagsWidget> {
|
|||||||
afterTagTapped: widget.afterTagTapped,
|
afterTagTapped: widget.afterTagTapped,
|
||||||
isClickable: widget.isClickable,
|
isClickable: widget.isClickable,
|
||||||
isSelected: widget.isSelectedPredicate(id),
|
isSelected: widget.isSelectedPredicate(id),
|
||||||
onSelected: () => widget.onTagSelected(id),
|
onSelected: () => widget.onTagSelected?.call(id),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
final documents = await _api.find(
|
final documents = await _api.findAll(
|
||||||
state.filter.copyWith(
|
state.filter.copyWith(
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
||||||
@@ -18,15 +17,6 @@ class LinkedDocumentsPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
||||||
final _pagingController =
|
|
||||||
PagingController<int, DocumentModel>(firstPageKey: 1);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_pagingController.nextPageKey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -38,8 +28,6 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
if (!state.isLoaded) {
|
if (!state.isLoaded) {
|
||||||
return const DocumentsListLoadingWidget();
|
return const DocumentsListLoadingWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pagingController.itemList = state.documents!.results;
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -48,42 +36,34 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomScrollView(
|
child: ListView.builder(
|
||||||
slivers: [
|
itemBuilder: (context, index) {
|
||||||
PagedSliverList<int, DocumentModel>(
|
return DocumentListItem(
|
||||||
pagingController: _pagingController,
|
isLabelClickable: false,
|
||||||
builderDelegate: PagedChildBuilderDelegate(
|
document: state.documents!.results.elementAt(index),
|
||||||
animateTransitions: true,
|
onTap: (doc) {
|
||||||
itemBuilder: (context, document, index) {
|
Navigator.push(
|
||||||
return DocumentListItem(
|
context,
|
||||||
isLabelClickable: false,
|
MaterialPageRoute(
|
||||||
document: document,
|
builder: (context) => BlocProvider(
|
||||||
onTap: (doc) {
|
create: (context) => DocumentDetailsCubit(
|
||||||
Navigator.push(
|
context.read<PaperlessDocumentsApi>(),
|
||||||
context,
|
state.documents!.results.elementAt(index),
|
||||||
MaterialPageRoute(
|
),
|
||||||
builder: (context) => BlocProvider(
|
child: const DocumentDetailsPage(
|
||||||
create: (context) => DocumentDetailsCubit(
|
isLabelClickable: false,
|
||||||
context.read<PaperlessDocumentsApi>(),
|
allowEdit: false,
|
||||||
document,
|
),
|
||||||
),
|
),
|
||||||
child: const DocumentDetailsPage(
|
),
|
||||||
isLabelClickable: false,
|
);
|
||||||
allowEdit: false,
|
},
|
||||||
),
|
isSelected: false,
|
||||||
),
|
isAtLeastOneSelected: false,
|
||||||
),
|
isTagSelectedPredicate: (_) => false,
|
||||||
);
|
onTagSelected: (int tag) {},
|
||||||
},
|
);
|
||||||
isSelected: false,
|
},
|
||||||
isAtLeastOneSelected: false,
|
|
||||||
isTagSelectedPredicate: (_) => false,
|
|
||||||
onTagSelected: (int tag) {},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
|
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
@@ -12,7 +12,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
|
|||||||
with HydratedMixin<AuthenticationState> {
|
with HydratedMixin<AuthenticationState> {
|
||||||
final LocalAuthenticationService _localAuthService;
|
final LocalAuthenticationService _localAuthService;
|
||||||
final PaperlessAuthenticationApi _authApi;
|
final PaperlessAuthenticationApi _authApi;
|
||||||
final AuthenticationAwareDioManager _dioWrapper;
|
final SessionManager _dioWrapper;
|
||||||
|
|
||||||
AuthenticationCubit(
|
AuthenticationCubit(
|
||||||
this._localAuthService,
|
this._localAuthService,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'package:injectable/injectable.dart';
|
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||||
|
|
||||||
@lazySingleton
|
|
||||||
class LocalAuthenticationService {
|
class LocalAuthenticationService {
|
||||||
final LocalVault localStore;
|
final LocalVault localStore;
|
||||||
final LocalAuthentication localAuthentication;
|
final LocalAuthentication localAuthentication;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
part 'notification_state.dart';
|
|
||||||
|
|
||||||
class NotificationCubit extends Cubit<NotificationState> {
|
|
||||||
NotificationCubit() : super(NotificationInitialState());
|
|
||||||
|
|
||||||
void navigateTo(String route, dynamic args) {}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
part of 'notification_cubit.dart';
|
|
||||||
|
|
||||||
abstract class NotificationState extends Equatable {
|
|
||||||
const NotificationState();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationInitialState extends NotificationState {}
|
|
||||||
|
|
||||||
class NotificationOpenDocumentDetailsPageState extends NotificationState {
|
|
||||||
final int documentId;
|
|
||||||
|
|
||||||
const NotificationOpenDocumentDetailsPageState(this.documentId);
|
|
||||||
}
|
|
||||||
@@ -11,9 +11,7 @@ class LocalNotificationService {
|
|||||||
final FlutterLocalNotificationsPlugin _plugin =
|
final FlutterLocalNotificationsPlugin _plugin =
|
||||||
FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
LocalNotificationService._();
|
LocalNotificationService();
|
||||||
|
|
||||||
static final LocalNotificationService instance = LocalNotificationService._();
|
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
@@ -71,7 +69,7 @@ class LocalNotificationService {
|
|||||||
body = task.taskFileName;
|
body = task.taskFileName;
|
||||||
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
|
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
|
||||||
payload = CreateDocumentSuccessNotificationResponsePayload(
|
payload = CreateDocumentSuccessNotificationResponsePayload(
|
||||||
task.relatedDocumentId!,
|
task.relatedDocument!,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
|
||||||
@@ -31,91 +32,97 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
|||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||||
builder: (context, connectivityState) {
|
builder: (context, connectivityState) {
|
||||||
final hasInternetConnection = connectivityState.isConnected;
|
final hasInternetConnection = connectivityState.isConnected;
|
||||||
return Column(
|
return SizedBox(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
height: height,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
builder: (context, state) {
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (!state.hasLoaded) {
|
children: [
|
||||||
return _buildLoadingWidget(context);
|
BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
}
|
builder: (context, state) {
|
||||||
if (state.value.isEmpty) {
|
if (!state.hasLoaded) {
|
||||||
return Text(S.of(context).savedViewsEmptyStateText);
|
return _buildLoadingWidget(context);
|
||||||
}
|
}
|
||||||
return SizedBox(
|
if (state.value.isEmpty) {
|
||||||
height: height,
|
return Text(S.of(context).savedViewsEmptyStateText);
|
||||||
child: ListView.separated(
|
}
|
||||||
itemCount: state.value.length,
|
return SizedBox(
|
||||||
scrollDirection: Axis.horizontal,
|
height: 38,
|
||||||
itemBuilder: (context, index) {
|
child: ListView.separated(
|
||||||
final view = state.value.values.elementAt(index);
|
itemCount: state.value.length,
|
||||||
return GestureDetector(
|
scrollDirection: Axis.horizontal,
|
||||||
onLongPress: hasInternetConnection
|
itemBuilder: (context, index) {
|
||||||
? () => _onDelete(context, view)
|
final view = state.value.values.elementAt(index);
|
||||||
: null,
|
return GestureDetector(
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
onLongPress: hasInternetConnection
|
||||||
builder: (context, docState) {
|
? () => _onDelete(context, view)
|
||||||
return FilterChip(
|
|
||||||
label: Text(
|
|
||||||
state.value.values.toList()[index].name,
|
|
||||||
),
|
|
||||||
selected: view.id == docState.selectedSavedViewId,
|
|
||||||
onSelected: enabled && hasInternetConnection
|
|
||||||
? (isSelected) =>
|
|
||||||
_onSelected(isSelected, context, view)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (context, index) => const SizedBox(
|
|
||||||
width: 4.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
S.of(context).savedViewsLabel,
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
buildWhen: (previous, current) =>
|
|
||||||
previous.filter != current.filter,
|
|
||||||
builder: (context, docState) {
|
|
||||||
return TextButton.icon(
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
onPressed: (enabled &&
|
|
||||||
state.hasLoaded &&
|
|
||||||
hasInternetConnection)
|
|
||||||
? () => _onCreatePressed(context, docState.filter)
|
|
||||||
: null,
|
: null,
|
||||||
label: Text(S.of(context).savedViewCreateNewLabel),
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
builder: (context, docState) {
|
||||||
|
final view = state.value.values.toList()[index];
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(
|
||||||
|
view.name,
|
||||||
|
),
|
||||||
|
selected:
|
||||||
|
view.id == docState.selectedSavedViewId,
|
||||||
|
onSelected: enabled && hasInternetConnection
|
||||||
|
? (isSelected) =>
|
||||||
|
_onSelected(isSelected, context, view)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(
|
||||||
|
width: 4.0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||||
],
|
builder: (context, state) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
S.of(context).savedViewsLabel,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
buildWhen: (previous, current) =>
|
||||||
|
previous.filter != current.filter,
|
||||||
|
builder: (context, docState) {
|
||||||
|
return TextButton.icon(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: (enabled &&
|
||||||
|
state.hasLoaded &&
|
||||||
|
hasInternetConnection)
|
||||||
|
? () =>
|
||||||
|
_onCreatePressed(context, docState.filter)
|
||||||
|
: null,
|
||||||
|
label: Text(S.of(context).savedViewCreateNewLabel),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoadingWidget(BuildContext context) {
|
Widget _buildLoadingWidget(BuildContext context) {
|
||||||
final r = Random(123456789);
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: height,
|
height: 38,
|
||||||
width: double.infinity,
|
width: MediaQuery.of(context).size.width,
|
||||||
child: Shimmer.fromColors(
|
child: Shimmer.fromColors(
|
||||||
baseColor: Theme.of(context).brightness == Brightness.light
|
baseColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[300]!
|
? Colors.grey[300]!
|
||||||
@@ -123,14 +130,35 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
|||||||
highlightColor: Theme.of(context).brightness == Brightness.light
|
highlightColor: Theme.of(context).brightness == Brightness.light
|
||||||
? Colors.grey[100]!
|
? Colors.grey[100]!
|
||||||
: Colors.grey[600]!,
|
: Colors.grey[600]!,
|
||||||
child: ListView.separated(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: 10,
|
children: [
|
||||||
itemBuilder: (context, index) => FilterChip(
|
FilterChip(
|
||||||
label: SizedBox(width: r.nextInt((index * 20) + 50).toDouble()),
|
label: const SizedBox(width: 32),
|
||||||
onSelected: null),
|
onSelected: (_) {},
|
||||||
separatorBuilder: (context, index) => const SizedBox(width: 4.0),
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
FilterChip(
|
||||||
|
label: const SizedBox(width: 64),
|
||||||
|
onSelected: (_) {},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
FilterChip(
|
||||||
|
label: const SizedBox(width: 100),
|
||||||
|
onSelected: (_) {},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
FilterChip(
|
||||||
|
label: const SizedBox(width: 32),
|
||||||
|
onSelected: (_) {},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4.0),
|
||||||
|
FilterChip(
|
||||||
|
label: const SizedBox(width: 48),
|
||||||
|
onSelected: (_) {},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import 'package:paperless_mobile/features/documents/view/pages/document_view.dar
|
|||||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
|
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart';
|
import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -140,7 +141,7 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
final file = await _assembleFileBytes(
|
final file = await _assembleFileBytes(
|
||||||
context.read<DocumentScannerCubit>().state,
|
context.read<DocumentScannerCubit>().state,
|
||||||
);
|
);
|
||||||
final uploaded = await Navigator.of(context).push(
|
final taskId = await Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => LabelRepositoriesProvider(
|
builder: (_) => LabelRepositoriesProvider(
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
@@ -165,8 +166,10 @@ class _ScannerPageState extends State<ScannerPage>
|
|||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
false;
|
false;
|
||||||
if (uploaded) {
|
if (taskId != null) {
|
||||||
|
// For paperless version older than 1.11.3, task id will always be null!
|
||||||
context.read<DocumentScannerCubit>().reset();
|
context.read<DocumentScannerCubit>().reset();
|
||||||
|
context.read<TaskStatusCubit>().listenToTaskChanges(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,30 @@ class TaskStatusCubit extends Cubit<TaskStatusState> {
|
|||||||
final PaperlessTasksApi _api;
|
final PaperlessTasksApi _api;
|
||||||
TaskStatusCubit(this._api) : super(const TaskStatusState());
|
TaskStatusCubit(this._api) : super(const TaskStatusState());
|
||||||
|
|
||||||
void startListeningToTask(String taskId) {
|
void listenToTaskChanges(String taskId) {
|
||||||
_api
|
_api
|
||||||
.listenForTaskChanges(taskId)
|
.listenForTaskChanges(taskId)
|
||||||
.forEach(
|
.forEach(
|
||||||
(element) => TaskStatusState(
|
(element) => emit(
|
||||||
isListening: true,
|
TaskStatusState(
|
||||||
isAcknowledged: false,
|
isListening: true,
|
||||||
task: element,
|
task: element,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.whenComplete(() => emit(state.copyWith(isListening: false)));
|
.whenComplete(() => emit(state.copyWith(isListening: false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void acknowledgeCurrentTask() {
|
Future<void> acknowledgeCurrentTask() async {
|
||||||
emit(state.copyWith(isListening: false, isAcknowledged: true));
|
if (state.task == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final task = await _api.acknowledgeTask(state.task!);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
task: task,
|
||||||
|
isListening: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,20 @@ part of 'task_status_cubit.dart';
|
|||||||
class TaskStatusState extends Equatable {
|
class TaskStatusState extends Equatable {
|
||||||
final Task? task;
|
final Task? task;
|
||||||
final bool isListening;
|
final bool isListening;
|
||||||
final bool isAcknowledged;
|
|
||||||
|
|
||||||
const TaskStatusState({
|
const TaskStatusState({
|
||||||
this.task,
|
this.task,
|
||||||
this.isListening = false,
|
this.isListening = false,
|
||||||
this.isAcknowledged = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isActive => isListening && !isAcknowledged;
|
|
||||||
|
|
||||||
bool get isSuccess => task?.status == TaskStatus.success;
|
bool get isSuccess => task?.status == TaskStatus.success;
|
||||||
|
|
||||||
|
bool get isAcknowledged => task?.acknowledged ?? false;
|
||||||
|
|
||||||
String? get taskId => task?.taskId;
|
String? get taskId => task?.taskId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [];
|
List<Object?> get props => [task, isListening];
|
||||||
|
|
||||||
TaskStatusState copyWith({
|
TaskStatusState copyWith({
|
||||||
Task? task,
|
Task? task,
|
||||||
@@ -28,7 +26,6 @@ class TaskStatusState extends Equatable {
|
|||||||
return TaskStatusState(
|
return TaskStatusState(
|
||||||
task: task ?? this.task,
|
task: task ?? this.task,
|
||||||
isListening: isListening ?? this.isListening,
|
isListening: isListening ?? this.isListening,
|
||||||
isAcknowledged: isAcknowledged ?? this.isAcknowledged,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,8 @@
|
|||||||
"@editLabelPageConfirmDeletionDialogTitle": {},
|
"@editLabelPageConfirmDeletionDialogTitle": {},
|
||||||
"editLabelPageDeletionDialogText": "Dokumenty mají přiřazen tento štítek. Odstraněním štítku bude označení odstraněno. Pokračovat?",
|
"editLabelPageDeletionDialogText": "Dokumenty mají přiřazen tento štítek. Odstraněním štítku bude označení odstraněno. Pokračovat?",
|
||||||
"@editLabelPageDeletionDialogText": {},
|
"@editLabelPageDeletionDialogText": {},
|
||||||
|
"errorMessageAcknowledgeTasksError": "",
|
||||||
|
"@errorMessageAcknowledgeTasksError": {},
|
||||||
"errorMessageAuthenticationFailed": "Přihlášení selhalo, zkuste to znovu.",
|
"errorMessageAuthenticationFailed": "Přihlášení selhalo, zkuste to znovu.",
|
||||||
"@errorMessageAuthenticationFailed": {},
|
"@errorMessageAuthenticationFailed": {},
|
||||||
"errorMessageAutocompleteQueryError": "Při automatickém doplnění požadavku došlo k chybě.",
|
"errorMessageAutocompleteQueryError": "Při automatickém doplnění požadavku došlo k chybě.",
|
||||||
@@ -250,6 +252,8 @@
|
|||||||
"@errorMessageStoragePathCreateFailed": {},
|
"@errorMessageStoragePathCreateFailed": {},
|
||||||
"errorMessageStoragePathLoadFailed": "Nelze načíst cestu k úložišti.",
|
"errorMessageStoragePathLoadFailed": "Nelze načíst cestu k úložišti.",
|
||||||
"@errorMessageStoragePathLoadFailed": {},
|
"@errorMessageStoragePathLoadFailed": {},
|
||||||
|
"errorMessageSuggestionsQueryError": "",
|
||||||
|
"@errorMessageSuggestionsQueryError": {},
|
||||||
"errorMessageTagCreateFailed": "Nelze vytvořit tag, zkuste to znovu.",
|
"errorMessageTagCreateFailed": "Nelze vytvořit tag, zkuste to znovu.",
|
||||||
"@errorMessageTagCreateFailed": {},
|
"@errorMessageTagCreateFailed": {},
|
||||||
"errorMessageTagLoadFailed": "Nelze načíst tagy.",
|
"errorMessageTagLoadFailed": "Nelze načíst tagy.",
|
||||||
@@ -260,25 +264,25 @@
|
|||||||
"@errorMessageUnsupportedFileFormat": {},
|
"@errorMessageUnsupportedFileFormat": {},
|
||||||
"errorReportLabel": "NAHLÁSIT",
|
"errorReportLabel": "NAHLÁSIT",
|
||||||
"@errorReportLabel": {},
|
"@errorReportLabel": {},
|
||||||
"extendedDateRangeDialogAbsoluteLabel": "Absolute",
|
"extendedDateRangeDialogAbsoluteLabel": "",
|
||||||
"@extendedDateRangeDialogAbsoluteLabel": {},
|
"@extendedDateRangeDialogAbsoluteLabel": {},
|
||||||
"extendedDateRangeDialogHintText": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.",
|
"extendedDateRangeDialogHintText": "",
|
||||||
"@extendedDateRangeDialogHintText": {},
|
"@extendedDateRangeDialogHintText": {},
|
||||||
"extendedDateRangeDialogRelativeAmountLabel": "Amount",
|
"extendedDateRangeDialogRelativeAmountLabel": "",
|
||||||
"@extendedDateRangeDialogRelativeAmountLabel": {},
|
"@extendedDateRangeDialogRelativeAmountLabel": {},
|
||||||
"extendedDateRangeDialogRelativeLabel": "Relative",
|
"extendedDateRangeDialogRelativeLabel": "",
|
||||||
"@extendedDateRangeDialogRelativeLabel": {},
|
"@extendedDateRangeDialogRelativeLabel": {},
|
||||||
"extendedDateRangeDialogRelativeLastLabel": "Last",
|
"extendedDateRangeDialogRelativeLastLabel": "",
|
||||||
"@extendedDateRangeDialogRelativeLastLabel": {},
|
"@extendedDateRangeDialogRelativeLastLabel": {},
|
||||||
"extendedDateRangeDialogRelativeTimeUnitLabel": "Time unit",
|
"extendedDateRangeDialogRelativeTimeUnitLabel": "",
|
||||||
"@extendedDateRangeDialogRelativeTimeUnitLabel": {},
|
"@extendedDateRangeDialogRelativeTimeUnitLabel": {},
|
||||||
"extendedDateRangeDialogTitle": "Select date range",
|
"extendedDateRangeDialogTitle": "",
|
||||||
"@extendedDateRangeDialogTitle": {},
|
"@extendedDateRangeDialogTitle": {},
|
||||||
"extendedDateRangePickerAfterLabel": "After",
|
"extendedDateRangePickerAfterLabel": "",
|
||||||
"@extendedDateRangePickerAfterLabel": {},
|
"@extendedDateRangePickerAfterLabel": {},
|
||||||
"extendedDateRangePickerBeforeLabel": "Before",
|
"extendedDateRangePickerBeforeLabel": "",
|
||||||
"@extendedDateRangePickerBeforeLabel": {},
|
"@extendedDateRangePickerBeforeLabel": {},
|
||||||
"extendedDateRangePickerDayText": "{count, plural, zero{} one{day} other{days}}",
|
"extendedDateRangePickerDayText": "{count, plural, other{}}",
|
||||||
"@extendedDateRangePickerDayText": {
|
"@extendedDateRangePickerDayText": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
@@ -298,9 +302,9 @@
|
|||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extendedDateRangePickerLastText": "Last",
|
"extendedDateRangePickerLastText": "",
|
||||||
"@extendedDateRangePickerLastText": {},
|
"@extendedDateRangePickerLastText": {},
|
||||||
"extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}",
|
"extendedDateRangePickerLastWeeksLabel": "{count, plural, other{}}",
|
||||||
"@extendedDateRangePickerLastWeeksLabel": {
|
"@extendedDateRangePickerLastWeeksLabel": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
@@ -312,7 +316,7 @@
|
|||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}",
|
"extendedDateRangePickerMonthText": "{count, plural, other{}}",
|
||||||
"@extendedDateRangePickerMonthText": {
|
"@extendedDateRangePickerMonthText": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
@@ -320,13 +324,13 @@
|
|||||||
},
|
},
|
||||||
"extendedDateRangePickerToLabel": "Do",
|
"extendedDateRangePickerToLabel": "Do",
|
||||||
"@extendedDateRangePickerToLabel": {},
|
"@extendedDateRangePickerToLabel": {},
|
||||||
"extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}",
|
"extendedDateRangePickerWeekText": "{count, plural, other{}}",
|
||||||
"@extendedDateRangePickerWeekText": {
|
"@extendedDateRangePickerWeekText": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}",
|
"extendedDateRangePickerYearText": "{count, plural, other{}}",
|
||||||
"@extendedDateRangePickerYearText": {
|
"@extendedDateRangePickerYearText": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
@@ -428,7 +432,7 @@
|
|||||||
"@loginPageClientCertificateSettingLabel": {},
|
"@loginPageClientCertificateSettingLabel": {},
|
||||||
"loginPageClientCertificateSettingSelectFileText": "Vybrat soubor...",
|
"loginPageClientCertificateSettingSelectFileText": "Vybrat soubor...",
|
||||||
"@loginPageClientCertificateSettingSelectFileText": {},
|
"@loginPageClientCertificateSettingSelectFileText": {},
|
||||||
"loginPageContinueLabel": "Continue",
|
"loginPageContinueLabel": "",
|
||||||
"@loginPageContinueLabel": {},
|
"@loginPageContinueLabel": {},
|
||||||
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Chybná nebo chybějící heslová fráze certifikátu.",
|
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Chybná nebo chybějící heslová fráze certifikátu.",
|
||||||
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
|
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
|
||||||
@@ -438,27 +442,27 @@
|
|||||||
"@loginPagePasswordFieldLabel": {},
|
"@loginPagePasswordFieldLabel": {},
|
||||||
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
|
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
|
||||||
"@loginPagePasswordValidatorMessageText": {},
|
"@loginPagePasswordValidatorMessageText": {},
|
||||||
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
|
"loginPageReachabilityInvalidClientCertificateConfigurationText": "",
|
||||||
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
||||||
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
|
"loginPageReachabilityMissingClientCertificateText": "",
|
||||||
"@loginPageReachabilityMissingClientCertificateText": {},
|
"@loginPageReachabilityMissingClientCertificateText": {},
|
||||||
"loginPageReachabilityNotReachableText": "Could not establish a connection to the server.",
|
"loginPageReachabilityNotReachableText": "",
|
||||||
"@loginPageReachabilityNotReachableText": {},
|
"@loginPageReachabilityNotReachableText": {},
|
||||||
"loginPageReachabilitySuccessText": "Connection successfully established.",
|
"loginPageReachabilitySuccessText": "",
|
||||||
"@loginPageReachabilitySuccessText": {},
|
"@loginPageReachabilitySuccessText": {},
|
||||||
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.",
|
"loginPageReachabilityUnresolvedHostText": "",
|
||||||
"@loginPageReachabilityUnresolvedHostText": {},
|
"@loginPageReachabilityUnresolvedHostText": {},
|
||||||
"loginPageServerUrlFieldLabel": "'Adresa serveru",
|
"loginPageServerUrlFieldLabel": "'Adresa serveru",
|
||||||
"@loginPageServerUrlFieldLabel": {},
|
"@loginPageServerUrlFieldLabel": {},
|
||||||
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
|
"loginPageServerUrlValidatorMessageInvalidAddressText": "",
|
||||||
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
||||||
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
|
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
|
||||||
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
||||||
"loginPageSignInButtonLabel": "Sign In",
|
"loginPageSignInButtonLabel": "",
|
||||||
"@loginPageSignInButtonLabel": {},
|
"@loginPageSignInButtonLabel": {},
|
||||||
"loginPageSignInTitle": "Sign In",
|
"loginPageSignInTitle": "",
|
||||||
"@loginPageSignInTitle": {},
|
"@loginPageSignInTitle": {},
|
||||||
"loginPageSignInToPrefixText": "Sign in to {serverAddress}",
|
"loginPageSignInToPrefixText": "",
|
||||||
"@loginPageSignInToPrefixText": {
|
"@loginPageSignInToPrefixText": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"serverAddress": {}
|
"serverAddress": {}
|
||||||
@@ -476,7 +480,7 @@
|
|||||||
"@onboardingDoneButtonLabel": {},
|
"@onboardingDoneButtonLabel": {},
|
||||||
"onboardingNextButtonLabel": "Další",
|
"onboardingNextButtonLabel": "Další",
|
||||||
"@onboardingNextButtonLabel": {},
|
"@onboardingNextButtonLabel": {},
|
||||||
"receiveSharedFilePermissionDeniedMessage": "Could not access the received file. Please try to open the app before sharing.",
|
"receiveSharedFilePermissionDeniedMessage": "",
|
||||||
"@receiveSharedFilePermissionDeniedMessage": {},
|
"@receiveSharedFilePermissionDeniedMessage": {},
|
||||||
"referencedDocumentsReadOnlyHintText": "Tento náhled nelze upravovat! Nelze upravovat nebo odstraňovat dokumenty. Bude načteno maximálně 100 odkazovaných dokumentů.",
|
"referencedDocumentsReadOnlyHintText": "Tento náhled nelze upravovat! Nelze upravovat nebo odstraňovat dokumenty. Bude načteno maximálně 100 odkazovaných dokumentů.",
|
||||||
"@referencedDocumentsReadOnlyHintText": {},
|
"@referencedDocumentsReadOnlyHintText": {},
|
||||||
@@ -540,12 +544,12 @@
|
|||||||
"@tagInboxTagPropertyLabel": {},
|
"@tagInboxTagPropertyLabel": {},
|
||||||
"uploadPageAutomaticallInferredFieldsHintText": "Pokud specifikuješ hodnoty pro tato pole, paperless instance nebude automaticky přiřazovat naučené hodnoty. Pokud mají být tato pole automaticky vyplňována, nevyplňujte zde nic.",
|
"uploadPageAutomaticallInferredFieldsHintText": "Pokud specifikuješ hodnoty pro tato pole, paperless instance nebude automaticky přiřazovat naučené hodnoty. Pokud mají být tato pole automaticky vyplňována, nevyplňujte zde nic.",
|
||||||
"@uploadPageAutomaticallInferredFieldsHintText": {},
|
"@uploadPageAutomaticallInferredFieldsHintText": {},
|
||||||
"verifyIdentityPageDescriptionText": "Use the configured biometric factor to authenticate and unlock your documents.",
|
"verifyIdentityPageDescriptionText": "",
|
||||||
"@verifyIdentityPageDescriptionText": {},
|
"@verifyIdentityPageDescriptionText": {},
|
||||||
"verifyIdentityPageLogoutButtonLabel": "Disconnect",
|
"verifyIdentityPageLogoutButtonLabel": "",
|
||||||
"@verifyIdentityPageLogoutButtonLabel": {},
|
"@verifyIdentityPageLogoutButtonLabel": {},
|
||||||
"verifyIdentityPageTitle": "Verify your identity",
|
"verifyIdentityPageTitle": "",
|
||||||
"@verifyIdentityPageTitle": {},
|
"@verifyIdentityPageTitle": {},
|
||||||
"verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity",
|
"verifyIdentityPageVerifyIdentityButtonLabel": "",
|
||||||
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
|
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
|
||||||
}
|
}
|
||||||
@@ -194,6 +194,8 @@
|
|||||||
"@editLabelPageConfirmDeletionDialogTitle": {},
|
"@editLabelPageConfirmDeletionDialogTitle": {},
|
||||||
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
|
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
|
||||||
"@editLabelPageDeletionDialogText": {},
|
"@editLabelPageDeletionDialogText": {},
|
||||||
|
"errorMessageAcknowledgeTasksError": "Dateiaufgabe konnte nicht verworfen werden.",
|
||||||
|
"@errorMessageAcknowledgeTasksError": {},
|
||||||
"errorMessageAuthenticationFailed": "Authentifizierung fehlgeschlagen, bitte versuche es erneut.",
|
"errorMessageAuthenticationFailed": "Authentifizierung fehlgeschlagen, bitte versuche es erneut.",
|
||||||
"@errorMessageAuthenticationFailed": {},
|
"@errorMessageAuthenticationFailed": {},
|
||||||
"errorMessageAutocompleteQueryError": "Beim automatischen Vervollständigen ist ein Fehler aufgetreten.",
|
"errorMessageAutocompleteQueryError": "Beim automatischen Vervollständigen ist ein Fehler aufgetreten.",
|
||||||
@@ -250,6 +252,8 @@
|
|||||||
"@errorMessageStoragePathCreateFailed": {},
|
"@errorMessageStoragePathCreateFailed": {},
|
||||||
"errorMessageStoragePathLoadFailed": "Speicherpfade konnten nicht geladen werden.",
|
"errorMessageStoragePathLoadFailed": "Speicherpfade konnten nicht geladen werden.",
|
||||||
"@errorMessageStoragePathLoadFailed": {},
|
"@errorMessageStoragePathLoadFailed": {},
|
||||||
|
"errorMessageSuggestionsQueryError": "Vorschläge konnten nicht geladen werden.",
|
||||||
|
"@errorMessageSuggestionsQueryError": {},
|
||||||
"errorMessageTagCreateFailed": "Tag konnte nicht erstellt werden, bitte versuche es erneut.",
|
"errorMessageTagCreateFailed": "Tag konnte nicht erstellt werden, bitte versuche es erneut.",
|
||||||
"@errorMessageTagCreateFailed": {},
|
"@errorMessageTagCreateFailed": {},
|
||||||
"errorMessageTagLoadFailed": "Tags konnten nicht geladen werden.",
|
"errorMessageTagLoadFailed": "Tags konnten nicht geladen werden.",
|
||||||
|
|||||||
@@ -194,6 +194,8 @@
|
|||||||
"@editLabelPageConfirmDeletionDialogTitle": {},
|
"@editLabelPageConfirmDeletionDialogTitle": {},
|
||||||
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
|
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
|
||||||
"@editLabelPageDeletionDialogText": {},
|
"@editLabelPageDeletionDialogText": {},
|
||||||
|
"errorMessageAcknowledgeTasksError": "Could not acknowledge tasks.",
|
||||||
|
"@errorMessageAcknowledgeTasksError": {},
|
||||||
"errorMessageAuthenticationFailed": "Authentication failed, please try again.",
|
"errorMessageAuthenticationFailed": "Authentication failed, please try again.",
|
||||||
"@errorMessageAuthenticationFailed": {},
|
"@errorMessageAuthenticationFailed": {},
|
||||||
"errorMessageAutocompleteQueryError": "An error ocurred while trying to autocomplete your query.",
|
"errorMessageAutocompleteQueryError": "An error ocurred while trying to autocomplete your query.",
|
||||||
@@ -250,6 +252,8 @@
|
|||||||
"@errorMessageStoragePathCreateFailed": {},
|
"@errorMessageStoragePathCreateFailed": {},
|
||||||
"errorMessageStoragePathLoadFailed": "Could not load storage paths.",
|
"errorMessageStoragePathLoadFailed": "Could not load storage paths.",
|
||||||
"@errorMessageStoragePathLoadFailed": {},
|
"@errorMessageStoragePathLoadFailed": {},
|
||||||
|
"errorMessageSuggestionsQueryError": "Could not load suggestions.",
|
||||||
|
"@errorMessageSuggestionsQueryError": {},
|
||||||
"errorMessageTagCreateFailed": "Could not create tag, please try again.",
|
"errorMessageTagCreateFailed": "Could not create tag, please try again.",
|
||||||
"@errorMessageTagCreateFailed": {},
|
"@errorMessageTagCreateFailed": {},
|
||||||
"errorMessageTagLoadFailed": "Could not load tags.",
|
"errorMessageTagLoadFailed": "Could not load tags.",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
|
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
|
||||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
|
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
|
||||||
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
|
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
|
||||||
@@ -27,7 +26,7 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi
|
|||||||
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
import 'package:paperless_mobile/core/service/dio_file_service.dart';
|
import 'package:paperless_mobile/core/service/dio_file_service.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
@@ -46,7 +45,6 @@ import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
|||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ void main() async {
|
|||||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await findSystemLocale();
|
await findSystemLocale();
|
||||||
await LocalNotificationService.instance.initialize();
|
|
||||||
|
|
||||||
// Initialize External dependencies
|
// Initialize External dependencies
|
||||||
final connectivity = Connectivity();
|
final connectivity = Connectivity();
|
||||||
@@ -78,27 +75,18 @@ void main() async {
|
|||||||
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
||||||
appSettingsCubit.state.preferredLocaleSubtag,
|
appSettingsCubit.state.preferredLocaleSubtag,
|
||||||
);
|
);
|
||||||
// Required for self signed client certificates
|
// Manages security context, required for self signed client certificates
|
||||||
final dioWrapper = AuthenticationAwareDioManager([
|
final sessionManager = SessionManager([languageHeaderInterceptor]);
|
||||||
DioHttpErrorInterceptor(),
|
|
||||||
PrettyDioLogger(
|
|
||||||
compact: true,
|
|
||||||
responseBody: false,
|
|
||||||
responseHeader: false,
|
|
||||||
request: false,
|
|
||||||
requestBody: false,
|
|
||||||
requestHeader: false,
|
|
||||||
),
|
|
||||||
languageHeaderInterceptor,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Initialize Paperless APIs
|
// Initialize Paperless APIs
|
||||||
final authApi = PaperlessAuthenticationApiImpl(dioWrapper.client);
|
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
|
||||||
final documentsApi = PaperlessDocumentsApiImpl(dioWrapper.client);
|
final documentsApi = PaperlessDocumentsApiImpl(sessionManager.client);
|
||||||
final labelsApi = PaperlessLabelApiImpl(dioWrapper.client);
|
final labelsApi = PaperlessLabelApiImpl(sessionManager.client);
|
||||||
final statsApi = PaperlessServerStatsApiImpl(dioWrapper.client);
|
final statsApi = PaperlessServerStatsApiImpl(sessionManager.client);
|
||||||
final savedViewsApi = PaperlessSavedViewsApiImpl(dioWrapper.client);
|
final savedViewsApi = PaperlessSavedViewsApiImpl(sessionManager.client);
|
||||||
final tasksApi = PaperlessTasksApiImpl(dioWrapper.client);
|
final tasksApi = PaperlessTasksApiImpl(
|
||||||
|
sessionManager.client,
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize Blocs/Cubits
|
// Initialize Blocs/Cubits
|
||||||
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
||||||
@@ -119,20 +107,23 @@ void main() async {
|
|||||||
final authCubit = AuthenticationCubit(
|
final authCubit = AuthenticationCubit(
|
||||||
localAuthService,
|
localAuthService,
|
||||||
authApi,
|
authApi,
|
||||||
dioWrapper,
|
sessionManager,
|
||||||
);
|
);
|
||||||
await authCubit
|
await authCubit
|
||||||
.restoreSessionState(appSettingsCubit.state.isLocalAuthenticationEnabled);
|
.restoreSessionState(appSettingsCubit.state.isLocalAuthenticationEnabled);
|
||||||
|
|
||||||
if (authCubit.state.isAuthenticated) {
|
if (authCubit.state.isAuthenticated) {
|
||||||
final auth = authCubit.state.authentication!;
|
final auth = authCubit.state.authentication!;
|
||||||
dioWrapper.updateSettings(
|
sessionManager.updateSettings(
|
||||||
baseUrl: auth.serverUrl,
|
baseUrl: auth.serverUrl,
|
||||||
authToken: auth.token,
|
authToken: auth.token,
|
||||||
clientCertificate: auth.clientCertificate,
|
clientCertificate: auth.clientCertificate,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final localNotificationService = LocalNotificationService();
|
||||||
|
await localNotificationService.initialize();
|
||||||
|
|
||||||
//Update language header in interceptor on language change.
|
//Update language header in interceptor on language change.
|
||||||
appSettingsCubit.stream.listen((event) => languageHeaderInterceptor
|
appSettingsCubit.stream.listen((event) => languageHeaderInterceptor
|
||||||
.preferredLocaleSubtag = event.preferredLocaleSubtag);
|
.preferredLocaleSubtag = event.preferredLocaleSubtag);
|
||||||
@@ -149,7 +140,7 @@ void main() async {
|
|||||||
create: (context) => cm.CacheManager(
|
create: (context) => cm.CacheManager(
|
||||||
cm.Config(
|
cm.Config(
|
||||||
'cacheKey',
|
'cacheKey',
|
||||||
fileService: DioFileService(dioWrapper.client),
|
fileService: DioFileService(sessionManager.client),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -157,6 +148,9 @@ void main() async {
|
|||||||
Provider<ConnectivityStatusService>.value(
|
Provider<ConnectivityStatusService>.value(
|
||||||
value: connectivityStatusService,
|
value: connectivityStatusService,
|
||||||
),
|
),
|
||||||
|
Provider<LocalNotificationService>.value(
|
||||||
|
value: localNotificationService,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: MultiRepositoryProvider(
|
child: MultiRepositoryProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -221,6 +215,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
vertical: 16.0,
|
vertical: 16.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
chipTheme: ChipThemeData(
|
chipTheme: ChipThemeData(
|
||||||
backgroundColor: Colors.lightGreen[50],
|
backgroundColor: Colors.lightGreen[50],
|
||||||
),
|
),
|
||||||
@@ -242,6 +237,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
vertical: 16.0,
|
vertical: 16.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
chipTheme: ChipThemeData(
|
chipTheme: ChipThemeData(
|
||||||
backgroundColor: Colors.green[900],
|
backgroundColor: Colors.green[900],
|
||||||
),
|
),
|
||||||
@@ -252,7 +248,9 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => PaperlessServerInformationCubit(context.read()),
|
create: (context) => PaperlessServerInformationCubit(
|
||||||
|
context.read<PaperlessServerStatsApi>(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||||
@@ -260,14 +258,8 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: true,
|
debugShowCheckedModeBanner: true,
|
||||||
title: "Paperless Mobile",
|
title: "Paperless Mobile",
|
||||||
theme: _lightTheme.copyWith(
|
theme: _lightTheme,
|
||||||
listTileTheme: _lightTheme.listTileTheme
|
darkTheme: _darkTheme,
|
||||||
.copyWith(tileColor: Colors.transparent),
|
|
||||||
),
|
|
||||||
darkTheme: _darkTheme.copyWith(
|
|
||||||
listTileTheme: _darkTheme.listTileTheme
|
|
||||||
.copyWith(tileColor: Colors.transparent),
|
|
||||||
),
|
|
||||||
themeMode: settings.preferredThemeMode,
|
themeMode: settings.preferredThemeMode,
|
||||||
supportedLocales: S.delegate.supportedLocales,
|
supportedLocales: S.delegate.supportedLocales,
|
||||||
locale: Locale.fromSubtags(
|
locale: Locale.fromSubtags(
|
||||||
|
|||||||
@@ -131,6 +131,6 @@ class DocumentModel extends Equatable {
|
|||||||
archiveSerialNumber,
|
archiveSerialNumber,
|
||||||
originalFileName,
|
originalFileName,
|
||||||
archivedFileName,
|
archivedFileName,
|
||||||
storagePath
|
storagePath,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
45
packages/paperless_api/lib/src/models/field_suggestions.dart
Normal file
45
packages/paperless_api/lib/src/models/field_suggestions.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'field_suggestions.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||||
|
class FieldSuggestions {
|
||||||
|
final Iterable<int> correspondents;
|
||||||
|
final Iterable<int> tags;
|
||||||
|
final Iterable<int> documentTypes;
|
||||||
|
final Iterable<int> storagePaths;
|
||||||
|
final Iterable<DateTime> dates;
|
||||||
|
|
||||||
|
const FieldSuggestions({
|
||||||
|
this.correspondents = const [],
|
||||||
|
this.tags = const [],
|
||||||
|
this.documentTypes = const [],
|
||||||
|
this.storagePaths = const [],
|
||||||
|
this.dates = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get hasSuggestedCorrespondents => correspondents.isNotEmpty;
|
||||||
|
bool get hasSuggestedTags => tags.isNotEmpty;
|
||||||
|
bool get hasSuggestedDocumentTypes => documentTypes.isNotEmpty;
|
||||||
|
bool get hasSuggestedStoragePaths => storagePaths.isNotEmpty;
|
||||||
|
bool get hasSuggestedDates => dates.isNotEmpty;
|
||||||
|
|
||||||
|
bool get hasSuggestions =>
|
||||||
|
hasSuggestedCorrespondents ||
|
||||||
|
hasSuggestedDates ||
|
||||||
|
hasSuggestedTags ||
|
||||||
|
hasSuggestedStoragePaths ||
|
||||||
|
hasSuggestedDocumentTypes;
|
||||||
|
|
||||||
|
int get suggestionsCount =>
|
||||||
|
(correspondents.isNotEmpty ? 1 : 0) +
|
||||||
|
(tags.isNotEmpty ? 1 : 0) +
|
||||||
|
(documentTypes.isNotEmpty ? 1 : 0) +
|
||||||
|
(storagePaths.isNotEmpty ? 1 : 0) +
|
||||||
|
(dates.isNotEmpty ? 1 : 0);
|
||||||
|
|
||||||
|
factory FieldSuggestions.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$FieldSuggestionsFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$FieldSuggestionsToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'field_suggestions.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
FieldSuggestions _$FieldSuggestionsFromJson(Map<String, dynamic> json) =>
|
||||||
|
FieldSuggestions(
|
||||||
|
correspondents:
|
||||||
|
(json['correspondents'] as List<dynamic>?)?.map((e) => e as int) ??
|
||||||
|
const [],
|
||||||
|
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as int) ?? const [],
|
||||||
|
documentTypes:
|
||||||
|
(json['document_types'] as List<dynamic>?)?.map((e) => e as int) ??
|
||||||
|
const [],
|
||||||
|
storagePaths:
|
||||||
|
(json['storage_paths'] as List<dynamic>?)?.map((e) => e as int) ??
|
||||||
|
const [],
|
||||||
|
dates: (json['dates'] as List<dynamic>?)
|
||||||
|
?.map((e) => DateTime.parse(e as String)) ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$FieldSuggestionsToJson(FieldSuggestions instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'correspondents': instance.correspondents.toList(),
|
||||||
|
'tags': instance.tags.toList(),
|
||||||
|
'document_types': instance.documentTypes.toList(),
|
||||||
|
'storage_paths': instance.storagePaths.toList(),
|
||||||
|
'dates': instance.dates.map((e) => e.toIso8601String()).toList(),
|
||||||
|
};
|
||||||
@@ -346,15 +346,17 @@ class FilterRule with EquatableMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Join values of all extended filter rules
|
//Join values of all extended filter rules if exist
|
||||||
final FilterRule extendedFilterRule = filterRules
|
if (filterRules.isNotEmpty) {
|
||||||
.where((r) => r.ruleType == extendedRule)
|
final FilterRule extendedFilterRule = filterRules
|
||||||
.reduce((previousValue, element) => previousValue.copyWith(
|
.where((r) => r.ruleType == extendedRule)
|
||||||
value: previousValue.value! + element.value!,
|
.reduce((previousValue, element) => previousValue.copyWith(
|
||||||
));
|
value: previousValue.value! + element.value!,
|
||||||
filterRules
|
));
|
||||||
..removeWhere((element) => element.ruleType == extendedRule)
|
filterRules
|
||||||
..add(extendedFilterRule);
|
..removeWhere((element) => element.ruleType == extendedRule)
|
||||||
|
..add(extendedFilterRule);
|
||||||
|
}
|
||||||
return filterRules;
|
return filterRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ export 'saved_view_model.dart';
|
|||||||
export 'similar_document_model.dart';
|
export 'similar_document_model.dart';
|
||||||
export 'task/task.dart';
|
export 'task/task.dart';
|
||||||
export 'task/task_status.dart';
|
export 'task/task_status.dart';
|
||||||
|
export 'field_suggestions.dart';
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ enum ErrorCode {
|
|||||||
deviceOffline,
|
deviceOffline,
|
||||||
serverUnreachable,
|
serverUnreachable,
|
||||||
similarQueryError,
|
similarQueryError,
|
||||||
|
suggestionsQueryError,
|
||||||
autocompleteQueryError,
|
autocompleteQueryError,
|
||||||
storagePathLoadFailed,
|
storagePathLoadFailed,
|
||||||
storagePathCreateFailed,
|
storagePathCreateFailed,
|
||||||
@@ -51,5 +52,6 @@ enum ErrorCode {
|
|||||||
deleteSavedViewError,
|
deleteSavedViewError,
|
||||||
requestTimedOut,
|
requestTimedOut,
|
||||||
unsupportedFileFormat,
|
unsupportedFileFormat,
|
||||||
missingClientCertificate;
|
missingClientCertificate,
|
||||||
|
acknowledgeTasksError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:paperless_api/src/request_utils.dart';
|
||||||
|
|
||||||
class PaperlessServerInformationModel {
|
class PaperlessServerInformationModel {
|
||||||
static const String versionHeader = 'x-version';
|
static const String versionHeader = 'x-version';
|
||||||
static const String apiVersionHeader = 'x-api-version';
|
static const String apiVersionHeader = 'x-api-version';
|
||||||
@@ -13,4 +15,9 @@ class PaperlessServerInformationModel {
|
|||||||
this.version = 'unknown',
|
this.version = 'unknown',
|
||||||
this.apiVersion = 1,
|
this.apiVersion = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
int compareToOtherVersion(String? other) {
|
||||||
|
return getExtendedVersionNumber(version ?? '0.0.0')
|
||||||
|
.compareTo(getExtendedVersionNumber(other ?? '0.0.0'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Task extends Equatable {
|
|||||||
final String? result;
|
final String? result;
|
||||||
final bool acknowledged;
|
final bool acknowledged;
|
||||||
@JsonKey(fromJson: tryParseNullable)
|
@JsonKey(fromJson: tryParseNullable)
|
||||||
final int? relatedDocumentId;
|
final int? relatedDocument;
|
||||||
|
|
||||||
const Task({
|
const Task({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -28,7 +28,7 @@ class Task extends Equatable {
|
|||||||
this.type,
|
this.type,
|
||||||
this.status,
|
this.status,
|
||||||
this.acknowledged = false,
|
this.acknowledged = false,
|
||||||
this.relatedDocumentId,
|
this.relatedDocument,
|
||||||
this.result,
|
this.result,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,6 +47,32 @@ class Task extends Equatable {
|
|||||||
status,
|
status,
|
||||||
result,
|
result,
|
||||||
acknowledged,
|
acknowledged,
|
||||||
relatedDocumentId,
|
relatedDocument,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Task copyWith({
|
||||||
|
int? id,
|
||||||
|
String? taskId,
|
||||||
|
String? taskFileName,
|
||||||
|
DateTime? dateCreated,
|
||||||
|
DateTime? dateDone,
|
||||||
|
String? type,
|
||||||
|
TaskStatus? status,
|
||||||
|
String? result,
|
||||||
|
bool? acknowledged,
|
||||||
|
int? relatedDocument,
|
||||||
|
}) {
|
||||||
|
return Task(
|
||||||
|
id: id ?? this.id,
|
||||||
|
taskId: taskId ?? this.taskId,
|
||||||
|
dateCreated: dateCreated ?? this.dateCreated,
|
||||||
|
acknowledged: acknowledged ?? this.acknowledged,
|
||||||
|
dateDone: dateDone ?? this.dateDone,
|
||||||
|
relatedDocument: relatedDocument ?? this.relatedDocument,
|
||||||
|
result: result ?? this.result,
|
||||||
|
status: status ?? this.status,
|
||||||
|
taskFileName: taskFileName ?? this.taskFileName,
|
||||||
|
type: type ?? this.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
|||||||
type: json['type'] as String?,
|
type: json['type'] as String?,
|
||||||
status: $enumDecodeNullable(_$TaskStatusEnumMap, json['status']),
|
status: $enumDecodeNullable(_$TaskStatusEnumMap, json['status']),
|
||||||
acknowledged: json['acknowledged'] as bool? ?? false,
|
acknowledged: json['acknowledged'] as bool? ?? false,
|
||||||
relatedDocumentId:
|
relatedDocument: tryParseNullable(json['related_document'] as String?),
|
||||||
tryParseNullable(json['related_document_id'] as String?),
|
|
||||||
result: json['result'] as String?,
|
result: json['result'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
|||||||
'status': _$TaskStatusEnumMap[instance.status],
|
'status': _$TaskStatusEnumMap[instance.status],
|
||||||
'result': instance.result,
|
'result': instance.result,
|
||||||
'acknowledged': instance.acknowledged,
|
'acknowledged': instance.acknowledged,
|
||||||
'related_document_id': instance.relatedDocumentId,
|
'related_document': instance.relatedDocument,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$TaskStatusEnumMap = {
|
const _$TaskStatusEnumMap = {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:paperless_api/src/models/bulk_edit_model.dart';
|
import 'package:paperless_api/src/models/models.dart';
|
||||||
import 'package:paperless_api/src/models/document_filter.dart';
|
|
||||||
import 'package:paperless_api/src/models/document_meta_data_model.dart';
|
|
||||||
import 'package:paperless_api/src/models/document_model.dart';
|
|
||||||
import 'package:paperless_api/src/models/paged_search_result.dart';
|
|
||||||
import 'package:paperless_api/src/models/similar_document_model.dart';
|
|
||||||
|
|
||||||
abstract class PaperlessDocumentsApi {
|
abstract class PaperlessDocumentsApi {
|
||||||
/// Uploads a document using a form data request and from server version 1.11.3
|
/// Uploads a document using a form data request and from server version 1.11.3
|
||||||
@@ -21,18 +16,16 @@ abstract class PaperlessDocumentsApi {
|
|||||||
});
|
});
|
||||||
Future<DocumentModel> update(DocumentModel doc);
|
Future<DocumentModel> update(DocumentModel doc);
|
||||||
Future<int> findNextAsn();
|
Future<int> findNextAsn();
|
||||||
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter);
|
Future<PagedSearchResult<DocumentModel>> findAll(DocumentFilter filter);
|
||||||
|
Future<DocumentModel?> find(int id);
|
||||||
Future<List<SimilarDocumentModel>> findSimilar(int docId);
|
Future<List<SimilarDocumentModel>> findSimilar(int docId);
|
||||||
Future<int> delete(DocumentModel doc);
|
Future<int> delete(DocumentModel doc);
|
||||||
Future<DocumentMetaData> getMetaData(DocumentModel document);
|
Future<DocumentMetaData> getMetaData(DocumentModel document);
|
||||||
Future<Iterable<int>> bulkAction(BulkAction action);
|
Future<Iterable<int>> bulkAction(BulkAction action);
|
||||||
Future<Uint8List> getPreview(int docId);
|
Future<Uint8List> getPreview(int docId);
|
||||||
String getThumbnailUrl(int docId);
|
String getThumbnailUrl(int docId);
|
||||||
Future<DocumentModel> waitForConsumptionFinished(
|
|
||||||
String filename,
|
|
||||||
String title,
|
|
||||||
);
|
|
||||||
Future<Uint8List> download(DocumentModel document);
|
Future<Uint8List> download(DocumentModel document);
|
||||||
|
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
||||||
|
|
||||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_api/src/constants.dart';
|
import 'package:paperless_api/src/constants.dart';
|
||||||
import 'package:paperless_api/src/converters/document_model_json_converter.dart';
|
|
||||||
import 'package:paperless_api/src/converters/similar_document_model_json_converter.dart';
|
|
||||||
import 'package:paperless_api/src/request_utils.dart';
|
|
||||||
|
|
||||||
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||||
final Dio client;
|
final Dio client;
|
||||||
@@ -82,8 +78,11 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter) async {
|
Future<PagedSearchResult<DocumentModel>> findAll(
|
||||||
final filterParams = filter.toQueryParameters();
|
DocumentFilter filter,
|
||||||
|
) async {
|
||||||
|
final filterParams = filter.toQueryParameters()
|
||||||
|
..addAll({'truncate_content': "true"});
|
||||||
try {
|
try {
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
"/api/documents/",
|
"/api/documents/",
|
||||||
@@ -156,7 +155,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
pageSize: 1,
|
pageSize: 1,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
final result = await find(asnQueryFilter);
|
final result = await findAll(asnQueryFilter);
|
||||||
return result.results
|
return result.results
|
||||||
.map((e) => e.archiveSerialNumber)
|
.map((e) => e.archiveSerialNumber)
|
||||||
.firstWhere((asn) => asn != null, orElse: () => 0)! +
|
.firstWhere((asn) => asn != null, orElse: () => 0)! +
|
||||||
@@ -187,26 +186,6 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<DocumentModel> waitForConsumptionFinished(
|
|
||||||
String fileName, String title) async {
|
|
||||||
PagedSearchResult<DocumentModel> results =
|
|
||||||
await find(DocumentFilter.latestDocument);
|
|
||||||
|
|
||||||
while ((results.results.isEmpty ||
|
|
||||||
(results.results[0].originalFileName != fileName &&
|
|
||||||
results.results[0].title != title))) {
|
|
||||||
//TODO: maybe implement more intelligent retry logic or find workaround for websocket authentication...
|
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
|
||||||
results = await find(DocumentFilter.latestDocument);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return results.results.first;
|
|
||||||
} on StateError {
|
|
||||||
throw const PaperlessServerException(ErrorCode.documentUploadFailed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> download(DocumentModel document) async {
|
Future<Uint8List> download(DocumentModel document) async {
|
||||||
try {
|
try {
|
||||||
@@ -273,4 +252,32 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
throw err.error;
|
throw err.error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<FieldSuggestions> findSuggestions(DocumentModel document) async {
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await client.get("/api/documents/${document.id}/suggestions/");
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return FieldSuggestions.fromJson(response.data);
|
||||||
|
}
|
||||||
|
throw const PaperlessServerException(ErrorCode.suggestionsQueryError);
|
||||||
|
} on DioError catch (err) {
|
||||||
|
throw err.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DocumentModel?> find(int id) async {
|
||||||
|
try {
|
||||||
|
final response = await client.get("/api/documents/$id/");
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return DocumentModel.fromJson(response.data);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} on DioError catch (err) {
|
||||||
|
throw err.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ abstract class PaperlessTasksApi {
|
|||||||
Future<Task?> find({int? id, String? taskId});
|
Future<Task?> find({int? id, String? taskId});
|
||||||
Future<Iterable<Task>> findAll([Iterable<int>? ids]);
|
Future<Iterable<Task>> findAll([Iterable<int>? ids]);
|
||||||
Stream<Task> listenForTaskChanges(String taskId);
|
Stream<Task> listenForTaskChanges(String taskId);
|
||||||
|
Future<Task> acknowledgeTask(Task task);
|
||||||
|
Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,48 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'dart:developer';
|
||||||
import 'package:paperless_api/src/models/task/task.dart';
|
|
||||||
import 'package:paperless_api/src/models/task/task_status.dart';
|
|
||||||
|
|
||||||
import 'paperless_tasks_api.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_api/src/request_utils.dart';
|
||||||
|
|
||||||
class PaperlessTasksApiImpl implements PaperlessTasksApi {
|
class PaperlessTasksApiImpl implements PaperlessTasksApi {
|
||||||
final Dio client;
|
final Dio _client;
|
||||||
|
|
||||||
const PaperlessTasksApiImpl(this.client);
|
PaperlessTasksApiImpl(this._client);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Task?> find({int? id, String? taskId}) async {
|
Future<Task?> find({int? id, String? taskId}) async {
|
||||||
assert(id != null || taskId != null);
|
assert((id != null) != (taskId != null));
|
||||||
String url = "/api/tasks/";
|
if (id != null) {
|
||||||
if (taskId != null) {
|
return _findById(id);
|
||||||
url += "?task_id=$taskId";
|
} else if (taskId != null) {
|
||||||
} else {
|
return _findByTaskId(taskId);
|
||||||
url += "$id/";
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await client.get(url);
|
/// API response returns List with single item
|
||||||
|
Future<Task?> _findById(int id) async {
|
||||||
|
final response = await _client.get("/api/tasks/$id/");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return Task.fromJson(response.data);
|
return Task.fromJson(response.data);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// API response returns List with single item
|
||||||
|
Future<Task?> _findByTaskId(String taskId) async {
|
||||||
|
final response = await _client.get("/api/tasks/?task_id=$taskId");
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
if ((response.data as List).isNotEmpty) {
|
||||||
|
return Task.fromJson((response.data as List).first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
|
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
|
||||||
final response = await client.get("/api/tasks/");
|
final response = await _client.get("/api/tasks/");
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return (response.data as List).map((e) => Task.fromJson(e));
|
return (response.data as List).map((e) => Task.fromJson(e));
|
||||||
}
|
}
|
||||||
@@ -37,17 +51,39 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Task> listenForTaskChanges(String taskId) async* {
|
Stream<Task> listenForTaskChanges(String taskId) async* {
|
||||||
bool isSuccess = false;
|
bool isCompleted = false;
|
||||||
while (!isSuccess) {
|
while (!isCompleted) {
|
||||||
final task = await find(taskId: taskId);
|
final task = await find(taskId: taskId);
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
throw Exception("Task with taskId $taskId does not exist.");
|
throw Exception("Task with taskId $taskId does not exist.");
|
||||||
}
|
}
|
||||||
|
log("Found new task: ${task.taskId}, ${task.id}, ${task.status}");
|
||||||
yield task;
|
yield task;
|
||||||
if (task.status == TaskStatus.success) {
|
if (task.status == TaskStatus.success ||
|
||||||
isSuccess = true;
|
task.status == TaskStatus.failure) {
|
||||||
|
isCompleted = true;
|
||||||
}
|
}
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Task> acknowledgeTask(Task task) async {
|
||||||
|
final acknowledgedTasks = await acknowledgeTasks([task]);
|
||||||
|
return acknowledgedTasks.first.copyWith(acknowledged: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Iterable<Task>> acknowledgeTasks(Iterable<Task> tasks) async {
|
||||||
|
final response = await _client.post("/api/acknowledge_tasks/", data: {
|
||||||
|
'tasks': tasks.map((e) => e.id).toList(),
|
||||||
|
});
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
if (response.data['result'] != tasks.length) {
|
||||||
|
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
|
||||||
|
}
|
||||||
|
return tasks.map((e) => e.copyWith(acknowledged: true)).toList();
|
||||||
|
}
|
||||||
|
throw const PaperlessServerException(ErrorCode.acknowledgeTasksError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
pubspec.lock
56
pubspec.lock
@@ -633,6 +633,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
flutter_staggered_grid_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_staggered_grid_view
|
||||||
|
sha256: "1312314293acceb65b92754298754801b0e1f26a1845833b740b30415bbbcf07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.2"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -696,14 +704,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
get_it:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: get_it
|
|
||||||
sha256: "290fde3a86072e4b37dbb03c07bec6126f0ecc28dad403c12ffe2e5a2d751ab7"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "7.2.0"
|
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -784,30 +784,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.0"
|
version: "3.3.0"
|
||||||
infinite_scroll_pagination:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: infinite_scroll_pagination
|
|
||||||
sha256: "9517328f4e373f08f57dbb11c5aac5b05554142024d6b60c903f3b73476d52db"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.2.0"
|
|
||||||
injectable:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: injectable
|
|
||||||
sha256: "7dab7d341feb40a0590d9ff6261aea9495522005e2c6763f9161a4face916f7b"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
injectable_generator:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: injectable_generator
|
|
||||||
sha256: "9a3bbd2c3ba821e31ef6cea3fc535c17e3a25c74e173b6cefa05f466c8338bc8"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.3"
|
|
||||||
integration_test:
|
integration_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1308,14 +1284,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
recase:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: recase
|
|
||||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
receive_sharing_intent:
|
receive_sharing_intent:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1457,14 +1425,6 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
sliver_tools:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sliver_tools
|
|
||||||
sha256: edf005f1a47c2ffa6f1e1a4f24dd99c45b8bccfff9b928d39170d36dc6fda871
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.8"
|
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ dependencies:
|
|||||||
equatable: ^2.0.3
|
equatable: ^2.0.3
|
||||||
flutter_form_builder: ^7.5.0
|
flutter_form_builder: ^7.5.0
|
||||||
form_builder_validators: ^8.4.0
|
form_builder_validators: ^8.4.0
|
||||||
infinite_scroll_pagination: ^3.2.0
|
|
||||||
package_info_plus: ^1.4.3+1
|
package_info_plus: ^1.4.3+1
|
||||||
font_awesome_flutter: ^10.1.0
|
font_awesome_flutter: ^10.1.0
|
||||||
local_auth: ^2.1.2
|
local_auth: ^2.1.2
|
||||||
@@ -85,6 +84,7 @@ dependencies:
|
|||||||
collection: ^1.17.0
|
collection: ^1.17.0
|
||||||
device_info_plus: ^4.1.3
|
device_info_plus: ^4.1.3
|
||||||
flutter_local_notifications: ^13.0.0
|
flutter_local_notifications: ^13.0.0
|
||||||
|
flutter_staggered_grid_view: ^0.6.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
@@ -92,7 +92,6 @@ dev_dependencies:
|
|||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
build_runner: ^2.1.11
|
build_runner: ^2.1.11
|
||||||
injectable_generator: ^2.1.0
|
|
||||||
mockito: ^5.3.2
|
mockito: ^5.3.2
|
||||||
bloc_test: ^9.1.0
|
bloc_test: ^9.1.0
|
||||||
dependency_validator: ^3.0.0
|
dependency_validator: ^3.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user