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(
{required BaseResponse response}) async =>
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(
{required BaseResponse response}) async =>
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(
{required BaseResponse response}) async =>
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/http.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
const interceptedRoutes = ['thumb/'];
@@ -33,4 +32,10 @@ class ResponseConversionInterceptor implements InterceptorContract {
}
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() {
_subject.add(const {});
}
@override
Map<int, Correspondent> get current => _subject.value;
}

View File

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

View File

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

View File

@@ -62,4 +62,7 @@ class TagRepositoryImpl implements LabelRepository<Tag> {
void clear() {
_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> {
Stream<Map<int, T>> get labels;
Map<int, T> get current;
Future<T> create(T label);
Future<T?> find(int id);
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(
'Stack Trace',
style: Theme.of(context).textTheme.subtitle1,
).padded(
const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0)),
).paddedOnly(top: 8.0, left: 8.0, right: 8.0),
TextButton.icon(
label: const Text('Copy'),
icon: const Icon(Icons.copy),

View File

@@ -3,13 +3,12 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.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/base_url_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/language_header.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_interceptor/http/http.dart';
import 'package:injectable/injectable.dart';
import 'package:local_auth/local_auth.dart';

View File

@@ -1,9 +1,32 @@
import 'package:flutter/widgets.dart';
extension WidgetPadding on Widget {
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(8)]) {
Widget padded([double all = 8.0]) {
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,
);
}

View File

@@ -12,18 +12,18 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<void> delete(DocumentModel document) async {
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 {
if (document.archiveSerialNumber == null) {
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';
class DocumentDetailsState with EquatableMixin {
final DocumentModel? document;
final DocumentModel document;
const DocumentDetailsState({
this.document,
required this.document,
});
@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/widgets/delete_document_confirmation_dialog.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/document_type/view/widgets/document_type_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(
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButton: widget.allowEdit
? FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: _onEdit,
? BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
return FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
);
},
)
: null,
bottomNavigationBar:
@@ -79,24 +84,20 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
children: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: widget.allowEdit && state.document != null
? () => _onDelete(state.document!)
onPressed: widget.allowEdit
? () => _onDelete(state.document)
: null,
).padded(const EdgeInsets.symmetric(horizontal: 4)),
).paddedSymmetrically(horizontal: 4),
DocumentDownloadButton(
document: state.document,
),
IconButton(
icon: const Icon(Icons.open_in_new),
onPressed: state.document != null
? () => _onOpen(state.document!)
: null,
).padded(const EdgeInsets.only(right: 4)),
onPressed: () => _onOpen(state.document),
).paddedOnly(right: 4.0),
IconButton(
icon: const Icon(Icons.share),
onPressed: state.document != null
? () => _onShare(state.document!)
: null,
onPressed: () => _onShare(state.document),
),
],
),
@@ -123,15 +124,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
expandedHeight: 200.0,
flexibleSpace:
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
if (state.document == null) {
return Container(height: 200);
}
return DocumentPreview(
id: state.document!.id,
fit: BoxFit.cover,
);
},
builder: (context, state) => DocumentPreview(
id: state.document.id,
fit: BoxFit.cover,
),
),
bottom: ColoredTabBar(
backgroundColor:
@@ -172,27 +168,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
],
body: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
builder: (context, state) {
if (state.document == null) {
return TabBarView(
children: [
Container(),
Container(),
Container(),
],
);
}
return TabBarView(
children: [
_buildDocumentOverview(
state.document!,
state.document,
widget.titleAndContentQueryString,
),
_buildDocumentContentView(
state.document!,
state.document,
widget.titleAndContentQueryString,
),
_buildDocumentMetaDataView(
state.document!,
state.document,
),
].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);
if (cubit.state.document == null) {
return;
}
Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => MultiRepositoryProvider(
providers: [
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
builder: (context) => BlocProvider(
create: (context) => EditDocumentCubit(
document,
documentsApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(
context,
),
storagePathRepository:
RepositoryProvider.of<LabelRepository<StoragePath>>(
context,
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
],
child: DocumentEditPage(
document: cubit.state.document!,
onEdit: (updatedDocument) {
return BlocProvider.of<DocumentDetailsCubit>(context)
.update(updatedDocument);
),
child: BlocListener<EditDocumentCubit, EditDocumentState>(
listenWhen: (previous, current) =>
previous.document != current.document,
listener: (context, state) {
cubit.replaceDocument(state.document);
},
child: const DocumentEditPage(),
),
),
maintainState: false,
maintainState: true,
),
);
}

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.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:paperless_api/paperless_api.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/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_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/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/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class DocumentEditPage extends StatefulWidget {
final DocumentModel document;
final FutureOr<void> Function(DocumentModel updatedDocument) onEdit;
const DocumentEditPage({
Key? key,
required this.document,
required this.onEdit,
}) : super(key: key);
@override
@@ -43,150 +34,133 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
static const fkCreatedDate = "createdAtDate";
static const fkStoragePath = 'storagePath';
late Future<Uint8List> documentBytes;
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
bool _isSubmitLoading = false;
@override
void initState() {
super.initState();
documentBytes =
getIt<PaperlessDocumentsApi>().getPreview(widget.document.id);
Widget build(BuildContext context) {
return BlocBuilder<EditDocumentCubit, EditDocumentState>(
builder: (context, state) {
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 build(BuildContext context) {
return LabelsBlocProvider(
child: Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
onPressed: _onSubmit,
icon: const Icon(Icons.save),
label: Text(S.of(context).genericActionSaveLabel),
Widget _buildStoragePathFormField(
int? initialId, Map<int, StoragePath> options) {
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: 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(
title: Text(S.of(context).documentEditPageTitle),
bottom: _isSubmitLoading
? const PreferredSize(
preferredSize: Size.fromHeight(4),
child: LinearProgressIndicator(),
)
: null,
child: AddCorrespondentPage(initialName: initialValue),
),
label: S.of(context).documentCorrespondentPropertyLabel,
state: options,
initialValue: CorrespondentQuery.fromId(initialId),
name: fkCorrespondent,
queryParameterIdBuilder: CorrespondentQuery.fromId,
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,
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().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(),
]),
),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
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>>
_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 {
Future<void> _onSubmit(DocumentModel document) async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value;
var updatedDocument = widget.document.copyWith(
var mergedDocument = document.copyWith(
title: values[fkTitle],
created: values[fkCreatedDate],
overwriteDocumentType: true,
@@ -201,9 +175,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
setState(() {
_isSubmitLoading = true;
});
try {
await widget.onEdit(updatedDocument);
await BlocProvider.of<EditDocumentCubit>(context)
.updateDocument(mergedDocument);
showSnackBar(context, S.of(context).documentUpdateSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
@@ -216,18 +190,18 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
}
}
Widget _buildTitleFormField() {
Widget _buildTitleFormField(String? initialTitle) {
return FormBuilderTextField(
name: fkTitle,
validator: FormBuilderValidators.required(),
decoration: InputDecoration(
label: Text(S.of(context).documentTitlePropertyLabel),
),
initialValue: widget.document.title,
initialValue: initialTitle,
);
}
Widget _buildCreatedAtFormField() {
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
return FormBuilderDateTimePicker(
inputType: InputType.date,
name: fkCreatedDate,
@@ -235,7 +209,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
prefixIcon: const Icon(Icons.calendar_month_outlined),
label: Text(S.of(context).documentCreatedPropertyLabel),
),
initialValue: widget.document.created,
initialValue: initialCreatedAtDate,
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
initialEntryMode: DatePickerEntryMode.calendar,
);

View File

@@ -1,10 +1,10 @@
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.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/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/view_type.dart';
import 'package:paperless_mobile/util.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart';
class DocumentsPage extends StatefulWidget {
const DocumentsPage({Key? key}) : super(key: key);
@@ -35,16 +34,17 @@ class DocumentsPage extends StatefulWidget {
class _DocumentsPageState extends State<DocumentsPage> {
late final DocumentsCubit _documentsCubit;
late final SavedViewCubit _savedViewCubit;
final _pagingController = PagingController<int, DocumentModel>(
firstPageKey: 1,
);
final _filterPanelController = PanelController();
@override
void initState() {
super.initState();
_documentsCubit = BlocProvider.of<DocumentsCubit>(context);
_savedViewCubit = BlocProvider.of<SavedViewCubit>(context);
try {
_documentsCubit.load();
} on PaperlessServerException catch (error, stackTrace) {
@@ -59,97 +59,69 @@ class _DocumentsPageState extends State<DocumentsPage> {
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
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_filterPanelController.isPanelOpen) {
FocusScope.of(context).unfocus();
_filterPanelController.close();
return false;
}
if (_documentsCubit.state.selection.isNotEmpty) {
_documentsCubit.resetSelection();
return false;
}
return true;
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) =>
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) {
_documentsCubit.load();
},
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) =>
previous != ConnectivityState.connected &&
current == ConnectivityState.connected,
listener: (context, state) {
_documentsCubit.load();
},
builder: (context, connectivityState) {
return Scaffold(
builder: (context, connectivityState) {
return Scaffold(
drawer: BlocProvider.value(
value: BlocProvider.of<AuthenticationCubit>(context),
child: InfoDrawer(
afterInboxClosed: () => _documentsCubit.reload(),
),
),
resizeToAvoidBottomInset: true,
body: SlidingUpPanel(
backdropEnabled: true,
parallaxEnabled: true,
parallaxOffset: .5,
controller: _filterPanelController,
defaultPanelState: PanelState.CLOSED,
minHeight: 48,
maxHeight: (MediaQuery.of(context).size.height * 3) / 4,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
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),
),
);
},
),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
return Badge(
toAnimate: false,
showBadge: appliedFiltersCount > 0,
badgeContent: appliedFiltersCount > 0
? Text(state.filter.appliedFiltersCount.toString())
: null,
child: FloatingActionButton(
child: const Icon(Icons.filter_alt),
onPressed: _openDocumentFilter,
),
);
},
),
);
},
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) {
@@ -193,6 +165,10 @@ class _DocumentsPageState extends State<DocumentsPage> {
child = SliverToBoxAdapter(
child: DocumentsEmptyState(
state: state,
onReset: () {
_documentsCubit.updateFilter();
_savedViewCubit.resetSelection();
},
),
);
}
@@ -201,51 +177,45 @@ class _DocumentsPageState extends State<DocumentsPage> {
onRefresh: _onRefresh,
child: CustomScrollView(
slivers: [
BlocProvider(
create: (context) => SavedViewCubit(
RepositoryProvider.of<SavedViewRepository>(context)),
child: BlocListener<SavedViewCubit, SavedViewState>(
listener: (context, state) {
try {
if (state.selectedSavedViewId == null) {
_documentsCubit.updateFilter();
} else {
final newFilter = state
.value[state.selectedSavedViewId]
?.toDocumentFilter();
if (newFilter != null) {
_documentsCubit.updateFilter(filter: newFilter);
}
BlocListener<SavedViewCubit, SavedViewState>(
listenWhen: (previous, current) =>
previous.selectedSavedViewId !=
current.selectedSavedViewId,
listener: (context, state) {
try {
if (state.selectedSavedViewId == null) {
_documentsCubit.updateFilter();
} else {
final newFilter = state
.value[state.selectedSavedViewId]
?.toDocumentFilter();
if (newFilter != null) {
_documentsCubit.updateFilter(filter: newFilter);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
child: DocumentsPageAppBar(
actions: [
const SortDocumentsButton(),
IconButton(
icon: Icon(
settings.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view,
),
onPressed: () =>
BlocProvider.of<ApplicationSettingsCubit>(
context)
.setViewType(
settings.preferredViewType.toggle()),
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
child: DocumentsPageAppBar(
actions: [
const SortDocumentsButton(),
IconButton(
icon: Icon(
settings.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view,
),
],
),
onPressed: () =>
BlocProvider.of<ApplicationSettingsCubit>(context)
.setViewType(
settings.preferredViewType.toggle(),
),
),
],
),
),
child,
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).size.height / 4,
),
)
],
),
);
@@ -296,4 +266,31 @@ class _DocumentsPageState extends State<DocumentsPage> {
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 {
final DocumentsState state;
final VoidCallback onReset;
const DocumentsEmptyState({
Key? key,
required this.state,
required this.onReset,
}) : super(key: key);
@override
@@ -22,10 +24,7 @@ class DocumentsEmptyState extends StatelessWidget {
subtitle: S.of(context).documentsPageEmptyStateNothingHereText,
bottomChild: state.filter != DocumentFilter.initial
? TextButton(
onPressed: () async {
await BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).resetSelection();
},
onPressed: onReset,
child: Text(
S.of(context).documentsFilterPageResetFilterLabel,
),

View File

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

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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';
class SortFieldSelectionBottomSheet extends StatefulWidget {
@@ -46,30 +49,58 @@ class _SortFieldSelectionBottomSheetState
S.of(context).documentsPageOrderByLabel,
style: Theme.of(context).textTheme.caption,
textAlign: TextAlign.start,
).padded(
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
TextButton(
child: Text(S.of(context).documentsFilterPageApplyFilterLabel),
onPressed: () => widget.onSubmit(
_currentSortField,
_currentSortOrder,
),
onPressed: () {
widget.onSubmit(
_currentSortField,
_currentSortOrder,
);
Navigator.pop(context);
},
),
],
),
).paddedSymmetrically(horizontal: 16, vertical: 8.0),
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(
SortField field,
) {
Widget _buildSortOption(SortField field, {bool enabled = true}) {
return ListTile(
enabled: enabled,
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text(
_localizedSortField(field),
@@ -77,6 +108,14 @@ class _SortFieldSelectionBottomSheetState
trailing: _currentSortField == field
? _buildOrderIcon(_currentSortOrder)
: 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_bloc/flutter_bloc.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/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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
@@ -35,7 +33,7 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
snap: true,
floating: true,
pinned: true,
flexibleSpace: _buildFlexibleArea(false),
flexibleSpace: _buildFlexibleArea(false, documentsState.filter),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
@@ -56,13 +54,12 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
snap: true,
floating: true,
pinned: true,
flexibleSpace: _buildFlexibleArea(true),
title: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return Text(
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})',
);
},
flexibleSpace: _buildFlexibleArea(
true,
documentsState.filter,
),
title: Text(
'${S.of(context).documentsPageTitle} (${_formatDocumentCount(documentsState.count)})',
),
actions: [
...widget.actions,
@@ -73,14 +70,18 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
);
}
Widget _buildFlexibleArea(bool enabled) {
Widget _buildFlexibleArea(bool enabled, DocumentFilter filter) {
return FlexibleSpaceBar(
background: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
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_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/view/widgets/search/sort_field_selection_bottom_sheet.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class SortDocumentsButton extends StatefulWidget {
const SortDocumentsButton({
Key? key,
}) : super(key: key);
class SortDocumentsButton extends StatelessWidget {
const SortDocumentsButton({super.key});
@override
State<SortDocumentsButton> createState() => _SortDocumentsButtonState();
}
class _SortDocumentsButtonState extends State<SortDocumentsButton> {
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.sort),
onPressed: _onOpenSortBottomSheet,
onPressed: () => _onOpenSortBottomSheet(context),
);
}
void _onOpenSortBottomSheet() {
void _onOpenSortBottomSheet(BuildContext context) {
showModalBottomSheet(
elevation: 2,
context: context,
@@ -32,19 +28,41 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
topRight: Radius.circular(16),
),
),
builder: (context) => FractionallySizedBox(
heightFactor: .6,
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),
builder: (_) => BlocProvider.value(
value: BlocProvider.of<DocumentsCubit>(context),
child: FractionallySizedBox(
heightFactor: .6,
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
),
);
},
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)));
}
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);

View File

@@ -28,7 +28,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
),
child: AddLabelFormWidget(
pageTitle: pageTitle,
label: fromJsonT({'name': initialName}),
label: initialName != null ? fromJsonT({'name': initialName}) : null,
additionalFields: additionalFields,
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) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(S.of(context).editLabelPageConfirmDeletionDialogTitle),
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),
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title:
Text(S.of(context).editLabelPageConfirmDeletionDialogTitle),
content: Text(
S.of(context).editLabelPageDeletionDialogText,
),
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 {
BlocProvider.of<EditLabelCubit<T>>(context).delete(label);
Navigator.pop(context);

View File

@@ -2,8 +2,8 @@ import 'package:flutter/widgets.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/edit_label/cubit/edit_label_cubit.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 {
final DocumentType documentType;
@@ -12,7 +12,7 @@ class EditDocumentTypePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
create: (context) => EditLabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: EditLabelPage<DocumentType>(

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.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/edit_label/cubit/edit_label_cubit.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';
class EditStoragePathPage extends StatelessWidget {
@@ -13,7 +13,7 @@ class EditStoragePathPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
create: (context) => EditLabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
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:paperless_api/paperless_api.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/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class EditTagPage extends StatelessWidget {
@@ -16,7 +16,7 @@ class EditTagPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Tag>(
create: (context) => EditLabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
child: EditLabelPage<Tag>(

View File

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

View File

@@ -59,6 +59,11 @@ class _HomePageState extends State<HomePage> {
BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
),
BlocProvider(
create: (context) => SavedViewCubit(
RepositoryProvider.of<SavedViewRepository>(context),
),
),
],
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/di_initializer.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/view/pages/inbox_page.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/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -52,7 +50,7 @@ class InfoDrawer extends StatelessWidget {
height: 32,
width: 32,
color: Theme.of(context).colorScheme.onPrimaryContainer,
).padded(const EdgeInsets.only(right: 8.0)),
).paddedOnly(right: 8.0),
Text(
S.of(context).appTitleText,
style: Theme.of(context).textTheme.headline5?.copyWith(
@@ -215,7 +213,7 @@ class InfoDrawer extends StatelessWidget {
create: (context) => InboxCubit(
RepositoryProvider.of<LabelRepository<Tag>>(context),
getIt<PaperlessDocumentsApi>(),
),
)..loadInbox(),
child: const InboxPage(),
),
),

View File

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

View File

@@ -11,9 +11,13 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
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(
(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;
}
Future<void> reload() {
return _repository.findAll();
}
Future<T> replace(T item) async {
assert(item.id != null);
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:paperless_api/paperless_api.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/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';
class TagFormField extends StatefulWidget {
@@ -18,6 +14,7 @@ class TagFormField extends StatefulWidget {
final bool notAssignedSelectable;
final bool anyAssignedSelectable;
final bool excludeAllowed;
final Map<int, Tag> selectableOptions;
const TagFormField({
super.key,
@@ -27,6 +24,7 @@ class TagFormField extends StatefulWidget {
this.notAssignedSelectable = true,
this.anyAssignedSelectable = true,
this.excludeAllowed = true,
required this.selectableOptions,
});
@override
@@ -47,10 +45,7 @@ class _TagFormFieldState extends State<TagFormField> {
_textEditingController = TextEditingController()
..addListener(() {
setState(() {
_showCreationSuffixIcon = BlocProvider.of<LabelCubit<Tag>>(context)
.state
.labels
.values
_showCreationSuffixIcon = widget.selectableOptions.values
.where(
(item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(),
@@ -66,122 +61,126 @@ class _TagFormFieldState extends State<TagFormField> {
@override
Widget build(BuildContext context) {
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, tagState) {
return FormBuilderField<TagsQuery>(
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypeAheadField<int>(
textFieldConfiguration: TextFieldConfiguration(
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.label_outline,
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context).documentTagsPropertyLabel,
hintText: S.of(context).tagFormFieldSearchHintText,
),
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,
final isEnabled = widget.selectableOptions.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0) ||
widget.allowCreation;
return FormBuilderField<TagsQuery>(
enabled: isEnabled,
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypeAheadField<int>(
textFieldConfiguration: TextFieldConfiguration(
enabled: isEnabled,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.label_outline,
),
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,
tagState.getLabel(query.id),
),
)
.toList(),
),
]
],
);
},
initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name,
);
},
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context).documentTagsPropertyLabel,
hintText: S.of(context).tagFormFieldSearchHintText,
),
controller: _textEditingController,
),
suggestionsCallback: (query) {
final suggestions = widget.selectableOptions.entries
.where(
(entry) => entry.value.name
.toLowerCase()
.startsWith(query.toLowerCase()),
)
.where((entry) =>
widget.allowCreation ||
(entry.value.documentCount ?? 0) > 0)
.map((entry) => entry.key)
.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 = 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
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index));
}

View File

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

View File

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

View File

@@ -81,7 +81,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
appSettings = ApplicationSettingsState.defaultSettings;
}
if (storedAuth == null || !storedAuth.isValid) {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false));
return emit(
AuthenticationState(isAuthenticated: false, wasLoginStored: false));
} else {
if (appSettings.isLocalAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService
@@ -103,8 +104,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
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_bloc/flutter_bloc.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/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/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/view/add_saved_view_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class SavedViewSelectionWidget extends StatelessWidget {
final DocumentFilter currentFilter;
const SavedViewSelectionWidget({
Key? key,
required this.height,
required this.enabled,
required this.currentFilter,
}) : super(key: key);
final double height;
@@ -64,10 +65,18 @@ class SavedViewSelectionWidget extends StatelessWidget {
S.of(context).savedViewsLabel,
style: Theme.of(context).textTheme.titleSmall,
),
TextButton.icon(
icon: const Icon(Icons.add),
onPressed: enabled ? () => _onCreatePressed(context) : null,
label: Text(S.of(context).savedViewCreateNewLabel),
BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) =>
previous.filter != current.filter,
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?>(
MaterialPageRoute(
builder: (context) => AddSavedViewPage(
currentFilter: getIt<DocumentsCubit>().state.filter,
currentFilter: filter,
),
),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import 'package:bloc_test/bloc_test.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_state.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';