WIP - more decoupling of blocs

This commit is contained in:
Anton Stubenbord
2022-12-12 01:29:34 +01:00
parent e2a20cea75
commit 2f31d9c053
51 changed files with 1083 additions and 800 deletions

View File

@@ -34,4 +34,10 @@ class AuthenticationInterceptor implements InterceptorContract {
Future<BaseResponse> interceptResponse( Future<BaseResponse> interceptResponse(
{required BaseResponse response}) async => {required BaseResponse response}) async =>
response; response;
@override
Future<bool> shouldInterceptRequest() async => true;
@override
Future<bool> shouldInterceptResponse() async => true;
} }

View File

@@ -25,4 +25,10 @@ class BaseUrlInterceptor implements InterceptorContract {
Future<BaseResponse> interceptResponse( Future<BaseResponse> interceptResponse(
{required BaseResponse response}) async => {required BaseResponse response}) async =>
response; response;
@override
Future<bool> shouldInterceptRequest() async => true;
@override
Future<bool> shouldInterceptResponse() async => true;
} }

View File

@@ -25,4 +25,10 @@ class LanguageHeaderInterceptor implements InterceptorContract {
Future<BaseResponse> interceptResponse( Future<BaseResponse> interceptResponse(
{required BaseResponse response}) async => {required BaseResponse response}) async =>
response; response;
@override
Future<bool> shouldInterceptRequest() async => true;
@override
Future<bool> shouldInterceptResponse() async => true;
} }

View File

@@ -1,5 +1,4 @@
import 'package:http/http.dart'; import 'package:http_interceptor/http_interceptor.dart';
import 'package:http_interceptor/http/http.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
const interceptedRoutes = ['thumb/']; const interceptedRoutes = ['thumb/'];
@@ -33,4 +32,10 @@ class ResponseConversionInterceptor implements InterceptorContract {
} }
return response; return response;
} }
@override
Future<bool> shouldInterceptRequest() async => true;
@override
Future<bool> shouldInterceptResponse() async => true;
} }

View File

@@ -64,4 +64,7 @@ class CorrespondentRepositoryImpl implements LabelRepository<Correspondent> {
void clear() { void clear() {
_subject.add(const {}); _subject.add(const {});
} }
@override
Map<int, Correspondent> get current => _subject.value;
} }

View File

@@ -63,4 +63,7 @@ class DocumentTypeRepositoryImpl implements LabelRepository<DocumentType> {
void clear() { void clear() {
_subject.add(const {}); _subject.add(const {});
} }
@override
Map<int, DocumentType> get current => _subject.value;
} }

View File

@@ -63,4 +63,7 @@ class StoragePathRepositoryImpl implements LabelRepository<StoragePath> {
void clear() { void clear() {
_subject.add(const {}); _subject.add(const {});
} }
@override
Map<int, StoragePath> get current => _subject.value;
} }

View File

@@ -62,4 +62,7 @@ class TagRepositoryImpl implements LabelRepository<Tag> {
void clear() { void clear() {
_subject.add(const {}); _subject.add(const {});
} }
@override
Map<int, Tag> get current => _subject.value;
} }

View File

@@ -3,6 +3,8 @@ import 'package:paperless_api/paperless_api.dart';
abstract class LabelRepository<T extends Label> { abstract class LabelRepository<T extends Label> {
Stream<Map<int, T>> get labels; Stream<Map<int, T>> get labels;
Map<int, T> get current;
Future<T> create(T label); Future<T> create(T label);
Future<T?> find(int id); Future<T?> find(int id);
Future<Iterable<T>> findAll([Iterable<int>? ids]); Future<Iterable<T>> findAll([Iterable<int>? ids]);

View File

@@ -71,8 +71,7 @@ Note: If you have the GitHub Android app installed, the descriptions will not be
Text( Text(
'Stack Trace', 'Stack Trace',
style: Theme.of(context).textTheme.subtitle1, style: Theme.of(context).textTheme.subtitle1,
).padded( ).paddedOnly(top: 8.0, left: 8.0, right: 8.0),
const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0)),
TextButton.icon( TextButton.icon(
label: const Text('Copy'), label: const Text('Copy'),
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),

View File

@@ -3,13 +3,12 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart'; import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart';
import 'package:paperless_mobile/core/interceptor/base_url_interceptor.dart'; import 'package:paperless_mobile/core/interceptor/base_url_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart';
import 'package:http/http.dart';
import 'package:http/io_client.dart'; import 'package:http/io_client.dart';
import 'package:http_interceptor/http/http.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';

View File

@@ -1,9 +1,32 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
extension WidgetPadding on Widget { extension WidgetPadding on Widget {
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(8)]) { Widget padded([double all = 8.0]) {
return Padding( return Padding(
padding: value, padding: EdgeInsets.all(all),
child: this,
);
}
Widget paddedSymmetrically({double horizontal = 0.0, double vertical = 0.0}) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical),
child: this,
);
}
Widget paddedOnly(
{double top = 0.0,
double bottom = 0.0,
double left = 0.0,
double right = 0.0}) {
return Padding(
padding: EdgeInsets.only(
top: top,
bottom: bottom,
left: left,
right: right,
),
child: this, child: this,
); );
} }

View File

@@ -12,18 +12,18 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<void> delete(DocumentModel document) async { Future<void> delete(DocumentModel document) async {
await _api.delete(document); await _api.delete(document);
emit(const DocumentDetailsState());
}
Future<void> update(DocumentModel document) async {
final updatedDocument = await _api.update(document);
emit(DocumentDetailsState(document: updatedDocument));
} }
Future<void> assignAsn(DocumentModel document) async { Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) { if (document.archiveSerialNumber == null) {
final int asn = await _api.findNextAsn(); final int asn = await _api.findNextAsn();
update(document.copyWith(archiveSerialNumber: asn)); final updatedDocument =
await _api.update(document.copyWith(archiveSerialNumber: asn));
emit(DocumentDetailsState(document: updatedDocument));
} }
} }
void replaceDocument(DocumentModel document) {
emit(DocumentDetailsState(document: document));
}
} }

View File

@@ -1,10 +1,10 @@
part of 'document_details_cubit.dart'; part of 'document_details_cubit.dart';
class DocumentDetailsState with EquatableMixin { class DocumentDetailsState with EquatableMixin {
final DocumentModel? document; final DocumentModel document;
const DocumentDetailsState({ const DocumentDetailsState({
this.document, required this.document,
}); });
@override @override

View File

@@ -18,6 +18,7 @@ import 'package:paperless_mobile/features/documents/view/pages/document_edit_pag
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart'; import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
@@ -65,9 +66,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Scaffold( child: Scaffold(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: widget.allowEdit floatingActionButton: widget.allowEdit
? FloatingActionButton( ? BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
child: const Icon(Icons.edit), builder: (context, state) {
onPressed: _onEdit, return FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
);
},
) )
: null, : null,
bottomNavigationBar: bottomNavigationBar:
@@ -79,24 +84,20 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: widget.allowEdit && state.document != null onPressed: widget.allowEdit
? () => _onDelete(state.document!) ? () => _onDelete(state.document)
: null, : null,
).padded(const EdgeInsets.symmetric(horizontal: 4)), ).paddedSymmetrically(horizontal: 4),
DocumentDownloadButton( DocumentDownloadButton(
document: state.document, document: state.document,
), ),
IconButton( IconButton(
icon: const Icon(Icons.open_in_new), icon: const Icon(Icons.open_in_new),
onPressed: state.document != null onPressed: () => _onOpen(state.document),
? () => _onOpen(state.document!) ).paddedOnly(right: 4.0),
: null,
).padded(const EdgeInsets.only(right: 4)),
IconButton( IconButton(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
onPressed: state.document != null onPressed: () => _onShare(state.document),
? () => _onShare(state.document!)
: null,
), ),
], ],
), ),
@@ -123,15 +124,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
expandedHeight: 200.0, expandedHeight: 200.0,
flexibleSpace: flexibleSpace:
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) { builder: (context, state) => DocumentPreview(
if (state.document == null) { id: state.document.id,
return Container(height: 200); fit: BoxFit.cover,
} ),
return DocumentPreview(
id: state.document!.id,
fit: BoxFit.cover,
);
},
), ),
bottom: ColoredTabBar( bottom: ColoredTabBar(
backgroundColor: backgroundColor:
@@ -172,27 +168,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
], ],
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>( body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) { builder: (context, state) {
if (state.document == null) {
return TabBarView(
children: [
Container(),
Container(),
Container(),
],
);
}
return TabBarView( return TabBarView(
children: [ children: [
_buildDocumentOverview( _buildDocumentOverview(
state.document!, state.document,
widget.titleAndContentQueryString, widget.titleAndContentQueryString,
), ),
_buildDocumentContentView( _buildDocumentContentView(
state.document!, state.document,
widget.titleAndContentQueryString, widget.titleAndContentQueryString,
), ),
_buildDocumentMetaDataView( _buildDocumentMetaDataView(
state.document!, state.document,
), ),
].padded(), ].padded(),
); );
@@ -204,47 +191,42 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
); );
} }
Future<void> _onEdit() async { Future<void> _onEdit(DocumentModel document) async {
{ {
final cubit = BlocProvider.of<DocumentDetailsCubit>(context); final cubit = BlocProvider.of<DocumentDetailsCubit>(context);
if (cubit.state.document == null) {
return;
}
Navigator.push<bool>( Navigator.push<bool>(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MultiRepositoryProvider( builder: (context) => BlocProvider(
providers: [ create: (context) => EditDocumentCubit(
RepositoryProvider.value( document,
value: RepositoryProvider.of<LabelRepository<DocumentType>>( documentsApi: getIt<PaperlessDocumentsApi>(),
context, correspondentRepository:
), RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
), ),
RepositoryProvider.value( documentTypeRepository:
value: RepositoryProvider.of<LabelRepository<Tag>>( RepositoryProvider.of<LabelRepository<DocumentType>>(
context, context,
),
), ),
RepositoryProvider.value( storagePathRepository:
value: RepositoryProvider.of<LabelRepository<StoragePath>>( RepositoryProvider.of<LabelRepository<StoragePath>>(
context, context,
),
), ),
RepositoryProvider.value( tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
value: RepositoryProvider.of<LabelRepository<Correspondent>>( context,
context,
),
), ),
], ),
child: DocumentEditPage( child: BlocListener<EditDocumentCubit, EditDocumentState>(
document: cubit.state.document!, listenWhen: (previous, current) =>
onEdit: (updatedDocument) { previous.document != current.document,
return BlocProvider.of<DocumentDetailsCubit>(context) listener: (context, state) {
.update(updatedDocument); cubit.replaceDocument(state.document);
}, },
child: const DocumentEditPage(),
), ),
), ),
maintainState: false, maintainState: true,
), ),
); );
} }

View File

