mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 09:15:49 -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
|
||||
flutter pub get
|
||||
```
|
||||
3. Build generated files (e.g. for injectable library)
|
||||
3. Build generated files (for json_serializable etc.)
|
||||
```sh
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
```
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@prod
|
||||
@test
|
||||
@lazySingleton
|
||||
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
||||
DocumentStatusCubit() : super(null);
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_api/paperless_api.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
|
||||
extends Cubit<PaperlessServerInformationState> {
|
||||
final PaperlessServerStatsApi service;
|
||||
final PaperlessServerStatsApi _api;
|
||||
|
||||
PaperlessServerInformationCubit(this.service)
|
||||
PaperlessServerInformationCubit(this._api)
|
||||
: super(PaperlessServerInformationState());
|
||||
|
||||
Future<void> updateInformtion() async {
|
||||
final information = await service.getServerInformation();
|
||||
final information = await _api.getServerInformation();
|
||||
emit(PaperlessServerInformationState(
|
||||
isLoaded: true,
|
||||
information: information,
|
||||
|
||||
@@ -68,5 +68,9 @@ String translateError(BuildContext context, ErrorCode code) {
|
||||
return S.of(context).errorMessageUnsupportedFileFormat;
|
||||
case ErrorCode.missingClientCertificate:
|
||||
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/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/features/login/model/client_certificate.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
|
||||
class AuthenticationAwareDioManager {
|
||||
class SessionManager {
|
||||
final Dio client;
|
||||
final List<Interceptor> interceptors;
|
||||
PaperlessServerInformationModel serverInformation;
|
||||
|
||||
AuthenticationAwareDioManager([this.interceptors = const []])
|
||||
: client = _initDio(interceptors);
|
||||
SessionManager([this.interceptors = const []])
|
||||
: client = _initDio(interceptors),
|
||||
serverInformation = PaperlessServerInformationModel();
|
||||
|
||||
static Dio _initDio(List<Interceptor> interceptors) {
|
||||
//en- and decoded by utf8 by default
|
||||
@@ -19,8 +24,19 @@ class AuthenticationAwareDioManager {
|
||||
dio.options.responseType = ResponseType.json;
|
||||
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
|
||||
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||
dio.interceptors.addAll(interceptors);
|
||||
dio.interceptors.add(RetryOnConnectionChangeInterceptor(dio: dio));
|
||||
dio.interceptors.addAll([
|
||||
...interceptors,
|
||||
DioHttpErrorInterceptor(),
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
responseBody: false,
|
||||
responseHeader: false,
|
||||
request: false,
|
||||
requestBody: false,
|
||||
requestHeader: false,
|
||||
),
|
||||
RetryOnConnectionChangeInterceptor(dio: dio)
|
||||
]);
|
||||
return dio;
|
||||
}
|
||||
|
||||
@@ -28,6 +44,7 @@ class AuthenticationAwareDioManager {
|
||||
String? baseUrl,
|
||||
String? authToken,
|
||||
ClientCertificate? clientCertificate,
|
||||
PaperlessServerInformationModel? serverInformation,
|
||||
}) {
|
||||
if (clientCertificate != null) {
|
||||
final context = SecurityContext()
|
||||
@@ -58,11 +75,16 @@ class AuthenticationAwareDioManager {
|
||||
if (authToken != null) {
|
||||
client.options.headers.addAll({'Authorization': 'Token $authToken'});
|
||||
}
|
||||
|
||||
if (serverInformation != null) {
|
||||
this.serverInformation = serverInformation;
|
||||
}
|
||||
}
|
||||
|
||||
void resetSettings() {
|
||||
client.httpClientAdapter = DefaultHttpClientAdapter();
|
||||
client.options.baseUrl = '';
|
||||
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;
|
||||
|
||||
DocumentDetailsCubit(this._api, DocumentModel initialDocument)
|
||||
: super(DocumentDetailsState(document: initialDocument));
|
||||
: super(DocumentDetailsState(document: initialDocument)) {
|
||||
loadSuggestions();
|
||||
}
|
||||
|
||||
Future<void> delete(DocumentModel document) async {
|
||||
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 {
|
||||
if (document.archiveSerialNumber == null) {
|
||||
final int asn = await _api.findNextAsn();
|
||||
final updatedDocument =
|
||||
await _api.update(document.copyWith(archiveSerialNumber: asn));
|
||||
emit(DocumentDetailsState(document: updatedDocument));
|
||||
emit(state.copyWith(document: updatedDocument));
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
final DocumentModel document;
|
||||
final bool isFullContentLoaded;
|
||||
final String? fullContent;
|
||||
final FieldSuggestions suggestions;
|
||||
|
||||
const DocumentDetailsState({
|
||||
required this.document,
|
||||
this.suggestions = const FieldSuggestions(),
|
||||
this.isFullContentLoaded = false,
|
||||
this.fullContent,
|
||||
});
|
||||
|
||||
@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:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:badges/badges.dart' as b;
|
||||
|
||||
class DocumentDetailsPage extends StatefulWidget {
|
||||
final bool allowEdit;
|
||||
@@ -63,9 +64,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
if (!connectivityState.isConnected) {
|
||||
return Container();
|
||||
}
|
||||
return FloatingActionButton(
|
||||
child: const Icon(Icons.edit),
|
||||
onPressed: () => _onEdit(state.document),
|
||||
return b.Badge(
|
||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||
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(
|
||||
state.document,
|
||||
widget.titleAndContentQueryString,
|
||||
state,
|
||||
),
|
||||
_buildDocumentMetaDataView(
|
||||
state.document,
|
||||
@@ -217,7 +231,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
listener: (context, state) {
|
||||
cubit.replaceDocument(state.document);
|
||||
},
|
||||
child: const DocumentEditPage(),
|
||||
child: DocumentEditPage(
|
||||
suggestions: cubit.state.suggestions,
|
||||
),
|
||||
),
|
||||
),
|
||||
maintainState: true,
|
||||
@@ -303,14 +319,30 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDocumentContentView(DocumentModel document, String? match) {
|
||||
return SingleChildScrollView(
|
||||
child: HighlightedText(
|
||||
text: document.content ?? "",
|
||||
highlights: match == null ? [] : match.split(" "),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
caseSensitive: false,
|
||||
),
|
||||
Widget _buildDocumentContentView(
|
||||
DocumentModel document,
|
||||
String? match,
|
||||
DocumentDetailsState state,
|
||||
) {
|
||||
return ListView(
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,17 +55,16 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> upload(
|
||||
Future<String?> upload(
|
||||
Uint8List bytes, {
|
||||
required String filename,
|
||||
required String title,
|
||||
required void Function(DocumentModel document)? onConsumptionFinished,
|
||||
int? documentType,
|
||||
int? correspondent,
|
||||
Iterable<int> tags = const [],
|
||||
DateTime? createdAt,
|
||||
}) async {
|
||||
await _documentApi.create(
|
||||
return await _documentApi.create(
|
||||
bytes,
|
||||
filename: filename,
|
||||
title: title,
|
||||
@@ -74,11 +73,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
tags: tags,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
if (onConsumptionFinished != null) {
|
||||
_documentApi
|
||||
.waitForConsumptionFinished(filename, title)
|
||||
.then((value) => onConsumptionFinished(value));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -25,14 +25,12 @@ class DocumentUploadPreparationPage extends StatefulWidget {
|
||||
final String? title;
|
||||
final String? filename;
|
||||
final String? fileExtension;
|
||||
final void Function(DocumentModel)? onSuccessfullyConsumed;
|
||||
|
||||
const DocumentUploadPreparationPage({
|
||||
Key? key,
|
||||
required this.fileBytes,
|
||||
this.title,
|
||||
this.filename,
|
||||
this.onSuccessfullyConsumed,
|
||||
this.fileExtension,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -236,19 +234,18 @@ class _DocumentUploadPreparationPageState
|
||||
final correspondent =
|
||||
fv[DocumentModel.correspondentKey] as IdQueryParameter;
|
||||
|
||||
await cubit.upload(
|
||||
final taskId = await cubit.upload(
|
||||
widget.fileBytes,
|
||||
filename:
|
||||
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
|
||||
title: title,
|
||||
onConsumptionFinished: widget.onSuccessfullyConsumed,
|
||||
documentType: docType.id,
|
||||
correspondent: correspondent.id,
|
||||
tags: tags.ids,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
showSnackBar(context, S.of(context).documentUploadSuccessText);
|
||||
Navigator.pop(context, true);
|
||||
Navigator.pop(context, taskId);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on PaperlessValidationErrors catch (errors) {
|
||||
|
||||
@@ -52,7 +52,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
log("[DocumentsCubit] load");
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
final result = await _api.find(state.filter);
|
||||
final result = await _api.findAll(state.filter);
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
hasLoaded: true,
|
||||
@@ -67,11 +67,13 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
log("[DocumentsCubit] reload");
|
||||
emit(state.copyWith(isLoading: true));
|
||||
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(
|
||||
hasLoaded: true,
|
||||
value: [result],
|
||||
isLoading: false,
|
||||
filter: filter,
|
||||
));
|
||||
} finally {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
@@ -81,7 +83,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
Future<void> _bulkReloadDocuments() async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
final result = await _api.find(
|
||||
final result = await _api.findAll(
|
||||
state.filter.copyWith(
|
||||
page: 1,
|
||||
pageSize: state.documents.length,
|
||||
@@ -106,7 +108,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
|
||||
try {
|
||||
final result = await _api.find(newFilter);
|
||||
final result = await _api.findAll(newFilter);
|
||||
emit(
|
||||
DocumentsState(
|
||||
hasLoaded: true,
|
||||
@@ -129,7 +131,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
log("[DocumentsCubit] updateFilter");
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _api.find(filter.copyWith(page: 1));
|
||||
final result = await _api.findAll(filter.copyWith(page: 1));
|
||||
|
||||
emit(
|
||||
DocumentsState(
|
||||
@@ -163,7 +165,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
|
||||
void toggleDocumentSelection(DocumentModel model) {
|
||||
log("[DocumentsCubit] toggleSelection");
|
||||
if (state.selection.contains(model)) {
|
||||
if (state.selectedIds.contains(model.id)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selection: state.selection
|
||||
@@ -183,6 +185,12 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
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() {
|
||||
log("[DocumentsCubit] reset");
|
||||
emit(const DocumentsState());
|
||||
@@ -196,7 +204,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
|
||||
if (filter == null) {
|
||||
return;
|
||||
}
|
||||
final results = await _api.find(filter.copyWith(page: 1));
|
||||
final results = await _api.findAll(filter.copyWith(page: 1));
|
||||
emit(
|
||||
DocumentsState(
|
||||
filter: filter,
|
||||
|
||||
@@ -23,6 +23,8 @@ class DocumentsState extends Equatable {
|
||||
this.selectedSavedViewId,
|
||||
});
|
||||
|
||||
List<int> get selectedIds => selection.map((e) => e.id).toList();
|
||||
|
||||
int get currentPageNumber {
|
||||
return filter.page;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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:intl/intl.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_document_type_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/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
|
||||
class DocumentEditPage extends StatefulWidget {
|
||||
final FieldSuggestions suggestions;
|
||||
const DocumentEditPage({
|
||||
Key? key,
|
||||
required this.suggestions,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -74,14 +79,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(state.document.created).padded(),
|
||||
_buildDocumentTypeFormField(
|
||||
state.document.documentType, state.documentTypes)
|
||||
.padded(),
|
||||
state.document.documentType,
|
||||
state.documentTypes,
|
||||
).padded(),
|
||||
_buildCorrespondentFormField(
|
||||
state.document.correspondent, state.correspondents)
|
||||
.padded(),
|
||||
state.document.correspondent,
|
||||
state.correspondents,
|
||||
).padded(),
|
||||
_buildStoragePathFormField(
|
||||
state.document.storagePath, state.storagePaths)
|
||||
.padded(),
|
||||
state.document.storagePath,
|
||||
state.storagePaths,
|
||||
).padded(),
|
||||
TagFormField(
|
||||
initialValue:
|
||||
IdsTagsQuery.included(state.document.tags.toList()),
|
||||
@@ -99,58 +107,101 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
}
|
||||
|
||||
Widget _buildStoragePathFormField(
|
||||
int? initialId, Map<int, StoragePath> options) {
|
||||
return LabelFormField<StoragePath>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||
create: (context) => context
|
||||
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
child: AddStoragePathPage(initalValue: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
|
||||
labelOptions: options,
|
||||
initialValue: IdQueryParameter.fromId(initialId),
|
||||
name: fkStoragePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
int? initialId,
|
||||
Map<int, StoragePath> options,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
LabelFormField<StoragePath>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
|
||||
child: AddStoragePathPage(initalValue: initialValue),
|
||||
),
|
||||
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(
|
||||
int? initialId, Map<int, Correspondent> options) {
|
||||
return LabelFormField<Correspondent>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
child: AddCorrespondentPage(initialName: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||
labelOptions: options,
|
||||
initialValue: IdQueryParameter.fromId(initialId),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
return Column(
|
||||
children: [
|
||||
LabelFormField<Correspondent>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
|
||||
create: (context) => context.read<
|
||||
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
|
||||
child: AddCorrespondentPage(initialName: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
|
||||
labelOptions: options,
|
||||
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(
|
||||
int? initialId, Map<int, DocumentType> options) {
|
||||
return LabelFormField<DocumentType>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
|
||||
create: (context) => context
|
||||
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
|
||||
child: AddDocumentTypePage(
|
||||
initialName: currentInput,
|
||||
int? initialId,
|
||||
Map<int, DocumentType> options,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
LabelFormField<DocumentType>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
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),
|
||||
),
|
||||
),
|
||||
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
|
||||
initialValue: IdQueryParameter.fromId(initialId),
|
||||
labelOptions: options,
|
||||
name: fkDocumentType,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
if (widget.suggestions.hasSuggestedDocumentTypes)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions: widget.suggestions.documentTypes,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(options[itemData]!.name),
|
||||
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
|
||||
?.didChange(IdQueryParameter.fromId(itemData)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -198,16 +249,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
}
|
||||
|
||||
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
|
||||
return FormBuilderDateTimePicker(
|
||||
inputType: InputType.date,
|
||||
name: fkCreatedDate,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||
),
|
||||
initialValue: initialCreatedAtDate,
|
||||
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
|
||||
initialEntryMode: DatePickerEntryMode.calendar,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FormBuilderDateTimePicker(
|
||||
inputType: InputType.date,
|
||||
name: fkCreatedDate,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||
),
|
||||
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:flutter/material.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_mobile/core/bloc/connectivity_cubit.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/view/pages/document_details_page.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/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/document_list.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.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/selection/documents_page_app_bar.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/labels/bloc/providers/labels_bloc_provider.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/view/saved_view_selection_widget.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/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';
|
||||
|
||||
class DocumentFilterIntent {
|
||||
@@ -42,9 +45,11 @@ class DocumentsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DocumentsPageState extends State<DocumentsPage> {
|
||||
final _pagingController = PagingController<int, DocumentModel>(
|
||||
firstPageKey: 1,
|
||||
);
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
double _offset = 0;
|
||||
double _last = 0;
|
||||
|
||||
static const double _savedViewWidgetHeight = 78 + 16;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -55,12 +60,36 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
} on PaperlessServerException catch (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
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -78,6 +107,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
}
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
const linearProgressIndicatorHeight = 4.0;
|
||||
return Scaffold(
|
||||
drawer: BlocProvider.value(
|
||||
value: context.read<AuthenticationCubit>(),
|
||||
@@ -85,16 +115,65 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
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>(
|
||||
builder: (context, state) {
|
||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||
return Badge.count(
|
||||
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
||||
alignment: const AlignmentDirectional(44, -4),
|
||||
isLabelVisible: appliedFiltersCount > 0,
|
||||
count: state.filter.appliedFiltersCount,
|
||||
backgroundColor: Colors.red,
|
||||
textColor: Colors.white,
|
||||
return b.Badge(
|
||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||
showBadge: appliedFiltersCount > 0,
|
||||
badgeContent: Text(
|
||||
'$appliedFiltersCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
animationType: b.BadgeAnimationType.fade,
|
||||
badgeColor: Theme.of(context).colorScheme.error,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.filter_alt_outlined),
|
||||
onPressed: _openDocumentFilter,
|
||||
@@ -103,12 +182,71 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
},
|
||||
),
|
||||
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 {
|
||||
final draggableSheetController = DraggableScrollableController();
|
||||
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) {
|
||||
final isConnected = connectivityState == ConnectivityState.connected;
|
||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
builder: (context, settings) {
|
||||
return BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
buildWhen: (previous, current) => !const ListEquality()
|
||||
.equals(previous.documents, current.documents),
|
||||
buildWhen: (previous, current) =>
|
||||
!const ListEquality()
|
||||
.equals(previous.documents, current.documents) ||
|
||||
previous.selectedIds != current.selectedIds,
|
||||
builder: (context, state) {
|
||||
// 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) {
|
||||
child = SliverToBoxAdapter(
|
||||
child: DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () {
|
||||
context.read<DocumentsCubit>().resetFilter();
|
||||
context.read<DocumentsCubit>().unselectView();
|
||||
},
|
||||
),
|
||||
return DocumentsEmptyState(
|
||||
state: state,
|
||||
onReset: () {
|
||||
context.read<DocumentsCubit>().resetFilter();
|
||||
context.read<DocumentsCubit>().unselectView();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
notificationPredicate: (_) => isConnected,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
DocumentsPageAppBar(
|
||||
isOffline: connectivityState != ConnectivityState.connected,
|
||||
actions: [
|
||||
const SortDocumentsButton(),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
settings.preferredViewType == ViewType.grid
|
||||
? Icons.list
|
||||
: Icons.grid_view,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<ApplicationSettingsCubit>()
|
||||
.setViewType(
|
||||
settings.preferredViewType.toggle(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
return AdaptiveDocumentsView(
|
||||
viewType: settings.preferredViewType,
|
||||
state: state,
|
||||
scrollController: _scrollController,
|
||||
onTap: _openDetails,
|
||||
onSelected: _onSelected,
|
||||
hasInternetConnection: isConnected,
|
||||
onTagSelected: _addTagToFilter,
|
||||
onCorrespondentSelected: _addCorrespondentToFilter,
|
||||
onDocumentTypeSelected: _addDocumentTypeToFilter,
|
||||
onStoragePathSelected: _addStoragePathToFilter,
|
||||
pageLoadingWidget: const NewItemsLoadingWidget(),
|
||||
beforeItems: const SizedBox(height: _savedViewWidgetHeight),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -355,15 +450,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadNewPage(int pageKey) async {
|
||||
final documentsCubit = context.read<DocumentsCubit>();
|
||||
final pageCount = documentsCubit.state
|
||||
.inferPageCount(pageSize: documentsCubit.state.filter.pageSize);
|
||||
if (pageCount <= pageKey + 1) {
|
||||
_pagingController.nextPageKey = null;
|
||||
}
|
||||
Future<void> _loadNewPage() async {
|
||||
try {
|
||||
await documentsCubit.loadMore();
|
||||
await context.read<DocumentsCubit>().loadMore();
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
}
|
||||
@@ -376,8 +465,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
Future<void> _onRefresh() async {
|
||||
try {
|
||||
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
|
||||
await context.read<DocumentsCubit>().reload();
|
||||
await context.read<SavedViewCubit>().reload();
|
||||
context.read<DocumentsCubit>().reload();
|
||||
context.read<SavedViewCubit>().reload();
|
||||
} on PaperlessServerException catch (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 bool isAtLeastOneSelected;
|
||||
final bool Function(int tagId) isTagSelectedPredicate;
|
||||
final void Function(int tagId) onTagSelected;
|
||||
final void Function(int tagId)? onTagSelected;
|
||||
|
||||
const DocumentGridItem({
|
||||
Key? key,
|
||||
@@ -57,9 +57,11 @@ class DocumentGridItem extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CorrespondentWidget(
|
||||
correspondentId: document.correspondent),
|
||||
correspondentId: document.correspondent,
|
||||
),
|
||||
DocumentTypeWidget(
|
||||
documentTypeId: document.documentType),
|
||||
documentTypeId: document.documentType,
|
||||
),
|
||||
Text(
|
||||
document.title,
|
||||
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) {
|
||||
return SizedBox(
|
||||
child: ListTile(
|
||||
trailing: Text("${document.id}"),
|
||||
dense: true,
|
||||
selected: isSelected,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
void 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);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_listenForReceivedFiles();
|
||||
@@ -195,7 +155,14 @@ class _HomePageState extends State<HomePage> {
|
||||
},
|
||||
),
|
||||
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(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.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
|
||||
.find(DocumentFilter(
|
||||
.findAll(DocumentFilter(
|
||||
tags: AnyAssignedTagsQuery(tagIds: inboxTags),
|
||||
sortField: SortField.added,
|
||||
))
|
||||
|
||||
@@ -10,7 +10,7 @@ class TagsWidget extends StatefulWidget {
|
||||
final Iterable<int> tagIds;
|
||||
final bool isMultiLine;
|
||||
final VoidCallback? afterTagTapped;
|
||||
final void Function(int tagId) onTagSelected;
|
||||
final void Function(int tagId)? onTagSelected;
|
||||
final bool isClickable;
|
||||
final bool Function(int id) isSelectedPredicate;
|
||||
|
||||
@@ -42,7 +42,7 @@ class _TagsWidgetState extends State<TagsWidget> {
|
||||
afterTagTapped: widget.afterTagTapped,
|
||||
isClickable: widget.isClickable,
|
||||
isSelected: widget.isSelectedPredicate(id),
|
||||
onSelected: () => widget.onTagSelected(id),
|
||||
onSelected: () => widget.onTagSelected?.call(id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -11,7 +11,7 @@ class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
final documents = await _api.find(
|
||||
final documents = await _api.findAll(
|
||||
state.filter.copyWith(
|
||||
pageSize: 100,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.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_mobile/core/widgets/documents_list_loading_widget.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> {
|
||||
final _pagingController =
|
||||
PagingController<int, DocumentModel>(firstPageKey: 1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.nextPageKey = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -38,8 +28,6 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
||||
if (!state.isLoaded) {
|
||||
return const DocumentsListLoadingWidget();
|
||||
}
|
||||
|
||||
_pagingController.itemList = state.documents!.results;
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
@@ -48,42 +36,34 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PagedSliverList<int, DocumentModel>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
animateTransitions: true,
|
||||
itemBuilder: (context, document, index) {
|
||||
return DocumentListItem(
|
||||
isLabelClickable: false,
|
||||
document: document,
|
||||
onTap: (doc) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => DocumentDetailsCubit(
|
||||
context.read<PaperlessDocumentsApi>(),
|
||||
document,
|
||||
),
|
||||
child: const DocumentDetailsPage(
|
||||
isLabelClickable: false,
|
||||
allowEdit: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
isSelected: false,
|
||||
isAtLeastOneSelected: false,
|
||||
isTagSelectedPredicate: (_) => false,
|
||||
onTagSelected: (int tag) {},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
return DocumentListItem(
|
||||
isLabelClickable: false,
|
||||
document: state.documents!.results.elementAt(index),
|
||||
onTap: (doc) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => DocumentDetailsCubit(
|
||||
context.read<PaperlessDocumentsApi>(),
|
||||
state.documents!.results.elementAt(index),
|
||||
),
|
||||
child: const DocumentDetailsPage(
|
||||
isLabelClickable: false,
|
||||
allowEdit: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
isSelected: false,
|
||||
isAtLeastOneSelected: false,
|
||||
isTagSelectedPredicate: (_) => false,
|
||||
onTagSelected: (int tag) {},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.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/model/authentication_information.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
@@ -12,7 +12,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
|
||||
with HydratedMixin<AuthenticationState> {
|
||||
final LocalAuthenticationService _localAuthService;
|
||||
final PaperlessAuthenticationApi _authApi;
|
||||
final AuthenticationAwareDioManager _dioWrapper;
|
||||
final SessionManager _dioWrapper;
|
||||
|
||||
AuthenticationCubit(
|
||||
this._localAuthService,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
|
||||
@lazySingleton
|
||||
class LocalAuthenticationService {
|
||||
final LocalVault localStore;
|
||||
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 =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
LocalNotificationService._();
|
||||
|
||||
static final LocalNotificationService instance = LocalNotificationService._();
|
||||
LocalNotificationService();
|
||||
|
||||
Future<void> initialize() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
@@ -71,7 +69,7 @@ class LocalNotificationService {
|
||||
body = task.taskFileName;
|
||||
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
|
||||
payload = CreateDocumentSuccessNotificationResponsePayload(
|
||||
task.relatedDocumentId!,
|
||||
task.relatedDocument!,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.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/selection/confirm_delete_saved_view_dialog.dart';
|
||||
@@ -31,91 +32,97 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivityState) {
|
||||
final hasInternetConnection = connectivityState.isConnected;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
if (!state.hasLoaded) {
|
||||
return _buildLoadingWidget(context);
|
||||
}
|
||||
if (state.value.isEmpty) {
|
||||
return Text(S.of(context).savedViewsEmptyStateText);
|
||||
}
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: ListView.separated(
|
||||
itemCount: state.value.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final view = state.value.values.elementAt(index);
|
||||
return GestureDetector(
|
||||
onLongPress: hasInternetConnection
|
||||
? () => _onDelete(context, view)
|
||||
: null,
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, docState) {
|
||||
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)
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
if (!state.hasLoaded) {
|
||||
return _buildLoadingWidget(context);
|
||||
}
|
||||
if (state.value.isEmpty) {
|
||||
return Text(S.of(context).savedViewsEmptyStateText);
|
||||
}
|
||||
return SizedBox(
|
||||
height: 38,
|
||||
child: ListView.separated(
|
||||
itemCount: state.value.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final view = state.value.values.elementAt(index);
|
||||
return GestureDetector(
|
||||
onLongPress: hasInternetConnection
|
||||
? () => _onDelete(context, view)
|
||||
: 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) {
|
||||
final r = Random(123456789);
|
||||
return SizedBox(
|
||||
height: height,
|
||||
width: double.infinity,
|
||||
height: 38,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[300]!
|
||||
@@ -123,14 +130,35 @@ class SavedViewSelectionWidget extends StatelessWidget {
|
||||
highlightColor: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]!
|
||||
: Colors.grey[600]!,
|
||||
child: ListView.separated(
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) => FilterChip(
|
||||
label: SizedBox(width: r.nextInt((index * 20) + 50).toDouble()),
|
||||
onSelected: null),
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 4.0),
|
||||
children: [
|
||||
FilterChip(
|
||||
label: const SizedBox(width: 32),
|
||||
onSelected: (_) {},
|
||||
),
|
||||
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/scan/bloc/document_scanner_cubit.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/util.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -140,7 +141,7 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
final file = await _assembleFileBytes(
|
||||
context.read<DocumentScannerCubit>().state,
|
||||
);
|
||||
final uploaded = await Navigator.of(context).push(
|
||||
final taskId = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LabelRepositoriesProvider(
|
||||
child: BlocProvider(
|
||||
@@ -165,8 +166,10 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
),
|
||||
) ??
|
||||
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<TaskStatusCubit>().listenToTaskChanges(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,20 +7,30 @@ class TaskStatusCubit extends Cubit<TaskStatusState> {
|
||||
final PaperlessTasksApi _api;
|
||||
TaskStatusCubit(this._api) : super(const TaskStatusState());
|
||||
|
||||
void startListeningToTask(String taskId) {
|
||||
void listenToTaskChanges(String taskId) {
|
||||
_api
|
||||
.listenForTaskChanges(taskId)
|
||||
.forEach(
|
||||
(element) => TaskStatusState(
|
||||
isListening: true,
|
||||
isAcknowledged: false,
|
||||
task: element,
|
||||
(element) => emit(
|
||||
TaskStatusState(
|
||||
isListening: true,
|
||||
task: element,
|
||||
),
|
||||
),
|
||||
)
|
||||
.whenComplete(() => emit(state.copyWith(isListening: false)));
|
||||
}
|
||||
|
||||
void acknowledgeCurrentTask() {
|
||||
emit(state.copyWith(isListening: false, isAcknowledged: true));
|
||||
Future<void> acknowledgeCurrentTask() async {
|
||||
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 {
|
||||
final Task? task;
|
||||
final bool isListening;
|
||||
final bool isAcknowledged;
|
||||
|
||||
const TaskStatusState({
|
||||
this.task,
|
||||
this.isListening = false,
|
||||
this.isAcknowledged = false,
|
||||
});
|
||||
|
||||
bool get isActive => isListening && !isAcknowledged;
|
||||
|
||||
bool get isSuccess => task?.status == TaskStatus.success;
|
||||
|
||||
bool get isAcknowledged => task?.acknowledged ?? false;
|
||||
|
||||
String? get taskId => task?.taskId;
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
List<Object?> get props => [task, isListening];
|
||||
|
||||
TaskStatusState copyWith({
|
||||
Task? task,
|
||||
@@ -28,7 +26,6 @@ class TaskStatusState extends Equatable {
|
||||
return TaskStatusState(
|
||||
task: task ?? this.task,
|
||||
isListening: isListening ?? this.isListening,
|
||||
isAcknowledged: isAcknowledged ?? this.isAcknowledged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +194,8 @@
|
||||
"@editLabelPageConfirmDeletionDialogTitle": {},
|
||||
"editLabelPageDeletionDialogText": "Dokumenty mají přiřazen tento štítek. Odstraněním štítku bude označení odstraněno. Pokračovat?",
|
||||
"@editLabelPageDeletionDialogText": {},
|
||||
"errorMessageAcknowledgeTasksError": "",
|
||||
"@errorMessageAcknowledgeTasksError": {},
|
||||
"errorMessageAuthenticationFailed": "Přihlášení selhalo, zkuste to znovu.",
|
||||
"@errorMessageAuthenticationFailed": {},
|
||||
"errorMessageAutocompleteQueryError": "Při automatickém doplnění požadavku došlo k chybě.",
|
||||
@@ -250,6 +252,8 @@
|
||||
"@errorMessageStoragePathCreateFailed": {},
|
||||
"errorMessageStoragePathLoadFailed": "Nelze načíst cestu k úložišti.",
|
||||
"@errorMessageStoragePathLoadFailed": {},
|
||||
"errorMessageSuggestionsQueryError": "",
|
||||
"@errorMessageSuggestionsQueryError": {},
|
||||
"errorMessageTagCreateFailed": "Nelze vytvořit tag, zkuste to znovu.",
|
||||
"@errorMessageTagCreateFailed": {},
|
||||
"errorMessageTagLoadFailed": "Nelze načíst tagy.",
|
||||
@@ -260,25 +264,25 @@
|
||||
"@errorMessageUnsupportedFileFormat": {},
|
||||
"errorReportLabel": "NAHLÁSIT",
|
||||
"@errorReportLabel": {},
|
||||
"extendedDateRangeDialogAbsoluteLabel": "Absolute",
|
||||
"extendedDateRangeDialogAbsoluteLabel": "",
|
||||
"@extendedDateRangeDialogAbsoluteLabel": {},
|
||||
"extendedDateRangeDialogHintText": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.",
|
||||
"extendedDateRangeDialogHintText": "",
|
||||
"@extendedDateRangeDialogHintText": {},
|
||||
"extendedDateRangeDialogRelativeAmountLabel": "Amount",
|
||||
"extendedDateRangeDialogRelativeAmountLabel": "",
|
||||
"@extendedDateRangeDialogRelativeAmountLabel": {},
|
||||
"extendedDateRangeDialogRelativeLabel": "Relative",
|
||||
"extendedDateRangeDialogRelativeLabel": "",
|
||||
"@extendedDateRangeDialogRelativeLabel": {},
|
||||
"extendedDateRangeDialogRelativeLastLabel": "Last",
|
||||
"extendedDateRangeDialogRelativeLastLabel": "",
|
||||
"@extendedDateRangeDialogRelativeLastLabel": {},
|
||||
"extendedDateRangeDialogRelativeTimeUnitLabel": "Time unit",
|
||||
"extendedDateRangeDialogRelativeTimeUnitLabel": "",
|
||||
"@extendedDateRangeDialogRelativeTimeUnitLabel": {},
|
||||
"extendedDateRangeDialogTitle": "Select date range",
|
||||
"extendedDateRangeDialogTitle": "",
|
||||
"@extendedDateRangeDialogTitle": {},
|
||||
"extendedDateRangePickerAfterLabel": "After",
|
||||
"extendedDateRangePickerAfterLabel": "",
|
||||
"@extendedDateRangePickerAfterLabel": {},
|
||||
"extendedDateRangePickerBeforeLabel": "Before",
|
||||
"extendedDateRangePickerBeforeLabel": "",
|
||||
"@extendedDateRangePickerBeforeLabel": {},
|
||||
"extendedDateRangePickerDayText": "{count, plural, zero{} one{day} other{days}}",
|
||||
"extendedDateRangePickerDayText": "{count, plural, other{}}",
|
||||
"@extendedDateRangePickerDayText": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
@@ -298,9 +302,9 @@
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"extendedDateRangePickerLastText": "Last",
|
||||
"extendedDateRangePickerLastText": "",
|
||||
"@extendedDateRangePickerLastText": {},
|
||||
"extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}",
|
||||
"extendedDateRangePickerLastWeeksLabel": "{count, plural, other{}}",
|
||||
"@extendedDateRangePickerLastWeeksLabel": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
@@ -312,7 +316,7 @@
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}",
|
||||
"extendedDateRangePickerMonthText": "{count, plural, other{}}",
|
||||
"@extendedDateRangePickerMonthText": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
@@ -320,13 +324,13 @@
|
||||
},
|
||||
"extendedDateRangePickerToLabel": "Do",
|
||||
"@extendedDateRangePickerToLabel": {},
|
||||
"extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}",
|
||||
"extendedDateRangePickerWeekText": "{count, plural, other{}}",
|
||||
"@extendedDateRangePickerWeekText": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}",
|
||||
"extendedDateRangePickerYearText": "{count, plural, other{}}",
|
||||
"@extendedDateRangePickerYearText": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
@@ -428,7 +432,7 @@
|
||||
"@loginPageClientCertificateSettingLabel": {},
|
||||
"loginPageClientCertificateSettingSelectFileText": "Vybrat soubor...",
|
||||
"@loginPageClientCertificateSettingSelectFileText": {},
|
||||
"loginPageContinueLabel": "Continue",
|
||||
"loginPageContinueLabel": "",
|
||||
"@loginPageContinueLabel": {},
|
||||
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Chybná nebo chybějící heslová fráze certifikátu.",
|
||||
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
|
||||
@@ -438,27 +442,27 @@
|
||||
"@loginPagePasswordFieldLabel": {},
|
||||
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
|
||||
"@loginPagePasswordValidatorMessageText": {},
|
||||
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
|
||||
"loginPageReachabilityInvalidClientCertificateConfigurationText": "",
|
||||
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
||||
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
|
||||
"loginPageReachabilityMissingClientCertificateText": "",
|
||||
"@loginPageReachabilityMissingClientCertificateText": {},
|
||||
"loginPageReachabilityNotReachableText": "Could not establish a connection to the server.",
|
||||
"loginPageReachabilityNotReachableText": "",
|
||||
"@loginPageReachabilityNotReachableText": {},
|
||||
"loginPageReachabilitySuccessText": "Connection successfully established.",
|
||||
"loginPageReachabilitySuccessText": "",
|
||||
"@loginPageReachabilitySuccessText": {},
|
||||
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.",
|
||||
"loginPageReachabilityUnresolvedHostText": "",
|
||||
"@loginPageReachabilityUnresolvedHostText": {},
|
||||
"loginPageServerUrlFieldLabel": "'Adresa serveru",
|
||||
"@loginPageServerUrlFieldLabel": {},
|
||||
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
|
||||
"loginPageServerUrlValidatorMessageInvalidAddressText": "",
|
||||
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
||||
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
|
||||
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
||||
"loginPageSignInButtonLabel": "Sign In",
|
||||
"loginPageSignInButtonLabel": "",
|
||||
"@loginPageSignInButtonLabel": {},
|
||||
"loginPageSignInTitle": "Sign In",
|
||||
"loginPageSignInTitle": "",
|
||||
"@loginPageSignInTitle": {},
|
||||
"loginPageSignInToPrefixText": "Sign in to {serverAddress}",
|
||||
"loginPageSignInToPrefixText": "",
|
||||
"@loginPageSignInToPrefixText": {
|
||||
"placeholders": {
|
||||
"serverAddress": {}
|
||||
@@ -476,7 +480,7 @@
|
||||
"@onboardingDoneButtonLabel": {},
|
||||
"onboardingNextButtonLabel": "Další",
|
||||
"@onboardingNextButtonLabel": {},
|
||||
"receiveSharedFilePermissionDeniedMessage": "Could not access the received file. Please try to open the app before sharing.",
|
||||
"receiveSharedFilePermissionDeniedMessage": "",
|
||||
"@receiveSharedFilePermissionDeniedMessage": {},
|
||||
"referencedDocumentsReadOnlyHintText": "Tento náhled nelze upravovat! Nelze upravovat nebo odstraňovat dokumenty. Bude načteno maximálně 100 odkazovaných dokumentů.",
|
||||
"@referencedDocumentsReadOnlyHintText": {},
|
||||
@@ -540,12 +544,12 @@
|
||||
"@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": {},
|
||||
"verifyIdentityPageDescriptionText": "Use the configured biometric factor to authenticate and unlock your documents.",
|
||||
"verifyIdentityPageDescriptionText": "",
|
||||
"@verifyIdentityPageDescriptionText": {},
|
||||
"verifyIdentityPageLogoutButtonLabel": "Disconnect",
|
||||
"verifyIdentityPageLogoutButtonLabel": "",
|
||||
"@verifyIdentityPageLogoutButtonLabel": {},
|
||||
"verifyIdentityPageTitle": "Verify your identity",
|
||||
"verifyIdentityPageTitle": "",
|
||||
"@verifyIdentityPageTitle": {},
|
||||
"verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity",
|
||||
"verifyIdentityPageVerifyIdentityButtonLabel": "",
|
||||
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
|
||||
}
|
||||
@@ -194,6 +194,8 @@
|
||||
"@editLabelPageConfirmDeletionDialogTitle": {},
|
||||
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
|
||||
"@editLabelPageDeletionDialogText": {},
|
||||
"errorMessageAcknowledgeTasksError": "Dateiaufgabe konnte nicht verworfen werden.",
|
||||
"@errorMessageAcknowledgeTasksError": {},
|
||||
"errorMessageAuthenticationFailed": "Authentifizierung fehlgeschlagen, bitte versuche es erneut.",
|
||||
"@errorMessageAuthenticationFailed": {},
|
||||
"errorMessageAutocompleteQueryError": "Beim automatischen Vervollständigen ist ein Fehler aufgetreten.",
|
||||
@@ -250,6 +252,8 @@
|
||||
"@errorMessageStoragePathCreateFailed": {},
|
||||
"errorMessageStoragePathLoadFailed": "Speicherpfade konnten nicht geladen werden.",
|
||||
"@errorMessageStoragePathLoadFailed": {},
|
||||
"errorMessageSuggestionsQueryError": "Vorschläge konnten nicht geladen werden.",
|
||||
"@errorMessageSuggestionsQueryError": {},
|
||||
"errorMessageTagCreateFailed": "Tag konnte nicht erstellt werden, bitte versuche es erneut.",
|
||||
"@errorMessageTagCreateFailed": {},
|
||||
"errorMessageTagLoadFailed": "Tags konnten nicht geladen werden.",
|
||||
|
||||
@@ -194,6 +194,8 @@
|
||||
"@editLabelPageConfirmDeletionDialogTitle": {},
|
||||
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
|
||||
"@editLabelPageDeletionDialogText": {},
|
||||
"errorMessageAcknowledgeTasksError": "Could not acknowledge tasks.",
|
||||
"@errorMessageAcknowledgeTasksError": {},
|
||||
"errorMessageAuthenticationFailed": "Authentication failed, please try again.",
|
||||
"@errorMessageAuthenticationFailed": {},
|
||||
"errorMessageAutocompleteQueryError": "An error ocurred while trying to autocomplete your query.",
|
||||
@@ -250,6 +252,8 @@
|
||||
"@errorMessageStoragePathCreateFailed": {},
|
||||
"errorMessageStoragePathLoadFailed": "Could not load storage paths.",
|
||||
"@errorMessageStoragePathLoadFailed": {},
|
||||
"errorMessageSuggestionsQueryError": "Could not load suggestions.",
|
||||
"@errorMessageSuggestionsQueryError": {},
|
||||
"errorMessageTagCreateFailed": "Could not create tag, please try again.",
|
||||
"@errorMessageTagCreateFailed": {},
|
||||
"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/connectivity_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/repository/impl/correspondent_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/storage_path_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/dio_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/generated/l10n.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
@@ -55,7 +53,6 @@ void main() async {
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await findSystemLocale();
|
||||
await LocalNotificationService.instance.initialize();
|
||||
|
||||
// Initialize External dependencies
|
||||
final connectivity = Connectivity();
|
||||
@@ -78,27 +75,18 @@ void main() async {
|
||||
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
||||
appSettingsCubit.state.preferredLocaleSubtag,
|
||||
);
|
||||
// Required for self signed client certificates
|
||||
final dioWrapper = AuthenticationAwareDioManager([
|
||||
DioHttpErrorInterceptor(),
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
responseBody: false,
|
||||
responseHeader: false,
|
||||
request: false,
|
||||
requestBody: false,
|
||||
requestHeader: false,
|
||||
),
|
||||
languageHeaderInterceptor,
|
||||
]);
|
||||
// Manages security context, required for self signed client certificates
|
||||
final sessionManager = SessionManager([languageHeaderInterceptor]);
|
||||
|
||||
// Initialize Paperless APIs
|
||||
final authApi = PaperlessAuthenticationApiImpl(dioWrapper.client);
|
||||
final documentsApi = PaperlessDocumentsApiImpl(dioWrapper.client);
|
||||
final labelsApi = PaperlessLabelApiImpl(dioWrapper.client);
|
||||
final statsApi = PaperlessServerStatsApiImpl(dioWrapper.client);
|
||||
final savedViewsApi = PaperlessSavedViewsApiImpl(dioWrapper.client);
|
||||
final tasksApi = PaperlessTasksApiImpl(dioWrapper.client);
|
||||
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
|
||||
final documentsApi = PaperlessDocumentsApiImpl(sessionManager.client);
|
||||
final labelsApi = PaperlessLabelApiImpl(sessionManager.client);
|
||||
final statsApi = PaperlessServerStatsApiImpl(sessionManager.client);
|
||||
final savedViewsApi = PaperlessSavedViewsApiImpl(sessionManager.client);
|
||||
final tasksApi = PaperlessTasksApiImpl(
|
||||
sessionManager.client,
|
||||
);
|
||||
|
||||
// Initialize Blocs/Cubits
|
||||
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
||||
@@ -119,20 +107,23 @@ void main() async {
|
||||
final authCubit = AuthenticationCubit(
|
||||
localAuthService,
|
||||
authApi,
|
||||
dioWrapper,
|
||||
sessionManager,
|
||||
);
|
||||
await authCubit
|
||||
.restoreSessionState(appSettingsCubit.state.isLocalAuthenticationEnabled);
|
||||
|
||||
if (authCubit.state.isAuthenticated) {
|
||||
final auth = authCubit.state.authentication!;
|
||||
dioWrapper.updateSettings(
|
||||
sessionManager.updateSettings(
|
||||
baseUrl: auth.serverUrl,
|
||||
authToken: auth.token,
|
||||
clientCertificate: auth.clientCertificate,
|
||||
);
|
||||
}
|
||||
|
||||
final localNotificationService = LocalNotificationService();
|
||||
await localNotificationService.initialize();
|
||||
|
||||
//Update language header in interceptor on language change.
|
||||
appSettingsCubit.stream.listen((event) => languageHeaderInterceptor
|
||||
.preferredLocaleSubtag = event.preferredLocaleSubtag);
|
||||
@@ -149,7 +140,7 @@ void main() async {
|
||||
create: (context) => cm.CacheManager(
|
||||
cm.Config(
|
||||
'cacheKey',
|
||||
fileService: DioFileService(dioWrapper.client),
|
||||
fileService: DioFileService(sessionManager.client),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -157,6 +148,9 @@ void main() async {
|
||||
Provider<ConnectivityStatusService>.value(
|
||||
value: connectivityStatusService,
|
||||
),
|
||||
Provider<LocalNotificationService>.value(
|
||||
value: localNotificationService,
|
||||
),
|
||||
],
|
||||
child: MultiRepositoryProvider(
|
||||
providers: [
|
||||
@@ -221,6 +215,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||
vertical: 16.0,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: Colors.lightGreen[50],
|
||||
),
|
||||
@@ -242,6 +237,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||
vertical: 16.0,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: Colors.green[900],
|
||||
),
|
||||
@@ -252,7 +248,9 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => PaperlessServerInformationCubit(context.read()),
|
||||
create: (context) => PaperlessServerInformationCubit(
|
||||
context.read<PaperlessServerStatsApi>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
@@ -260,14 +258,8 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: true,
|
||||
title: "Paperless Mobile",
|
||||
theme: _lightTheme.copyWith(
|
||||
listTileTheme: _lightTheme.listTileTheme
|
||||
.copyWith(tileColor: Colors.transparent),
|
||||
),
|
||||
darkTheme: _darkTheme.copyWith(
|
||||
listTileTheme: _darkTheme.listTileTheme
|
||||
.copyWith(tileColor: Colors.transparent),
|
||||
),
|
||||
theme: _lightTheme,
|
||||
darkTheme: _darkTheme,
|
||||
themeMode: settings.preferredThemeMode,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
locale: Locale.fromSubtags(
|
||||
|
||||
@@ -131,6 +131,6 @@ class DocumentModel extends Equatable {
|
||||
archiveSerialNumber,
|
||||
originalFileName,
|
||||
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
|
||||
final FilterRule extendedFilterRule = filterRules
|
||||
.where((r) => r.ruleType == extendedRule)
|
||||
.reduce((previousValue, element) => previousValue.copyWith(
|
||||
value: previousValue.value! + element.value!,
|
||||
));
|
||||
filterRules
|
||||
..removeWhere((element) => element.ruleType == extendedRule)
|
||||
..add(extendedFilterRule);
|
||||
//Join values of all extended filter rules if exist
|
||||
if (filterRules.isNotEmpty) {
|
||||
final FilterRule extendedFilterRule = filterRules
|
||||
.where((r) => r.ruleType == extendedRule)
|
||||
.reduce((previousValue, element) => previousValue.copyWith(
|
||||
value: previousValue.value! + element.value!,
|
||||
));
|
||||
filterRules
|
||||
..removeWhere((element) => element.ruleType == extendedRule)
|
||||
..add(extendedFilterRule);
|
||||
}
|
||||
return filterRules;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,3 +24,4 @@ export 'saved_view_model.dart';
|
||||
export 'similar_document_model.dart';
|
||||
export 'task/task.dart';
|
||||
export 'task/task_status.dart';
|
||||
export 'field_suggestions.dart';
|
||||
|
||||
@@ -43,6 +43,7 @@ enum ErrorCode {
|
||||
deviceOffline,
|
||||
serverUnreachable,
|
||||
similarQueryError,
|
||||
suggestionsQueryError,
|
||||
autocompleteQueryError,
|
||||
storagePathLoadFailed,
|
||||
storagePathCreateFailed,
|
||||
@@ -51,5 +52,6 @@ enum ErrorCode {
|
||||
deleteSavedViewError,
|
||||
requestTimedOut,
|
||||
unsupportedFileFormat,
|
||||
missingClientCertificate;
|
||||
missingClientCertificate,
|
||||
acknowledgeTasksError;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:paperless_api/src/request_utils.dart';
|
||||
|
||||
class PaperlessServerInformationModel {
|
||||
static const String versionHeader = 'x-version';
|
||||
static const String apiVersionHeader = 'x-api-version';
|
||||
@@ -13,4 +15,9 @@ class PaperlessServerInformationModel {
|
||||
this.version = 'unknown',
|
||||
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 bool acknowledged;
|
||||
@JsonKey(fromJson: tryParseNullable)
|
||||
final int? relatedDocumentId;
|
||||
final int? relatedDocument;
|
||||
|
||||
const Task({
|
||||
required this.id,
|
||||
@@ -28,7 +28,7 @@ class Task extends Equatable {
|
||||
this.type,
|
||||
this.status,
|
||||
this.acknowledged = false,
|
||||
this.relatedDocumentId,
|
||||
this.relatedDocument,
|
||||
this.result,
|
||||
});
|
||||
|
||||
@@ -47,6 +47,32 @@ class Task extends Equatable {
|
||||
status,
|
||||
result,
|
||||
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?,
|
||||
status: $enumDecodeNullable(_$TaskStatusEnumMap, json['status']),
|
||||
acknowledged: json['acknowledged'] as bool? ?? false,
|
||||
relatedDocumentId:
|
||||
tryParseNullable(json['related_document_id'] as String?),
|
||||
relatedDocument: tryParseNullable(json['related_document'] as String?),
|
||||
result: json['result'] as String?,
|
||||
);
|
||||
|
||||
@@ -32,7 +31,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'status': _$TaskStatusEnumMap[instance.status],
|
||||
'result': instance.result,
|
||||
'acknowledged': instance.acknowledged,
|
||||
'related_document_id': instance.relatedDocumentId,
|
||||
'related_document': instance.relatedDocument,
|
||||
};
|
||||
|
||||
const _$TaskStatusEnumMap = {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:paperless_api/src/models/bulk_edit_model.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';
|
||||
import 'package:paperless_api/src/models/models.dart';
|
||||
|
||||
abstract class PaperlessDocumentsApi {
|
||||
/// 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<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<int> delete(DocumentModel doc);
|
||||
Future<DocumentMetaData> getMetaData(DocumentModel document);
|
||||
Future<Iterable<int>> bulkAction(BulkAction action);
|
||||
Future<Uint8List> getPreview(int docId);
|
||||
String getThumbnailUrl(int docId);
|
||||
Future<DocumentModel> waitForConsumptionFinished(
|
||||
String filename,
|
||||
String title,
|
||||
);
|
||||
Future<Uint8List> download(DocumentModel document);
|
||||
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
||||
|
||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.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 {
|
||||
final Dio client;
|
||||
@@ -82,8 +78,11 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter) async {
|
||||
final filterParams = filter.toQueryParameters();
|
||||
Future<PagedSearchResult<DocumentModel>> findAll(
|
||||
DocumentFilter filter,
|
||||
) async {
|
||||
final filterParams = filter.toQueryParameters()
|
||||
..addAll({'truncate_content': "true"});
|
||||
try {
|
||||
final response = await client.get(
|
||||
"/api/documents/",
|
||||
@@ -156,7 +155,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
pageSize: 1,
|
||||
);
|
||||
try {
|
||||
final result = await find(asnQueryFilter);
|
||||
final result = await findAll(asnQueryFilter);
|
||||
return result.results
|
||||
.map((e) => e.archiveSerialNumber)
|
||||
.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
|
||||
Future<Uint8List> download(DocumentModel document) async {
|
||||
try {
|
||||
@@ -273,4 +252,32 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
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<Iterable<Task>> findAll([Iterable<int>? ids]);
|
||||
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 'package:paperless_api/src/models/task/task.dart';
|
||||
import 'package:paperless_api/src/models/task/task_status.dart';
|
||||
import 'dart:developer';
|
||||
|
||||
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 {
|
||||
final Dio client;
|
||||
final Dio _client;
|
||||
|
||||
const PaperlessTasksApiImpl(this.client);
|
||||
PaperlessTasksApiImpl(this._client);
|
||||
|
||||
@override
|
||||
Future<Task?> find({int? id, String? taskId}) async {
|
||||
assert(id != null || taskId != null);
|
||||
String url = "/api/tasks/";
|
||||
if (taskId != null) {
|
||||
url += "?task_id=$taskId";
|
||||
} else {
|
||||
url += "$id/";
|
||||
assert((id != null) != (taskId != null));
|
||||
if (id != null) {
|
||||
return _findById(id);
|
||||
} else if (taskId != null) {
|
||||
return _findByTaskId(taskId);
|
||||
}
|
||||
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) {
|
||||
return Task.fromJson(response.data);
|
||||
}
|
||||
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
|
||||
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) {
|
||||
return (response.data as List).map((e) => Task.fromJson(e));
|
||||
}
|
||||
@@ -37,17 +51,39 @@ class PaperlessTasksApiImpl implements PaperlessTasksApi {
|
||||
|
||||
@override
|
||||
Stream<Task> listenForTaskChanges(String taskId) async* {
|
||||
bool isSuccess = false;
|
||||
while (!isSuccess) {
|
||||
bool isCompleted = false;
|
||||
while (!isCompleted) {
|
||||
final task = await find(taskId: taskId);
|
||||
if (task == null) {
|
||||
throw Exception("Task with taskId $taskId does not exist.");
|
||||
}
|
||||
log("Found new task: ${task.taskId}, ${task.id}, ${task.status}");
|
||||
yield task;
|
||||
if (task.status == TaskStatus.success) {
|
||||
isSuccess = true;
|
||||
if (task.status == TaskStatus.success ||
|
||||
task.status == TaskStatus.failure) {
|
||||
isCompleted = true;
|
||||
}
|
||||
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"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -696,14 +704,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -784,30 +784,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1308,14 +1284,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1457,14 +1425,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -57,7 +57,6 @@ dependencies:
|
||||
equatable: ^2.0.3
|
||||
flutter_form_builder: ^7.5.0
|
||||
form_builder_validators: ^8.4.0
|
||||
infinite_scroll_pagination: ^3.2.0
|
||||
package_info_plus: ^1.4.3+1
|
||||
font_awesome_flutter: ^10.1.0
|
||||
local_auth: ^2.1.2
|
||||
@@ -85,6 +84,7 @@ dependencies:
|
||||
collection: ^1.17.0
|
||||
device_info_plus: ^4.1.3
|
||||
flutter_local_notifications: ^13.0.0
|
||||
flutter_staggered_grid_view: ^0.6.2
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
@@ -92,7 +92,6 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.1.11
|
||||
injectable_generator: ^2.1.0
|
||||
mockito: ^5.3.2
|
||||
bloc_test: ^9.1.0
|
||||
dependency_validator: ^3.0.0
|
||||
|
||||
Reference in New Issue
Block a user