@@ -1,4 +1,3 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -33,7 +32,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
onPressed: Platform.isAndroid && widget.document != null onPressed: Platform.isAndroid && widget.document != null
? () => _onDownload(widget.document!) ? () => _onDownload(widget.document!)
: null, : null,
).padded(const EdgeInsets.only(right: 4)); ).paddedOnly(right: 4);
} }
Future<void> _onDownload(DocumentModel document) async { Future<void> _onDownload(DocumentModel document) async {

View File

@@ -200,11 +200,12 @@ class _DocumentUploadPreparationPageState
CorrespondentQuery.notAssigned, CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
), ),
const TagFormField( TagFormField(
name: DocumentModel.tagsKey, name: DocumentModel.tagsKey,
notAssignedSelectable: false, notAssignedSelectable: false,
anyAssignedSelectable: false, anyAssignedSelectable: false,
excludeAllowed: false, excludeAllowed: false,
selectableOptions: state.tags,
//Label: "Tags" + " *", //Label: "Tags" + " *",
), ),
Text( Text(

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@@ -8,27 +7,19 @@ import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
class DocumentEditPage extends StatefulWidget { class DocumentEditPage extends StatefulWidget {
final DocumentModel document;
final FutureOr<void> Function(DocumentModel updatedDocument) onEdit;
const DocumentEditPage({ const DocumentEditPage({
Key? key, Key? key,
required this.document,
required this.onEdit,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -43,150 +34,133 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
static const fkCreatedDate = "createdAtDate"; static const fkCreatedDate = "createdAtDate";
static const fkStoragePath = 'storagePath'; static const fkStoragePath = 'storagePath';
late Future<Uint8List> documentBytes;
final GlobalKey<FormBuilderState> _formKey = GlobalKey(); final GlobalKey<FormBuilderState> _formKey = GlobalKey();
bool _isSubmitLoading = false; bool _isSubmitLoading = false;
@override @override
void initState() { Widget build(BuildContext context) {
super.initState(); return BlocBuilder<EditDocumentCubit, EditDocumentState>(
documentBytes = builder: (context, state) {
getIt<PaperlessDocumentsApi>().getPreview(widget.document.id); return Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _onSubmit(state.document),
icon: const Icon(Icons.save),
label: Text(S.of(context).genericActionSaveLabel),
),
appBar: AppBar(
title: Text(S.of(context).documentEditPageTitle),
bottom: _isSubmitLoading
? const PreferredSize(
preferredSize: Size.fromHeight(4),
child: LinearProgressIndicator(),
)
: null,
),
extendBody: true,
body: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 8,
left: 8,
right: 8,
),
child: FormBuilder(
key: _formKey,
child: ListView(children: [
_buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created).padded(),
_buildDocumentTypeFormField(
state.document.documentType, state.documentTypes)
.padded(),
_buildCorrespondentFormField(
state.document.correspondent, state.correspondents)
.padded(),
_buildStoragePathFormField(
state.document.storagePath, state.storagePaths)
.padded(),
TagFormField(
initialValue: IdsTagsQuery.included(state.document.tags),
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
name: fkTags,
selectableOptions: state.tags,
).padded(),
]),
),
));
},
);
} }
@override Widget _buildStoragePathFormField(
Widget build(BuildContext context) { int? initialId, Map<int, StoragePath> options) {
return LabelsBlocProvider( return LabelFormField<StoragePath, StoragePathQuery>(
child: Scaffold( notAssignedSelectable: false,
resizeToAvoidBottomInset: false, formBuilderState: _formKey.currentState,
floatingActionButton: FloatingActionButton.extended( labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value(
onPressed: _onSubmit, value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
icon: const Icon(Icons.save), child: AddStoragePathPage(initalValue: initialValue),
label: Text(S.of(context).genericActionSaveLabel), ),
label: S.of(context).documentStoragePathPropertyLabel,
state: options,
initialValue: StoragePathQuery.fromId(initialId),
name: fkStoragePath,
queryParameterIdBuilder: StoragePathQuery.fromId,
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
prefixIcon: const Icon(Icons.folder_outlined),
);
}
Widget _buildCorrespondentFormField(
int? initialId, Map<int, Correspondent> options) {
return LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
), ),
appBar: AppBar( child: AddCorrespondentPage(initialName: initialValue),
title: Text(S.of(context).documentEditPageTitle), ),
bottom: _isSubmitLoading label: S.of(context).documentCorrespondentPropertyLabel,
? const PreferredSize( state: options,
preferredSize: Size.fromHeight(4), initialValue: CorrespondentQuery.fromId(initialId),
child: LinearProgressIndicator(), name: fkCorrespondent,
) queryParameterIdBuilder: CorrespondentQuery.fromId,
: null, queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outlined),
);
}
Widget _buildDocumentTypeFormField(
int? initialId, Map<int, DocumentType> options) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
), ),
extendBody: true, child: AddDocumentTypePage(
body: Padding( initialName: currentInput,
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 8,
left: 8,
right: 8,
),
child: FormBuilder(
key: _formKey,
child: ListView(children: [
_buildTitleFormField().padded(),
_buildCreatedAtFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildCorrespondentFormField().padded(),
_buildStoragePathFormField().padded(),
TagFormField(
initialValue: IdsTagsQuery.included(widget.document.tags),
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
name: fkTags,
).padded(),
]),
),
), ),
), ),
label: S.of(context).documentDocumentTypePropertyLabel,
initialValue: DocumentTypeQuery.fromId(initialId),
state: options,
name: fkDocumentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined),
); );
} }
BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>> Future<void> _onSubmit(DocumentModel document) async {
_buildStoragePathFormField() {
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
child: AddStoragePathPage(initalValue: initialValue),
),
label: S.of(context).documentStoragePathPropertyLabel,
state: state.labels,
initialValue: StoragePathQuery.fromId(widget.document.storagePath),
name: fkStoragePath,
queryParameterIdBuilder: StoragePathQuery.fromId,
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
prefixIcon: const Icon(Icons.folder_outlined),
);
},
);
}
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>
_buildCorrespondentFormField() {
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
child: AddCorrespondentPage(initialName: initialValue),
),
label: S.of(context).documentCorrespondentPropertyLabel,
state: state.labels,
initialValue:
CorrespondentQuery.fromId(widget.document.correspondent),
name: fkCorrespondent,
queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outlined),
);
},
);
}
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>
_buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) =>
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
label: S.of(context).documentDocumentTypePropertyLabel,
initialValue: DocumentTypeQuery.fromId(widget.document.documentType),
state: state.labels,
name: fkDocumentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined),
);
},
);
}
Future<void> _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value; final values = _formKey.currentState!.value;
var updatedDocument = widget.document.copyWith( var mergedDocument = document.copyWith(
title: values[fkTitle], title: values[fkTitle],
created: values[fkCreatedDate], created: values[fkCreatedDate],
overwriteDocumentType: true, overwriteDocumentType: true,
@@ -201,9 +175,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
setState(() { setState(() {
_isSubmitLoading = true; _isSubmitLoading = true;
}); });
try { try {
await widget.onEdit(updatedDocument); await BlocProvider.of<EditDocumentCubit>(context)
.updateDocument(mergedDocument);
showSnackBar(context, S.of(context).documentUpdateSuccessMessage); showSnackBar(context, S.of(context).documentUpdateSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
@@ -216,18 +190,18 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
} }
} }
Widget _buildTitleFormField() { Widget _buildTitleFormField(String? initialTitle) {
return FormBuilderTextField( return FormBuilderTextField(
name: fkTitle, name: fkTitle,
validator: FormBuilderValidators.required(), validator: FormBuilderValidators.required(),
decoration: InputDecoration( decoration: InputDecoration(
label: Text(S.of(context).documentTitlePropertyLabel), label: Text(S.of(context).documentTitlePropertyLabel),
), ),
initialValue: widget.document.title, initialValue: initialTitle,
); );
} }
Widget _buildCreatedAtFormField() { Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
return FormBuilderDateTimePicker( return FormBuilderDateTimePicker(
inputType: InputType.date, inputType: InputType.date,
name: fkCreatedDate, name: fkCreatedDate,
@@ -235,7 +209,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
prefixIcon: const Icon(Icons.calendar_month_outlined), prefixIcon: const Icon(Icons.calendar_month_outlined),
label: Text(S.of(context).documentCreatedPropertyLabel), label: Text(S.of(context).documentCreatedPropertyLabel),
), ),
initialValue: widget.document.created, initialValue: initialCreatedAtDate,
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
initialEntryMode: DatePickerEntryMode.calendar, initialEntryMode: DatePickerEntryMode.calendar,
); );

View File

@@ -1,10 +1,10 @@
import 'package:badges/badges.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart'; import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
@@ -24,7 +24,6 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
class DocumentsPage extends StatefulWidget { class DocumentsPage extends StatefulWidget {
const DocumentsPage({Key? key}) : super(key: key); const DocumentsPage({Key? key}) : super(key: key);
@@ -35,16 +34,17 @@ class DocumentsPage extends StatefulWidget {
class _DocumentsPageState extends State<DocumentsPage> { class _DocumentsPageState extends State<DocumentsPage> {
late final DocumentsCubit _documentsCubit; late final DocumentsCubit _documentsCubit;
late final SavedViewCubit _savedViewCubit;
final _pagingController = PagingController<int, DocumentModel>( final _pagingController = PagingController<int, DocumentModel>(
firstPageKey: 1, firstPageKey: 1,
); );
final _filterPanelController = PanelController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_documentsCubit = BlocProvider.of<DocumentsCubit>(context); _documentsCubit = BlocProvider.of<DocumentsCubit>(context);
_savedViewCubit = BlocProvider.of<SavedViewCubit>(context);
try { try {
_documentsCubit.load(); _documentsCubit.load();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
@@ -59,97 +59,69 @@ class _DocumentsPageState extends State<DocumentsPage> {
super.dispose(); super.dispose();
} }
Future<void> _loadNewPage(int pageKey) async {
final pageCount = _documentsCubit.state
.inferPageCount(pageSize: _documentsCubit.state.filter.pageSize);
if (pageCount <= pageKey + 1) {
_pagingController.nextPageKey = null;
}
try {
await _documentsCubit.loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
void _onSelected(DocumentModel model) {
_documentsCubit.toggleDocumentSelection(model);
}
Future<void> _onRefresh() async {
try {
await _documentsCubit.updateCurrentFilter(
(filter) => filter.copyWith(page: 1),
);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return BlocConsumer<ConnectivityCubit, ConnectivityState>(
onWillPop: () async { listenWhen: (previous, current) =>
if (_filterPanelController.isPanelOpen) { previous != ConnectivityState.connected &&
FocusScope.of(context).unfocus(); current == ConnectivityState.connected,
_filterPanelController.close(); listener: (context, state) {
return false; _documentsCubit.load();
}
if (_documentsCubit.state.selection.isNotEmpty) {
_documentsCubit.resetSelection();
return false;
}
return true;
}, },
child: BlocConsumer<ConnectivityCubit, ConnectivityState>( builder: (context, connectivityState) {
listenWhen: (previous, current) => return Scaffold(
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) {
_documentsCubit.load();
},
builder: (context, connectivityState) {
return Scaffold(
drawer: BlocProvider.value( drawer: BlocProvider.value(
value: BlocProvider.of<AuthenticationCubit>(context), value: BlocProvider.of<AuthenticationCubit>(context),
child: InfoDrawer( child: InfoDrawer(
afterInboxClosed: () => _documentsCubit.reload(), afterInboxClosed: () => _documentsCubit.reload(),
), ),
), ),
resizeToAvoidBottomInset: true, floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
body: SlidingUpPanel( builder: (context, state) {
backdropEnabled: true, final appliedFiltersCount = state.filter.appliedFiltersCount;
parallaxEnabled: true, return Badge(
parallaxOffset: .5, toAnimate: false,
controller: _filterPanelController, showBadge: appliedFiltersCount > 0,
defaultPanelState: PanelState.CLOSED, badgeContent: appliedFiltersCount > 0
minHeight: 48, ? Text(state.filter.appliedFiltersCount.toString())
maxHeight: (MediaQuery.of(context).size.height * 3) / 4, : null,
borderRadius: const BorderRadius.only( child: FloatingActionButton(
topLeft: Radius.circular(16), child: const Icon(Icons.filter_alt),
topRight: Radius.circular(16), onPressed: _openDocumentFilter,
), ),
body: _buildBody(connectivityState), );
color: Theme.of(context).scaffoldBackgroundColor, },
panelBuilder: (scrollController) =>
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return LabelsBlocProvider(
child: DocumentFilterPanel(
panelController: _filterPanelController,
scrollController: scrollController,
initialFilter: state.filter,
onFilterChanged: (filter) =>
_documentsCubit.updateFilter(filter: filter),
),
);
},
),
), ),
); resizeToAvoidBottomInset: true,
}, body: _buildBody(connectivityState));
},
);
}
void _openDocumentFilter() async {
final filter = await showModalBottomSheet(
context: context,
builder: (context) => SizedBox(
height: MediaQuery.of(context).size.height - kToolbarHeight - 16,
child: LabelsBlocProvider(
child: DocumentFilterPanel(
initialFilter: _documentsCubit.state.filter,
),
),
),
isDismissible: true,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
), ),
); );
if (filter != null) {
_documentsCubit.updateFilter(filter: filter);
_savedViewCubit.resetSelection();
}
} }
Widget _buildBody(ConnectivityState connectivityState) { Widget _buildBody(ConnectivityState connectivityState) {
@@ -193,6 +165,10 @@ class _DocumentsPageState extends State<DocumentsPage> {
child = SliverToBoxAdapter( child = SliverToBoxAdapter(
child: DocumentsEmptyState( child: DocumentsEmptyState(
state: state, state: state,
onReset: () {
_documentsCubit.updateFilter();
_savedViewCubit.resetSelection();
},
), ),
); );
} }
@@ -201,51 +177,45 @@ class _DocumentsPageState extends State<DocumentsPage> {
onRefresh: _onRefresh, onRefresh: _onRefresh,
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
BlocProvider( BlocListener<SavedViewCubit, SavedViewState>(
create: (context) => SavedViewCubit( listenWhen: (previous, current) =>
RepositoryProvider.of<SavedViewRepository>(context)), previous.selectedSavedViewId !=
child: BlocListener<SavedViewCubit, SavedViewState>( current.selectedSavedViewId,
listener: (context, state) { listener: (context, state) {
try { try {
if (state.selectedSavedViewId == null) { if (state.selectedSavedViewId == null) {
_documentsCubit.updateFilter(); _documentsCubit.updateFilter();
} else { } else {
final newFilter = state final newFilter = state
.value[state.selectedSavedViewId] .value[state.selectedSavedViewId]
?.toDocumentFilter(); ?.toDocumentFilter();
if (newFilter != null) { if (newFilter != null) {
_documentsCubit.updateFilter(filter: newFilter); _documentsCubit.updateFilter(filter: newFilter);
}
} }
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} }
}, } on PaperlessServerException catch (error, stackTrace) {
child: DocumentsPageAppBar( showErrorMessage(context, error, stackTrace);
actions: [ }
const SortDocumentsButton(), },
IconButton( child: DocumentsPageAppBar(
icon: Icon( actions: [
settings.preferredViewType == ViewType.grid const SortDocumentsButton(),
? Icons.list IconButton(
: Icons.grid_view, icon: Icon(
), settings.preferredViewType == ViewType.grid
onPressed: () => ? Icons.list
BlocProvider.of<ApplicationSettingsCubit>( : Icons.grid_view,
context)
.setViewType(
settings.preferredViewType.toggle()),
), ),
], onPressed: () =>
), BlocProvider.of<ApplicationSettingsCubit>(context)
.setViewType(
settings.preferredViewType.toggle(),
),
),
],
), ),
), ),
child, child,
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).size.height / 4,
),
)
], ],
), ),
); );
@@ -296,4 +266,31 @@ class _DocumentsPageState extends State<DocumentsPage> {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
} }
Future<void> _loadNewPage(int pageKey) async {
final pageCount = _documentsCubit.state
.inferPageCount(pageSize: _documentsCubit.state.filter.pageSize);
if (pageCount <= pageKey + 1) {
_pagingController.nextPageKey = null;
}
try {
await _documentsCubit.loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
void _onSelected(DocumentModel model) {
_documentsCubit.toggleDocumentSelection(model);
}
Future<void> _onRefresh() async {
try {
await _documentsCubit.updateCurrentFilter(
(filter) => filter.copyWith(page: 1),
);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
} }

View File

@@ -9,9 +9,11 @@ import 'package:paperless_mobile/generated/l10n.dart';
class DocumentsEmptyState extends StatelessWidget { class DocumentsEmptyState extends StatelessWidget {
final DocumentsState state; final DocumentsState state;
final VoidCallback onReset;
const DocumentsEmptyState({ const DocumentsEmptyState({
Key? key, Key? key,
required this.state, required this.state,
required this.onReset,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -22,10 +24,7 @@ class DocumentsEmptyState extends StatelessWidget {
subtitle: S.of(context).documentsPageEmptyStateNothingHereText, subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
bottomChild: state.filter != DocumentFilter.initial bottomChild: state.filter != DocumentFilter.initial
? TextButton( ? TextButton(
onPressed: () async { onPressed: onReset,
await BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).resetSelection();
},
child: Text( child: Text(
S.of(context).documentsFilterPageResetFilterLabel, S.of(context).documentsFilterPageResetFilterLabel,
), ),

View File

@@ -1,34 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/view/widgets/search/query_type_form_field.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.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/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
enum DateRangeSelection { before, after } enum DateRangeSelection { before, after }
class DocumentFilterPanel extends StatefulWidget { class DocumentFilterPanel extends StatefulWidget {
final PanelController panelController;
final ScrollController scrollController;
final DocumentFilter initialFilter; final DocumentFilter initialFilter;
final void Function(DocumentFilter filter) onFilterChanged;
const DocumentFilterPanel({ const DocumentFilterPanel({
Key? key, Key? key,
required this.panelController,
required this.scrollController,
required this.onFilterChanged,
required this.initialFilter, required this.initialFilter,
}) : super(key: key); }) : super(key: key);
@@ -60,33 +50,17 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const radius = Radius.circular(16);
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16), topLeft: radius,
topRight: Radius.circular(16), topRight: radius,
), ),
child: FormBuilder( child: FormBuilder(
key: _formKey, key: _formKey,
child: Column( child: Column(
children: [ children: [
Stack( _buildDraggableResetHeader(),
alignment: Alignment.center,
children: [
_buildDragLine(),
Align(
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label:
Text(S.of(context).documentsFilterPageResetFilterLabel),
onPressed: () => _resetFilter(context),
),
),
],
),
const SizedBox(
height: 8.0,
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -101,9 +75,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
], ],
).padded(), ).padded(),
const SizedBox(
height: 16.0,
),
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -111,34 +82,30 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
topRight: Radius.circular(16.0), topRight: Radius.circular(16.0),
), ),
child: ListView( child: ListView(
controller: widget.scrollController,
children: [ children: [
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(S.of(context).documentsFilterPageSearchLabel), child: Text(S.of(context).documentsFilterPageSearchLabel),
).padded(const EdgeInsets.only(left: 8.0)), ).paddedOnly(left: 8.0),
_buildQueryFormField(), _buildQueryFormField().padded(),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: child:
Text(S.of(context).documentsFilterPageAdvancedLabel), Text(S.of(context).documentsFilterPageAdvancedLabel),
).padded(const EdgeInsets.only(left: 8.0, top: 8.0)), ).padded(),
_buildCreatedDateRangePickerFormField().padded(), _buildCreatedDateRangePickerFormField(),
_buildAddedDateRangePickerFormField().padded(), _buildAddedDateRangePickerFormField(),
_buildCorrespondentFormField().padded(), _buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(), _buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(), _buildStoragePathFormField().padded(),
TagFormField( _buildTagsFormField()
name: DocumentModel.tagsKey, .paddedSymmetrically(horizontal: 8, vertical: 4.0),
initialValue: widget.initialFilter.tags,
allowCreation: false,
).padded(),
// Required in order for the storage path field to be visible when typing // Required in order for the storage path field to be visible when typing
const SizedBox( const SizedBox(
height: 150, height: 150,
), ),
], ],
).padded(), ),
), ),
), ),
], ],
@@ -147,13 +114,39 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
BlocBuilder<LabelCubit<Tag>, LabelState<Tag>> _buildTagsFormField() {
return BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) {
return TagFormField(
name: DocumentModel.tagsKey,
initialValue: widget.initialFilter.tags,
allowCreation: false,
selectableOptions: state.labels,
);
},
);
}
Stack _buildDraggableResetHeader() {
return Stack(
alignment: Alignment.center,
children: [
_buildDragLine(),
Align(
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label: Text(S.of(context).documentsFilterPageResetFilterLabel),
onPressed: () => _resetFilter(context),
),
),
],
);
}
void _resetFilter(BuildContext context) async { void _resetFilter(BuildContext context) async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
await BlocProvider.of<DocumentsCubit>(context).updateFilter(); Navigator.pop(context, DocumentFilter.initial);
BlocProvider.of<SavedViewCubit>(context).resetSelection();
if (!widget.panelController.isPanelClosed) {
widget.panelController.close();
}
} }
//TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField! //TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField!
@@ -238,14 +231,17 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
), ),
initialValue: widget.initialFilter.queryText, initialValue: widget.initialFilter.queryText,
).padded(); );
} }
Widget _buildDateRangePickerHelper(String formFieldKey) { Widget _buildDateRangePickerHelper(String formFieldKey) {
return SingleChildScrollView( const spacer = SizedBox(width: 8.0);
scrollDirection: Axis.horizontal, return SizedBox(
child: Row( height: 64,
child: ListView(
scrollDirection: Axis.horizontal,
children: [ children: [
spacer,
ActionChip( ActionChip(
label: Text( label: Text(
S.of(context).documentsFilterPageDateRangeLastSevenDaysLabel, S.of(context).documentsFilterPageDateRangeLastSevenDaysLabel,
@@ -258,7 +254,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
); );
}, },
).padded(const EdgeInsets.only(right: 8.0)), ),
spacer,
ActionChip( ActionChip(
label: Text( label: Text(
S.of(context).documentsFilterPageDateRangeLastMonthLabel, S.of(context).documentsFilterPageDateRangeLastMonthLabel,
@@ -275,7 +272,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
); );
}, },
).padded(const EdgeInsets.only(right: 8.0)), ),
spacer,
ActionChip( ActionChip(
label: Text( label: Text(
S.of(context).documentsFilterPageDateRangeLastThreeMonthsLabel, S.of(context).documentsFilterPageDateRangeLastThreeMonthsLabel,
@@ -295,7 +293,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
); );
}, },
).padded(const EdgeInsets.only(right: 8.0)), ),
spacer,
ActionChip( ActionChip(
label: Text( label: Text(
S.of(context).documentsFilterPageDateRangeLastYearLabel, S.of(context).documentsFilterPageDateRangeLastYearLabel,
@@ -316,6 +315,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
}, },
), ),
spacer,
], ],
), ),
); );
@@ -358,12 +358,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
labelText: S.of(context).documentCreatedPropertyLabel, labelText: S.of(context).documentCreatedPropertyLabel,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () => onPressed: () {
_formKey.currentState?.fields[fkCreatedAt]?.didChange(null), _formKey.currentState?.fields[fkCreatedAt]?.didChange(null);
},
), ),
), ),
), ).paddedSymmetrically(horizontal: 8, vertical: 4.0),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(fkCreatedAt), _buildDateRangePickerHelper(fkCreatedAt),
], ],
); );
@@ -393,7 +393,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
child: child!, child: child!,
), ),
format: DateFormat.yMMMd(Localizations.localeOf(context).toString()), format: DateFormat.yMMMd(),
fieldStartLabelText: fieldStartLabelText:
S.of(context).documentsFilterPageDateRangeFieldStartLabel, S.of(context).documentsFilterPageDateRangeFieldStartLabel,
fieldEndLabelText: fieldEndLabelText:
@@ -406,11 +406,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
labelText: S.of(context).documentAddedPropertyLabel, labelText: S.of(context).documentAddedPropertyLabel,
suffixIcon: IconButton( suffixIcon: IconButton(
icon: const Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () => onPressed: () {
_formKey.currentState?.fields[fkAddedAt]?.didChange(null), _formKey.currentState?.fields[fkAddedAt]?.didChange(null);
},
), ),
), ),
), ).paddedSymmetrically(horizontal: 8),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
_buildDateRangePickerHelper(fkAddedAt), _buildDateRangePickerHelper(fkAddedAt),
], ],
@@ -429,28 +430,33 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
} }
void _onApplyFilter() async { void _onApplyFilter() async {
if (_formKey.currentState?.saveAndValidate() ?? false) { _formKey.currentState?.save();
if (_formKey.currentState?.validate() ?? false) {
final v = _formKey.currentState!.value; final v = _formKey.currentState!.value;
final docCubit = BlocProvider.of<DocumentsCubit>(context); DocumentFilter newFilter = DocumentFilter(
DocumentFilter newFilter = docCubit.state.filter.copyWith(
createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end, createdDateBefore: (v[fkCreatedAt] as DateTimeRange?)?.end,
createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start, createdDateAfter: (v[fkCreatedAt] as DateTimeRange?)?.start,
correspondent: v[fkCorrespondent] as CorrespondentQuery?, correspondent: v[fkCorrespondent] as CorrespondentQuery? ??
documentType: v[fkDocumentType] as DocumentTypeQuery?, DocumentFilter.initial.correspondent,
storagePath: v[fkStoragePath] as StoragePathQuery?, documentType: v[fkDocumentType] as DocumentTypeQuery? ??
tags: v[DocumentModel.tagsKey] as TagsQuery?, DocumentFilter.initial.documentType,
page: 1, storagePath: v[fkStoragePath] as StoragePathQuery? ??
DocumentFilter.initial.storagePath,
tags: v[DocumentModel.tagsKey] as TagsQuery? ??
DocumentFilter.initial.tags,
queryText: v[fkQuery] as String?, queryText: v[fkQuery] as String?,
addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end, addedDateBefore: (v[fkAddedAt] as DateTimeRange?)?.end,
addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start, addedDateAfter: (v[fkAddedAt] as DateTimeRange?)?.start,
queryType: v[QueryTypeFormField.fkQueryType] as QueryType, queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
asnQuery: widget.initialFilter.asnQuery,
page: 1,
pageSize: widget.initialFilter.pageSize,
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
); );
try { try {
await BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: newFilter);
BlocProvider.of<SavedViewCubit>(context).resetSelection();
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
widget.panelController.close(); Navigator.pop(context, newFilter);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.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/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget { class SortFieldSelectionBottomSheet extends StatefulWidget {
@@ -46,30 +49,58 @@ class _SortFieldSelectionBottomSheetState
S.of(context).documentsPageOrderByLabel, S.of(context).documentsPageOrderByLabel,
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
textAlign: TextAlign.start, textAlign: TextAlign.start,
).padded(
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
), ),
TextButton( TextButton(
child: Text(S.of(context).documentsFilterPageApplyFilterLabel), child: Text(S.of(context).documentsFilterPageApplyFilterLabel),
onPressed: () => widget.onSubmit( onPressed: () {
_currentSortField, widget.onSubmit(
_currentSortOrder, _currentSortField,
), _currentSortOrder,
);
Navigator.pop(context);
},
), ),
], ],
), ).paddedSymmetrically(horizontal: 16, vertical: 8.0),
Column( Column(
children: SortField.values.map(_buildSortOption).toList(), children: [
_buildSortOption(SortField.archiveSerialNumber),
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return _buildSortOption(
SortField.correspondentName,
enabled: state.labels.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0),
);
},
),
_buildSortOption(SortField.title),
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return _buildSortOption(
SortField.documentType,
enabled: state.labels.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0),
);
},
),
_buildSortOption(SortField.created),
_buildSortOption(SortField.added),
_buildSortOption(SortField.modified),
],
), ),
], ],
), ),
); );
} }
Widget _buildSortOption( Widget _buildSortOption(SortField field, {bool enabled = true}) {
SortField field,
) {
return ListTile( return ListTile(
enabled: enabled,
contentPadding: const EdgeInsets.symmetric(horizontal: 32), contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text( title: Text(
_localizedSortField(field), _localizedSortField(field),
@@ -77,6 +108,14 @@ class _SortFieldSelectionBottomSheetState
trailing: _currentSortField == field trailing: _currentSortField == field
? _buildOrderIcon(_currentSortOrder) ? _buildOrderIcon(_currentSortOrder)
: null, : null,
onTap: () {
setState(() {
_currentSortOrder = (_currentSortField == field
? _currentSortOrder.toggle()
: SortOrder.descending);
_currentSortField = field;
});
},
); );
} }

View File

@@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.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/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -35,7 +33,7 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
snap: true, snap: true,
floating: true, floating: true,
pinned: true, pinned: true,
flexibleSpace: _buildFlexibleArea(false), flexibleSpace: _buildFlexibleArea(false, documentsState.filter),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => onPressed: () =>
@@ -56,13 +54,12 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
snap: true, snap: true,
floating: true, floating: true,
pinned: true, pinned: true,
flexibleSpace: _buildFlexibleArea(true), flexibleSpace: _buildFlexibleArea(
title: BlocBuilder<DocumentsCubit, DocumentsState>( true,
builder: (context, state) { documentsState.filter,
return Text( ),
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})', title: Text(
); '${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})',
},
), ),
actions: [ actions: [
...widget.actions, ...widget.actions,
@@ -73,14 +70,18 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
); );
} }
Widget _buildFlexibleArea(bool enabled) { Widget _buildFlexibleArea(bool enabled, DocumentFilter filter) {
return FlexibleSpaceBar( return FlexibleSpaceBar(
background: Padding( background: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
SavedViewSelectionWidget(height: 48, enabled: enabled), SavedViewSelectionWidget(
height: 48,
enabled: enabled,
currentFilter: filter,
),
], ],
), ),
), ),

View File

@@ -1,27 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class SortDocumentsButton extends StatefulWidget { class SortDocumentsButton extends StatelessWidget {
const SortDocumentsButton({ const SortDocumentsButton({super.key});
Key? key,
}) : super(key: key);
@override
State<SortDocumentsButton> createState() => _SortDocumentsButtonState();
}
class _SortDocumentsButtonState extends State<SortDocumentsButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
icon: const Icon(Icons.sort), icon: const Icon(Icons.sort),
onPressed: _onOpenSortBottomSheet, onPressed: () => _onOpenSortBottomSheet(context),
); );
} }
void _onOpenSortBottomSheet() { void _onOpenSortBottomSheet(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
elevation: 2, elevation: 2,
context: context, context: context,
@@ -32,19 +28,41 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
), ),
builder: (context) => FractionallySizedBox( builder: (_) => BlocProvider.value(
heightFactor: .6, value: BlocProvider.of<DocumentsCubit>(context),
child: BlocBuilder<DocumentsCubit, DocumentsState>( child: FractionallySizedBox(
builder: (context, state) { heightFactor: .6,
return SortFieldSelectionBottomSheet( child: MultiBlocProvider(
initialSortField: state.filter.sortField, providers: [
initialSortOrder: state.filter.sortOrder, BlocProvider(
onSubmit: (field, order) => create: (context) => LabelCubit<DocumentType>(
BlocProvider.of<DocumentsCubit>(context).updateCurrentFilter( RepositoryProvider.of<LabelRepository<DocumentType>>(context),
(filter) => filter.copyWith(sortField: field, sortOrder: order), ),
), ),
); BlocProvider(
}, create: (context) => LabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(
context),
),
),
],
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return SortFieldSelectionBottomSheet(
initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) =>
BlocProvider.of<DocumentsCubit>(context)
.updateCurrentFilter(
(filter) => filter.copyWith(
sortField: field,
sortOrder: order,
),
),
);
},
),
),
), ),
), ),
); );

View File

@@ -0,0 +1,91 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:collection/collection.dart';
part 'edit_document_state.dart';
class EditDocumentCubit extends Cubit<EditDocumentState> {
final DocumentModel _initialDocument;
final PaperlessDocumentsApi _docsApi;
final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType> _documentTypeRepository;
final LabelRepository<StoragePath> _storagePathRepository;
final LabelRepository<Tag> _tagRepository;
final List<StreamSubscription> _subscriptions = [];
EditDocumentCubit(
DocumentModel document, {
required PaperlessDocumentsApi documentsApi,
required LabelRepository<Correspondent> correspondentRepository,
required LabelRepository<DocumentType> documentTypeRepository,
required LabelRepository<StoragePath> storagePathRepository,
required LabelRepository<Tag> tagRepository,
}) : _initialDocument = document,
_docsApi = documentsApi,
_correspondentRepository = correspondentRepository,
_documentTypeRepository = documentTypeRepository,
_storagePathRepository = storagePathRepository,
_tagRepository = tagRepository,
super(
EditDocumentState(
document: document,
correspondents: correspondentRepository.current,
documentTypes: documentTypeRepository.current,
storagePaths: storagePathRepository.current,
tags: tagRepository.current,
),
) {
_subscriptions.add(
_correspondentRepository.labels
.listen((v) => emit(state.copyWith(correspondents: v))),
);
_subscriptions.add(
_documentTypeRepository.labels
.listen((v) => emit(state.copyWith(documentTypes: v))),
);
_subscriptions.add(
_storagePathRepository.labels
.listen((v) => emit(state.copyWith(storagePaths: v))),
);
_subscriptions.add(
_tagRepository.labels.listen(
(v) => emit(state.copyWith(tags: v)),
),
);
}
Future<void> updateDocument(DocumentModel document) async {
final updated = await _docsApi.update(document);
// Reload changed labels (documentCount property changes with removal/add)
if (document.documentType != _initialDocument.documentType) {
_documentTypeRepository
.find((document.documentType ?? _initialDocument.documentType)!);
}
if (document.correspondent != _initialDocument.correspondent) {
_correspondentRepository
.find((document.correspondent ?? _initialDocument.correspondent)!);
}
if (document.storagePath != _initialDocument.storagePath) {
_storagePathRepository
.find((document.storagePath ?? _initialDocument.storagePath)!);
}
if (!const DeepCollectionEquality.unordered()
.equals(document.tags, _initialDocument.tags)) {
_tagRepository.findAll(document.tags);
}
emit(state.copyWith(document: updated));
}
@override
Future<void> close() {
for (final sub in _subscriptions) {
sub.cancel();
}
return super.close();
}
}

View File

@@ -0,0 +1,43 @@
part of 'edit_document_cubit.dart';
class EditDocumentState extends Equatable {
final DocumentModel document;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, StoragePath> storagePaths;
final Map<int, Tag> tags;
const EditDocumentState({
required this.correspondents,
required this.documentTypes,
required this.storagePaths,
required this.tags,
required this.document,
});
@override
List<Object> get props => [
correspondents,
documentTypes,
storagePaths,
tags,
document,
];
EditDocumentState copyWith({
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, StoragePath>? storagePaths,
Map<int, Tag>? tags,
DocumentModel? document,
}) {
return EditDocumentState(
document: document ?? this.document,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
storagePaths: storagePaths ?? this.storagePaths,
tags: tags ?? this.tags,
);
}
}

View File

@@ -17,9 +17,9 @@ class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
.listen((labels) => emit(EditLabelState(labels: labels))); .listen((labels) => emit(EditLabelState(labels: labels)));
} }
Future<void> create(T label) => _repository.create(label); Future<T> create(T label) => _repository.create(label);
Future<void> update(T label) => _repository.update(label); Future<T> update(T label) => _repository.update(label);
Future<void> delete(T label) => _repository.delete(label); Future<void> delete(T label) => _repository.delete(label);

View File

@@ -28,7 +28,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
), ),
child: AddLabelFormWidget( child: AddLabelFormWidget(
pageTitle: pageTitle, pageTitle: pageTitle,
label: fromJsonT({'name': initialName}), label: initialName != null ? fromJsonT({'name': initialName}) : null,
additionalFields: additionalFields, additionalFields: additionalFields,
fromJsonT: fromJsonT, fromJsonT: fromJsonT,
), ),

View File

@@ -70,33 +70,38 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
); );
} }
void _onDelete(BuildContext context) { void _onDelete(BuildContext context) async {
if ((label.documentCount ?? 0) > 0) { if ((label.documentCount ?? 0) > 0) {
showDialog( final shouldDelete = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(S.of(context).editLabelPageConfirmDeletionDialogTitle), title:
content: Text( Text(S.of(context).editLabelPageConfirmDeletionDialogTitle),
S.of(context).editLabelPageDeletionDialogText, content: Text(
), S.of(context).editLabelPageDeletionDialogText,
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(S.of(context).genericActionCancelLabel),
),
TextButton(
onPressed: () {
BlocProvider.of<EditLabelCubit<T>>(context).delete(label);
Navigator.pop(context);
},
child: Text(
S.of(context).genericActionDeleteLabel,
style: TextStyle(color: Theme.of(context).errorColor),
), ),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(S.of(context).genericActionCancelLabel),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: Text(
S.of(context).genericActionDeleteLabel,
style: TextStyle(color: Theme.of(context).errorColor),
),
),
],
), ),
], ) ??
), false;
); if (shouldDelete) {
BlocProvider.of<EditLabelCubit<T>>(context).delete(label);
Navigator.pop(context);
}
} else { } else {
BlocProvider.of<EditLabelCubit<T>>(context).delete(label); BlocProvider.of<EditLabelCubit<T>>(context).delete(label);
Navigator.pop(context); Navigator.pop(context);

View File

@@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class EditDocumentTypePage extends StatelessWidget { class EditDocumentTypePage extends StatelessWidget {
final DocumentType documentType; final DocumentType documentType;
@@ -12,7 +12,7 @@ class EditDocumentTypePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<DocumentType>( create: (context) => EditLabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context), RepositoryProvider.of<LabelRepository<DocumentType>>(context),
), ),
child: EditLabelPage<DocumentType>( child: EditLabelPage<DocumentType>(

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
class EditStoragePathPage extends StatelessWidget { class EditStoragePathPage extends StatelessWidget {
@@ -13,7 +13,7 @@ class EditStoragePathPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<StoragePath>( create: (context) => EditLabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context), RepositoryProvider.of<LabelRepository<StoragePath>>(context),
), ),
child: EditLabelPage<StoragePath>( child: EditLabelPage<StoragePath>(

View File

@@ -4,8 +4,8 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart'; import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart'; import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class EditTagPage extends StatelessWidget { class EditTagPage extends StatelessWidget {
@@ -16,7 +16,7 @@ class EditTagPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => LabelCubit<Tag>( create: (context) => EditLabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context), RepositoryProvider.of<LabelRepository<Tag>>(context),
), ),
child: EditLabelPage<Tag>( child: EditLabelPage<Tag>(

View File

@@ -10,7 +10,7 @@ import 'package:paperless_mobile/util.dart';
class SubmitButtonConfig<T extends Label> { class SubmitButtonConfig<T extends Label> {
final Widget icon; final Widget icon;
final Widget label; final Widget label;
final Future<void> Function(T) onSubmit; final Future<T> Function(T) onSubmit;
SubmitButtonConfig({ SubmitButtonConfig({
required this.icon, required this.icon,
@@ -117,8 +117,9 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
...widget.initialValue?.toJson() ?? {}, ...widget.initialValue?.toJson() ?? {},
..._formKey.currentState!.value ..._formKey.currentState!.value
}; };
await widget.submitButtonConfig.onSubmit(widget.fromJsonT(mergedJson)); final createdLabel = await widget.submitButtonConfig
Navigator.pop(context); .onSubmit(widget.fromJsonT(mergedJson));
Navigator.pop(context, createdLabel);
} on PaperlessValidationErrors catch (errorMessages) { } on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = errorMessages); setState(() => _errors = errorMessages);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {

View File

@@ -59,6 +59,11 @@ class _HomePageState extends State<HomePage> {
BlocProvider.value( BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()), value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
), ),
BlocProvider(
create: (context) => SavedViewCubit(
RepositoryProvider.of<SavedViewRepository>(context),
),
),
], ],
child: const DocumentsPage(), child: const DocumentsPage(),
), ),

View File

@@ -8,11 +8,9 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro
import 'package:paperless_mobile/core/repository/saved_view_repository.dart'; import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/settings_page.dart'; import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -52,7 +50,7 @@ class InfoDrawer extends StatelessWidget {
height: 32, height: 32,
width: 32, width: 32,
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context).colorScheme.onPrimaryContainer,
).padded(const EdgeInsets.only(right: 8.0)), ).paddedOnly(right: 8.0),
Text( Text(
S.of(context).appTitleText, S.of(context).appTitleText,
style: Theme.of(context).textTheme.headline5?.copyWith( style: Theme.of(context).textTheme.headline5?.copyWith(
@@ -215,7 +213,7 @@ class InfoDrawer extends StatelessWidget {
create: (context) => InboxCubit( create: (context) => InboxCubit(
RepositoryProvider.of<LabelRepository<Tag>>(context), RepositoryProvider.of<LabelRepository<Tag>>(context),
getIt<PaperlessDocumentsApi>(), getIt<PaperlessDocumentsApi>(),
), )..loadInbox(),
child: const InboxPage(), child: const InboxPage(),
), ),
), ),

View File

@@ -54,12 +54,12 @@ class _InboxPageState extends State<InboxPage> {
'${state.inboxItems.length} ${S.of(context).inboxPageUnseenText}', '${state.inboxItems.length} ${S.of(context).inboxPageUnseenText}',
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
).padded(const EdgeInsets.symmetric(horizontal: 4.0)), ).paddedSymmetrically(horizontal: 4.0),
), ),
), ),
); );
}, },
).padded(const EdgeInsets.symmetric(horizontal: 8.0)), ).paddedSymmetrically(horizontal: 8.0),
), ),
), ),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>( floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
@@ -108,7 +108,7 @@ class _InboxPageState extends State<InboxPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
).padded(), ).padded(),
), ),
).padded(const EdgeInsets.only(top: 8.0)), ).paddedOnly(top: 8.0),
), ),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
@@ -137,14 +137,7 @@ class _InboxPageState extends State<InboxPage> {
S.of(context).inboxPageUsageHintText, S.of(context).inboxPageUsageHintText,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
).padded( ).padded(),
const EdgeInsets.only(
top: 8.0,
left: 8.0,
right: 8.0,
bottom: 8.0,
),
),
), ),
...slivers ...slivers
], ],

View File

@@ -11,9 +11,13 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
late StreamSubscription _subscription; late StreamSubscription _subscription;
LabelCubit(this._repository) : super(LabelState.initial()) { LabelCubit(LabelRepository<T> repository)
: _repository = repository,
super(LabelState(labels: repository.current, isLoaded: true)) {
_subscription = _repository.labels.listen( _subscription = _repository.labels.listen(
(update) => emit(LabelState(isLoaded: true, labels: update)), (update) => emit(
LabelState(isLoaded: true, labels: update),
),
); );
} }
@@ -28,6 +32,10 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
return addedItem; return addedItem;
} }
Future<void> reload() {
return _repository.findAll();
}
Future<T> replace(T item) async { Future<T> replace(T item) async {
assert(item.id != null); assert(item.id != null);
final updatedItem = await _repository.update(item); final updatedItem = await _repository.update(item);

View File

@@ -4,11 +4,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_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/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class TagFormField extends StatefulWidget { class TagFormField extends StatefulWidget {
@@ -18,6 +14,7 @@ class TagFormField extends StatefulWidget {
final bool notAssignedSelectable; final bool notAssignedSelectable;
final bool anyAssignedSelectable; final bool anyAssignedSelectable;
final bool excludeAllowed; final bool excludeAllowed;
final Map<int, Tag> selectableOptions;
const TagFormField({ const TagFormField({
super.key, super.key,
@@ -27,6 +24,7 @@ class TagFormField extends StatefulWidget {
this.notAssignedSelectable = true, this.notAssignedSelectable = true,
this.anyAssignedSelectable = true, this.anyAssignedSelectable = true,
this.excludeAllowed = true, this.excludeAllowed = true,
required this.selectableOptions,
}); });
@override @override
@@ -47,10 +45,7 @@ class _TagFormFieldState extends State<TagFormField> {
_textEditingController = TextEditingController() _textEditingController = TextEditingController()
..addListener(() { ..addListener(() {
setState(() { setState(() {
_showCreationSuffixIcon = BlocProvider.of<LabelCubit<Tag>>(context) _showCreationSuffixIcon = widget.selectableOptions.values
.state
.labels
.values
.where( .where(
(item) => item.name.toLowerCase().startsWith( (item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(), _textEditingController.text.toLowerCase(),
@@ -66,122 +61,126 @@ class _TagFormFieldState extends State<TagFormField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TagBlocProvider( final isEnabled = widget.selectableOptions.values.fold<bool>(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>( false,
builder: (context, tagState) { (previousValue, element) =>
return FormBuilderField<TagsQuery>( previousValue || (element.documentCount ?? 0) > 0) ||
builder: (field) { widget.allowCreation;
return Column(
crossAxisAlignment: CrossAxisAlignment.start, return FormBuilderField<TagsQuery>(
children: [ enabled: isEnabled,
TypeAheadField<int>( builder: (field) {
textFieldConfiguration: TextFieldConfiguration( return Column(
decoration: InputDecoration( crossAxisAlignment: CrossAxisAlignment.start,
prefixIcon: const Icon( children: [
Icons.label_outline, TypeAheadField<int>(
), textFieldConfiguration: TextFieldConfiguration(
suffixIcon: _buildSuffixIcon(context, field), enabled: isEnabled,
labelText: S.of(context).documentTagsPropertyLabel, decoration: InputDecoration(
hintText: S.of(context).tagFormFieldSearchHintText, prefixIcon: const Icon(
), Icons.label_outline,
controller: _textEditingController,
),
suggestionsCallback: (query) {
final suggestions = tagState.labels.values
.where((element) => element.name
.toLowerCase()
.startsWith(query.toLowerCase()))
.map((e) => e.id!)
.toList();
if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery)
.ids
.contains(element));
}
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
suggestions.insert(0, _onlyNotAssignedId);
}
if (widget.anyAssignedSelectable &&
field.value is! AnyAssignedTagsQuery) {
suggestions.insert(0, _anyAssignedId);
}
return suggestions;
},
getImmediateSuggestions: true,
animationStart: 1,
itemBuilder: (context, data) {
if (data == _onlyNotAssignedId) {
return ListTile(
title: Text(S.of(context).labelNotAssignedText),
);
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = tagState.getLabel(data)!;
return ListTile(
leading: Icon(
Icons.circle,
color: tag.color,
),
title: Text(
tag.name,
style: TextStyle(
color:
Theme.of(context).colorScheme.onBackground),
),
);
},
onSuggestionSelected: (id) {
if (id == _onlyNotAssignedId) {
//Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(tagsQuery
.withIdQueriesAdded([IncludeTagIdQuery(id)]));
}
_textEditingController.clear();
},
direction: AxisDirection.up,
), ),
if (field.value is OnlyNotAssignedTagsQuery) ...[ suffixIcon: _buildSuffixIcon(context, field),
_buildNotAssignedTag(field) labelText: S.of(context).documentTagsPropertyLabel,
] else if (field.value is AnyAssignedTagsQuery) ...[ hintText: S.of(context).tagFormFieldSearchHintText,
_buildAnyAssignedTag(field) ),
] else ...[ controller: _textEditingController,
// field.value is IdsTagsQuery ),
Wrap( suggestionsCallback: (query) {
alignment: WrapAlignment.start, final suggestions = widget.selectableOptions.entries
runAlignment: WrapAlignment.start, .where(
spacing: 8.0, (entry) => entry.value.name
children: ((field.value as IdsTagsQuery).queries) .toLowerCase()
.map( .startsWith(query.toLowerCase()),
(query) => _buildTag( )
field, .where((entry) =>
query, widget.allowCreation ||
tagState.getLabel(query.id), (entry.value.documentCount ?? 0) > 0)
), .map((entry) => entry.key)
) .toList();
.toList(), if (field.value is IdsTagsQuery) {
), suggestions.removeWhere((element) =>
] (field.value as IdsTagsQuery).ids.contains(element));
], }
); if (widget.notAssignedSelectable &&
}, field.value is! OnlyNotAssignedTagsQuery) {
initialValue: widget.initialValue ?? const IdsTagsQuery(), suggestions.insert(0, _onlyNotAssignedId);
name: widget.name, }
); if (widget.anyAssignedSelectable &&
}, field.value is! AnyAssignedTagsQuery) {
), suggestions.insert(0, _anyAssignedId);
}
return suggestions;
},
getImmediateSuggestions: true,
animationStart: 1,
itemBuilder: (context, data) {
if (data == _onlyNotAssignedId) {
return ListTile(
title: Text(S.of(context).labelNotAssignedText),
);
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = widget.selectableOptions[data]!;
return ListTile(
leading: Icon(
Icons.circle,
color: tag.color,
),
title: Text(
tag.name,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground),
),
);
},
onSuggestionSelected: (id) {
if (id == _onlyNotAssignedId) {
//Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(
tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(id)]));
}
_textEditingController.clear();
},
direction: AxisDirection.up,
),
if (field.value is OnlyNotAssignedTagsQuery) ...[
_buildNotAssignedTag(field)
] else if (field.value is AnyAssignedTagsQuery) ...[
_buildAnyAssignedTag(field)
] else ...[
// field.value is IdsTagsQuery
Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
spacing: 8.0,
children: ((field.value as IdsTagsQuery).queries)
.map(
(query) => _buildTag(
field,
query,
widget.selectableOptions[query.id],
),
)
.toList(),
),
]
],
);
},
initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name,
); );
} }

View File

@@ -30,6 +30,7 @@ class _LabelsPageState extends State<LabelsPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: 4, vsync: this) _tabController = TabController(length: 4, vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index)); ..addListener(() => setState(() => _currentIndex = _tabController.index));
} }

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
@@ -55,13 +57,15 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
super.initState(); super.initState();
_showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id); _showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id);
_textEditingController = TextEditingController( _textEditingController = TextEditingController(
text: widget.state[widget.initialValue?.id]?.name ?? '') text: widget.state[widget.initialValue?.id]?.name ?? '',
..addListener(() { )..addListener(() {
setState(() { setState(() {
_showCreationSuffixIcon = widget.state.values _showCreationSuffixIcon = widget.state.values
.where((item) => item.name.toLowerCase().startsWith( .where(
_textEditingController.text.toLowerCase(), (item) => item.name.toLowerCase().startsWith(
)) _textEditingController.text.toLowerCase(),
),
)
.isEmpty; .isEmpty;
}); });
setState(() => setState(() =>
@@ -71,7 +75,13 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEnabled = widget.state.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0) ||
widget.labelCreationWidgetBuilder != null;
return FormBuilderTypeAhead<IdQueryParameter>( return FormBuilderTypeAhead<IdQueryParameter>(
enabled: isEnabled,
noItemsFoundBuilder: (context) => Padding( noItemsFoundBuilder: (context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text( child: Text(
@@ -88,13 +98,20 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
S.of(context).labelNotAssignedText), S.of(context).labelNotAssignedText),
), ),
suggestionsCallback: (pattern) { suggestionsCallback: (pattern) {
final List<IdQueryParameter> suggestions = widget.state.keys final List<IdQueryParameter> suggestions = widget.state.entries
.where((item) => .where(
widget.state[item]!.name (entry) =>
.toLowerCase() widget.state[entry.key]!.name
.startsWith(pattern.toLowerCase()) || .toLowerCase()
pattern.isEmpty) .contains(pattern.toLowerCase()) ||
.map((id) => widget.queryParameterIdBuilder(id)) pattern.isEmpty,
)
.where(
(entry) =>
widget.labelCreationWidgetBuilder != null ||
(entry.value.documentCount ?? 0) > 0,
)
.map((entry) => widget.queryParameterIdBuilder(entry.key))
.toList(); .toList();
if (widget.notAssignedSelectable) { if (widget.notAssignedSelectable) {
suggestions.insert(0, widget.queryParameterNotAssignedBuilder()); suggestions.insert(0, widget.queryParameterNotAssignedBuilder());
@@ -128,21 +145,23 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
Widget? _buildSuffixIcon(BuildContext context) { Widget? _buildSuffixIcon(BuildContext context) {
if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) { if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) {
return IconButton( return IconButton(
onPressed: () => Navigator.of(context) onPressed: () async {
.push<T>(MaterialPageRoute( FocusScope.of(context).unfocus();
builder: (context) => widget final createdLabel = await showDialog(
.labelCreationWidgetBuilder!(_textEditingController.text))) context: context,
.then((value) { builder: (context) => widget.labelCreationWidgetBuilder!(
if (value != null) { _textEditingController.text,
),
);
if (createdLabel != null) {
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done). // If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done).
widget.formBuilderState?.fields[widget.name] widget.formBuilderState?.fields[widget.name]
?.didChange(widget.queryParameterIdBuilder(value.id)); ?.didChange(widget.queryParameterIdBuilder(createdLabel.id));
_textEditingController.text = value.name; _textEditingController.text = createdLabel.name;
FocusScope.of(context).unfocus();
} else { } else {
_reset(); _reset();
} }
}), },
icon: const Icon( icon: const Icon(
Icons.new_label, Icons.new_label,
), ),
@@ -158,8 +177,9 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
} }
void _reset() { void _reset() {
widget.formBuilderState?.fields[widget.name] widget.formBuilderState?.fields[widget.name]?.didChange(
?.didChange(widget.queryParameterIdBuilder(null)); widget.queryParameterIdBuilder(null), // equivalnt to IdQueryParam.unset()
);
_textEditingController.clear(); _textEditingController.clear();
} }
@@ -169,9 +189,7 @@ class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
} else if (T == DocumentType) { } else if (T == DocumentType) {
return S.of(context).documentTypeFormFieldSearchHintText; return S.of(context).documentTypeFormFieldSearchHintText;
} else { } else {
return S return S.of(context).tagFormFieldSearchHintText;
.of(context)
.tagFormFieldSearchHintText; //TODO: Update tag form field once there is multi selection support.
} }
} }
} }

View File

@@ -62,18 +62,21 @@ class LabelTabView<T extends Label> extends StatelessWidget {
), ),
); );
} }
return ListView( return RefreshIndicator(
children: labels onRefresh: BlocProvider.of<LabelCubit<T>>(context).reload,
.map((l) => LabelItem<T>( child: ListView(
name: l.name, children: labels
content: .map((l) => LabelItem<T>(
contentBuilder?.call(l) ?? Text(l.match ?? '-'), name: l.name,
onOpenEditPage: onEdit, content:
filterBuilder: filterBuilder, contentBuilder?.call(l) ?? Text(l.match ?? '-'),
leading: leadingBuilder?.call(l), onOpenEditPage: onEdit,
label: l, filterBuilder: filterBuilder,
)) leading: leadingBuilder?.call(l),
.toList(), label: l,
))
.toList(),
),
); );
}, },
); );

View File

@@ -81,7 +81,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
appSettings = ApplicationSettingsState.defaultSettings; appSettings = ApplicationSettingsState.defaultSettings;
} }
if (storedAuth == null || !storedAuth.isValid) { if (storedAuth == null || !storedAuth.isValid) {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false)); return emit(
AuthenticationState(isAuthenticated: false, wasLoginStored: false));
} else { } else {
if (appSettings.isLocalAuthenticationEnabled) { if (appSettings.isLocalAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService final localAuthSuccess = await _localAuthService
@@ -103,8 +104,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
wasLocalAuthenticationSuccessful: false, wasLocalAuthenticationSuccessful: false,
)); ));
} }
} else {
return emit(AuthenticationState(
isAuthenticated: true,
authentication: storedAuth,
wasLoginStored: true,
));
} }
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
} }
} }

View File

@@ -1,20 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
class SavedViewSelectionWidget extends StatelessWidget { class SavedViewSelectionWidget extends StatelessWidget {
final DocumentFilter currentFilter;
const SavedViewSelectionWidget({ const SavedViewSelectionWidget({
Key? key, Key? key,
required this.height, required this.height,
required this.enabled, required this.enabled,
required this.currentFilter,
}) : super(key: key); }) : super(key: key);
final double height; final double height;
@@ -64,10 +65,18 @@ class SavedViewSelectionWidget extends StatelessWidget {
S.of(context).savedViewsLabel, S.of(context).savedViewsLabel,
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
TextButton.icon( BlocBuilder<DocumentsCubit, DocumentsState>(
icon: const Icon(Icons.add), buildWhen: (previous, current) =>
onPressed: enabled ? () => _onCreatePressed(context) : null, previous.filter != current.filter,
label: Text(S.of(context).savedViewCreateNewLabel), builder: (context, docState) {
return TextButton.icon(
icon: const Icon(Icons.add),
onPressed: enabled
? () => _onCreatePressed(context, docState.filter)
: null,
label: Text(S.of(context).savedViewCreateNewLabel),
);
},
), ),
], ],
), ),
@@ -75,11 +84,11 @@ class SavedViewSelectionWidget extends StatelessWidget {
); );
} }
void _onCreatePressed(BuildContext context) async { void _onCreatePressed(BuildContext context, DocumentFilter filter) async {
final newView = await Navigator.of(context).push<SavedView?>( final newView = await Navigator.of(context).push<SavedView?>(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddSavedViewPage( builder: (context) => AddSavedViewPage(
currentFilter: getIt<DocumentsCubit>().state.filter, currentFilter: filter,
), ),
), ),
); );

View File

@@ -107,6 +107,7 @@ class _ScannerPageState extends State<ScannerPage>
if (kDebugMode) { if (kDebugMode) {
dev.log('[ScannerPage] Created temporary file: ${file.path}'); dev.log('[ScannerPage] Created temporary file: ${file.path}');
} }
final success = await EdgeDetection.detectEdge(file.path); final success = await EdgeDetection.detectEdge(file.path);
if (!success) { if (!success) {
if (kDebugMode) { if (kDebugMode) {

View File

@@ -120,6 +120,10 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
), ),
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(), border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
), ),
chipTheme: ChipThemeData( chipTheme: ChipThemeData(
backgroundColor: Colors.lightGreen[50], backgroundColor: Colors.lightGreen[50],
@@ -134,8 +138,11 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
scrolledUnderElevation: 0.0, scrolledUnderElevation: 0.0,
), ),
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(), border: OutlineInputBorder(),
), contentPadding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
)),
chipTheme: ChipThemeData( chipTheme: ChipThemeData(
backgroundColor: Colors.green[900], backgroundColor: Colors.green[900],
), ),

View File

@@ -25,7 +25,7 @@ class DocumentFilter extends Equatable {
final DocumentTypeQuery documentType; final DocumentTypeQuery documentType;
final CorrespondentQuery correspondent; final CorrespondentQuery correspondent;
final StoragePathQuery storagePath; final StoragePathQuery storagePath;
final AsnQuery asn; final AsnQuery asnQuery;
final TagsQuery tags; final TagsQuery tags;
final SortField sortField; final SortField sortField;
final SortOrder sortOrder; final SortOrder sortOrder;
@@ -42,7 +42,7 @@ class DocumentFilter extends Equatable {
this.documentType = const DocumentTypeQuery.unset(), this.documentType = const DocumentTypeQuery.unset(),
this.correspondent = const CorrespondentQuery.unset(), this.correspondent = const CorrespondentQuery.unset(),
this.storagePath = const StoragePathQuery.unset(), this.storagePath = const StoragePathQuery.unset(),
this.asn = const AsnQuery.unset(), this.asnQuery = const AsnQuery.unset(),
this.tags = const IdsTagsQuery(), this.tags = const IdsTagsQuery(),
this.sortField = SortField.created, this.sortField = SortField.created,
this.sortOrder = SortOrder.descending, this.sortOrder = SortOrder.descending,
@@ -60,7 +60,7 @@ class DocumentFilter extends Equatable {
sb.write(correspondent.toQueryParameter()); sb.write(correspondent.toQueryParameter());
sb.write(tags.toQueryParameter()); sb.write(tags.toQueryParameter());
sb.write(storagePath.toQueryParameter()); sb.write(storagePath.toQueryParameter());
sb.write(asn.toQueryParameter()); sb.write(asnQuery.toQueryParameter());
if (queryText?.isNotEmpty ?? false) { if (queryText?.isNotEmpty ?? false) {
sb.write("&${queryType.queryParam}=$queryText"); sb.write("&${queryType.queryParam}=$queryText");
@@ -104,6 +104,7 @@ class DocumentFilter extends Equatable {
DocumentTypeQuery? documentType, DocumentTypeQuery? documentType,
CorrespondentQuery? correspondent, CorrespondentQuery? correspondent,
StoragePathQuery? storagePath, StoragePathQuery? storagePath,
AsnQuery? asnQuery,
TagsQuery? tags, TagsQuery? tags,
SortField? sortField, SortField? sortField,
SortOrder? sortOrder, SortOrder? sortOrder,
@@ -129,6 +130,7 @@ class DocumentFilter extends Equatable {
queryText: queryText ?? this.queryText, queryText: queryText ?? this.queryText,
createdDateBefore: createdDateBefore ?? this.createdDateBefore, createdDateBefore: createdDateBefore ?? this.createdDateBefore,
createdDateAfter: createdDateAfter ?? this.createdDateAfter, createdDateAfter: createdDateAfter ?? this.createdDateAfter,
asnQuery: asnQuery ?? this.asnQuery,
); );
} }
@@ -153,6 +155,19 @@ class DocumentFilter extends Equatable {
return null; return null;
} }
int get appliedFiltersCount => [
documentType != initial.documentType,
correspondent != initial.correspondent,
storagePath != initial.storagePath,
tags != initial.tags,
(addedDateAfter != initial.addedDateAfter ||
addedDateBefore != initial.addedDateBefore),
(createdDateAfter != initial.createdDateAfter ||
createdDateBefore != initial.createdDateBefore),
asnQuery != initial.asnQuery,
(queryType != initial.queryType || queryText != initial.queryText),
].fold(0, (previousValue, element) => previousValue += element ? 1 : 0);
@override @override
List<Object?> get props => [ List<Object?> get props => [
pageSize, pageSize,
@@ -160,7 +175,7 @@ class DocumentFilter extends Equatable {
documentType, documentType,
correspondent, correspondent,
storagePath, storagePath,
asn, asnQuery,
tags, tags,
sortField, sortField,
sortOrder, sortOrder,

View File

@@ -51,9 +51,9 @@ class DocumentModel extends Equatable {
: id = json[idKey], : id = json[idKey],
title = json[titleKey], title = json[titleKey],
content = json[contentKey], content = json[contentKey],
created = DateTime.parse(json[createdKey]), created = DateTime.parse(json[createdKey]).toLocal(),
modified = DateTime.parse(json[modifiedKey]), modified = DateTime.parse(json[modifiedKey]).toLocal(),
added = DateTime.parse(json[addedKey]), added = DateTime.parse(json[addedKey]).toLocal(),
archiveSerialNumber = json[asnKey], archiveSerialNumber = json[asnKey],
originalFileName = json[originalFileNameKey], originalFileName = json[originalFileNameKey],
archivedFileName = json[archivedFileNameKey], archivedFileName = json[archivedFileNameKey],
@@ -71,9 +71,9 @@ class DocumentModel extends Equatable {
contentKey: content, contentKey: content,
correspondentKey: correspondent, correspondentKey: correspondent,
documentTypeKey: documentType, documentTypeKey: documentType,
createdKey: created.toUtc().toIso8601String(), createdKey: created.toIso8601String(),
modifiedKey: modified.toUtc().toIso8601String(), modifiedKey: modified.toIso8601String(),
addedKey: added.toUtc().toIso8601String(), addedKey: added.toIso8601String(),
originalFileNameKey: originalFileName, originalFileNameKey: originalFileName,
tagsKey: tags.toList(), tagsKey: tags.toList(),
storagePathKey: storagePath, storagePathKey: storagePath,

View File

@@ -187,7 +187,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
const DocumentFilter asnQueryFilter = DocumentFilter( const DocumentFilter asnQueryFilter = DocumentFilter(
sortField: SortField.archiveSerialNumber, sortField: SortField.archiveSerialNumber,
sortOrder: SortOrder.descending, sortOrder: SortOrder.descending,
asn: AsnQuery.anyAssigned(), asnQuery: AsnQuery.anyAssigned(),
page: 1, page: 1,
pageSize: 1, pageSize: 1,
); );

View File

@@ -35,7 +35,7 @@ packages:
name: asn1lib name: asn1lib
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.4.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -43,13 +43,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.9.0" version: "2.9.0"
badges:
dependency: "direct main"
description:
name: badges
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
barcode: barcode:
dependency: transitive dependency: transitive
description: description:
name: barcode name: barcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.2.3"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@@ -77,14 +84,14 @@ packages:
name: build name: build
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
name: build_config name: build_config
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.1.1"
build_daemon: build_daemon:
dependency: transitive dependency: transitive
description: description:
@@ -98,21 +105,21 @@ packages:
name: build_resolvers name: build_resolvers
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.9" version: "2.0.10"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.11" version: "2.3.0"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
name: build_runner_core name: build_runner_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.2.3" version: "7.2.7"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -126,28 +133,28 @@ packages:
name: built_value name: built_value
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.3.2" version: "8.4.2"
cached_network_image: cached_network_image:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.1" version: "3.2.3"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_platform_interface name: cached_network_image_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "2.0.0"
cached_network_image_web: cached_network_image_web:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_web name: cached_network_image_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -210,7 +217,7 @@ packages:
name: connectivity_plus_platform_interface name: connectivity_plus_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.1" version: "1.2.3"
connectivity_plus_web: connectivity_plus_web:
dependency: transitive dependency: transitive
description: description:
@@ -231,7 +238,7 @@ packages:
name: convert name: convert
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.1.1"
coverage: coverage:
dependency: transitive dependency: transitive
description: description:
@@ -266,7 +273,7 @@ packages:
name: dart_style name: dart_style
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.3" version: "2.2.4"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@@ -343,13 +350,13 @@ packages:
name: dropdown_search name: dropdown_search
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.3" version: "5.0.5"
edge_detection: edge_detection:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: master ref: master
resolved-ref: "19fbebef99360e9cf0b59c6a90ff7cd26d4d6e7d" resolved-ref: "2d417dd77e075cb12e82a390e50cc4554e877ec4"
url: "https://github.com/sawankumarbundelkhandi/edge_detection" url: "https://github.com/sawankumarbundelkhandi/edge_detection"
source: git source: git
version: "1.1.1" version: "1.1.1"
@@ -366,7 +373,7 @@ packages:
name: encrypted_shared_preferences name: encrypted_shared_preferences
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -474,7 +481,7 @@ packages:
name: flutter_form_builder name: flutter_form_builder
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.5.0" version: "7.7.0"
flutter_keyboard_visibility: flutter_keyboard_visibility:
dependency: transitive dependency: transitive
description: description:
@@ -535,14 +542,14 @@ packages:
name: flutter_native_splash name: flutter_native_splash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.11" version: "2.2.16"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.6" version: "2.0.7"
flutter_rating_bar: flutter_rating_bar:
dependency: transitive dependency: transitive
description: description:
@@ -556,7 +563,7 @@ packages:
name: flutter_svg name: flutter_svg
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1+1" version: "1.1.6"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -587,20 +594,20 @@ packages:
name: fluttertoast name: fluttertoast
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.1.1" version: "8.1.2"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: font_awesome_flutter name: font_awesome_flutter
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "10.1.0" version: "10.3.0"
form_builder_extra_fields: form_builder_extra_fields:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: main ref: main
resolved-ref: "33ba0a4407086275ac4357badc631be550fb3bcc" resolved-ref: b02de7dad9c00ece575ad4b8dfba73a3e1239e9c
url: "https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git" url: "https://github.com/flutter-form-builder-ecosystem/form_builder_extra_fields.git"
source: git source: git
version: "8.4.0" version: "8.4.0"
@@ -636,14 +643,14 @@ packages:
name: glob name: glob
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
name: graphs name: graphs
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.2.0"
hive: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -657,7 +664,7 @@ packages:
name: html name: html
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.15.0" version: "0.15.1"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -671,28 +678,28 @@ packages:
name: http_interceptor name: http_interceptor
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0-beta.5" version: "2.0.0-beta.6"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
name: http_multi_server name: http_multi_server
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.1" version: "4.0.2"
image: image:
dependency: "direct main" dependency: "direct main"
description: description:
name: image name: image
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.2.2"
infinite_scroll_pagination: infinite_scroll_pagination:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -713,7 +720,7 @@ packages:
name: injectable_generator name: injectable_generator
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
integration_test: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -739,7 +746,7 @@ packages:
name: introduction_screen name: introduction_screen
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.1.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -781,7 +788,7 @@ packages:
name: local_auth_android name: local_auth_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.13" version: "1.0.15"
local_auth_ios: local_auth_ios:
dependency: transitive dependency: transitive
description: description:
@@ -809,7 +816,7 @@ packages:
name: logging name: logging
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -837,7 +844,7 @@ packages:
name: mime name: mime
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.0.3"
mockito: mockito:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -886,7 +893,7 @@ packages:
name: package_config name: package_config
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
package_info_plus: package_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -921,7 +928,7 @@ packages:
name: package_info_plus_web name: package_info_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
package_info_plus_windows: package_info_plus_windows:
dependency: transitive dependency: transitive
description: description:
@@ -949,14 +956,14 @@ packages:
name: path_drawing name: path_drawing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
name: path_parsing name: path_parsing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.1"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -970,14 +977,14 @@ packages:
name: path_provider_android name: path_provider_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.14" version: "2.0.22"
path_provider_ios: path_provider_ios:
dependency: transitive dependency: transitive
description: description:
name: path_provider_ios name: path_provider_ios
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.9" version: "2.0.11"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -998,7 +1005,7 @@ packages:
name: path_provider_platform_interface name: path_provider_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.4" version: "2.0.5"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1012,7 +1019,7 @@ packages:
name: pdf name: pdf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.8.1" version: "3.8.4"
pdfx: pdfx:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1047,28 +1054,28 @@ packages:
name: permission_handler_apple name: permission_handler_apple
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.0.4" version: "9.0.7"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.7.0" version: "3.9.0"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_windows name: permission_handler_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.0" version: "0.1.2"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.0" version: "5.1.0"
photo_view: photo_view:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1089,21 +1096,21 @@ packages:
name: plugin_platform_interface name: plugin_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.3"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
name: pointycastle name: pointycastle
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.6.0" version: "3.6.2"
pool: pool:
dependency: transitive dependency: transitive
description: description:
name: pool name: pool
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" version: "1.5.1"
process: process:
dependency: transitive dependency: transitive
description: description:
@@ -1117,21 +1124,21 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.3" version: "6.0.4"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
name: pub_semver name: pub_semver
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.3"
pubspec_parse: pubspec_parse:
dependency: transitive dependency: transitive
description: description:
name: pubspec_parse name: pubspec_parse
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
qr: qr:
dependency: transitive dependency: transitive
description: description:
@@ -1139,6 +1146,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
recase:
dependency: transitive
description:
name: recase
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
receive_sharing_intent: receive_sharing_intent:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1159,7 +1173,7 @@ packages:
name: share_plus name: share_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.2.0" version: "6.3.0"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1180,7 +1194,7 @@ packages:
name: shared_preferences_android name: shared_preferences_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.12" version: "2.0.14"
shared_preferences_ios: shared_preferences_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1208,7 +1222,7 @@ packages:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.1.0"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
@@ -1229,7 +1243,7 @@ packages:
name: shelf name: shelf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
shelf_packages_handler: shelf_packages_handler:
dependency: transitive dependency: transitive
description: description:
@@ -1250,7 +1264,7 @@ packages:
name: shelf_web_socket name: shelf_web_socket
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.3"
shimmer: shimmer:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1264,47 +1278,40 @@ packages:
name: signature name: signature
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.2.1" version: "5.3.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.99"
sliding_up_panel:
dependency: "direct main"
description:
name: sliding_up_panel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0+1"
sliver_tools: sliver_tools:
dependency: transitive dependency: transitive
description: description:
name: sliver_tools name: sliver_tools
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.7" version: "0.2.8"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.2" version: "1.2.6"
source_map_stack_trace: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
name: source_map_stack_trace name: source_map_stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.1"
source_maps: source_maps:
dependency: transitive dependency: transitive
description: description:
name: source_maps name: source_maps
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.10.10" version: "0.10.11"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -1318,14 +1325,14 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2+1" version: "2.2.2"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1+1" version: "2.4.0+2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1346,7 +1353,7 @@ packages:
name: stream_transform name: stream_transform
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.1.0"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@@ -1367,7 +1374,7 @@ packages:
name: synchronized name: synchronized
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0+2" version: "3.0.0+3"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -1430,14 +1437,14 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.2" version: "6.1.7"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.17" version: "6.0.22"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1486,7 +1493,7 @@ packages:
name: uuid name: uuid
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.6" version: "3.0.7"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -1507,7 +1514,7 @@ packages:
name: watcher name: watcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
web_socket_channel: web_socket_channel:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1535,14 +1542,14 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1" version: "3.1.2"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0+1" version: "0.2.0+2"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@@ -1559,4 +1566,4 @@ packages:
version: "3.1.1" version: "3.1.1"
sdks: sdks:
dart: ">=2.18.5 <3.0.0" dart: ">=2.18.5 <3.0.0"
flutter: ">=3.0.0" flutter: ">=3.3.0"

View File

@@ -64,7 +64,6 @@ dependencies:
ref: main ref: main
form_builder_validators: ^8.4.0 form_builder_validators: ^8.4.0
infinite_scroll_pagination: ^3.2.0 infinite_scroll_pagination: ^3.2.0
sliding_up_panel: ^2.0.0+1
package_info_plus: ^1.4.3+1 package_info_plus: ^1.4.3+1
font_awesome_flutter: ^10.1.0 font_awesome_flutter: ^10.1.0
local_auth: ^2.1.2 local_auth: ^2.1.2
@@ -82,6 +81,7 @@ dependencies:
path: packages/paperless_api path: packages/paperless_api
hive: ^2.2.3 hive: ^2.2.3
rxdart: ^0.27.7 rxdart: ^0.27.7
badges: ^2.0.3
dev_dependencies: dev_dependencies:
integration_test: integration_test:

View File

@@ -1,7 +1,6 @@
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart'; import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';