WIP - More decoupling of data layer from ui layer

This commit is contained in:
Anton Stubenbord
2022-12-09 00:54:39 +01:00
parent 75fa2f7713
commit c9694fa8d0
87 changed files with 2508 additions and 1879 deletions

View File

@@ -1,6 +1,4 @@
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:paperless_mobile/di_initializer.dart';

View File

@@ -1,7 +1,4 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
class BlocChangesObserver extends BlocObserver {
@override

View File

@@ -0,0 +1,67 @@
import 'dart:async';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class CorrespondentRepositoryImpl implements LabelRepository<Correspondent> {
final PaperlessLabelsApi _api;
final _subject = BehaviorSubject<Map<int, Correspondent>>.seeded(const {});
CorrespondentRepositoryImpl(this._api);
@override
Stream<Map<int, Correspondent>> get labels =>
_subject.stream.asBroadcastStream();
@override
Future<Correspondent> create(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent);
final updatedState = {..._subject.value}
..putIfAbsent(created.id!, () => created);
_subject.add(updatedState);
return created;
}
@override
Future<void> delete(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent);
final updatedState = {..._subject.value}
..removeWhere((k, v) => k == correspondent.id);
_subject.add(updatedState);
}
@override
Future<Correspondent?> find(int id) async {
final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) {
final updatedState = {..._subject.value}..[id] = correspondent;
_subject.add(updatedState);
return correspondent;
}
return null;
}
@override
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids);
final updatedState = {..._subject.value}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
_subject.add(updatedState);
return correspondents;
}
@override
Future<Correspondent> update(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {..._subject.value}
..update(updated.id!, (_) => updated);
_subject.add(updatedState);
return updated;
}
@override
void clear() {
_subject.add(const {});
}
}

View File

@@ -0,0 +1,66 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class DocumentTypeRepositoryImpl implements LabelRepository<DocumentType> {
final PaperlessLabelsApi _api;
final _subject = BehaviorSubject<Map<int, DocumentType>>.seeded(const {});
DocumentTypeRepositoryImpl(this._api);
@override
Stream<Map<int, DocumentType>> get labels =>
_subject.stream.asBroadcastStream();
@override
Future<DocumentType> create(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType);
final updatedState = {..._subject.value}
..putIfAbsent(created.id!, () => created);
_subject.add(updatedState);
return created;
}
@override
Future<void> delete(DocumentType documentType) async {
await _api.deleteDocumentType(documentType);
final updatedState = {..._subject.value}
..removeWhere((k, v) => k == documentType.id);
_subject.add(updatedState);
}
@override
Future<DocumentType?> find(int id) async {
final documentType = await _api.getDocumentType(id);
if (documentType != null) {
final updatedState = {..._subject.value}..[id] = documentType;
_subject.add(updatedState);
return documentType;
}
return null;
}
@override
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {..._subject.value}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
_subject.add(updatedState);
return documentTypes;
}
@override
Future<DocumentType> update(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType);
final updatedState = {..._subject.value}
..update(updated.id!, (_) => updated);
_subject.add(updatedState);
return updated;
}
@override
void clear() {
_subject.add(const {});
}
}

View File

@@ -0,0 +1,57 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/models/saved_view_model.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:rxdart/rxdart.dart';
class SavedViewRepositoryImpl implements SavedViewRepository {
final PaperlessSavedViewsApi _api;
SavedViewRepositoryImpl(this._api);
final BehaviorSubject<Map<int, SavedView>> _subject =
BehaviorSubject.seeded({});
@override
Stream<Map<int, SavedView>> get savedViews =>
_subject.stream.asBroadcastStream();
@override
void clear() {}
@override
Future<SavedView> create(SavedView view) async {
final created = await _api.save(view);
final updatedState = {..._subject.value}
..putIfAbsent(created.id!, () => created);
_subject.add(updatedState);
return created;
}
@override
Future<int> delete(SavedView view) async {
await _api.delete(view);
final updatedState = {..._subject.value}..remove(view.id);
_subject.add(updatedState);
return view.id!;
}
@override
Future<SavedView?> find(int id) async {
final found = await _api.find(id);
final updatedState = {..._subject.value}
..update(id, (_) => found, ifAbsent: () => found);
_subject.add(updatedState);
return found;
}
@override
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids);
final updatedState = {
..._subject.value,
...{for (final view in found) view.id!: view},
};
_subject.add(updatedState);
return found;
}
}

View File

@@ -0,0 +1,66 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class StoragePathRepositoryImpl implements LabelRepository<StoragePath> {
final PaperlessLabelsApi _api;
final _subject = BehaviorSubject<Map<int, StoragePath>>.seeded(const {});
StoragePathRepositoryImpl(this._api);
@override
Stream<Map<int, StoragePath>> get labels =>
_subject.stream.asBroadcastStream();
@override
Future<StoragePath> create(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath);
final updatedState = {..._subject.value}
..putIfAbsent(created.id!, () => created);
_subject.add(updatedState);
return created;
}
@override
Future<void> delete(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath);
final updatedState = {..._subject.value}
..removeWhere((k, v) => k == storagePath.id);
_subject.add(updatedState);
}
@override
Future<StoragePath?> find(int id) async {
final storagePath = await _api.getStoragePath(id);
if (storagePath != null) {
final updatedState = {..._subject.value}..[id] = storagePath;
_subject.add(updatedState);
return storagePath;
}
return null;
}
@override
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {..._subject.value}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
_subject.add(updatedState);
return storagePaths;
}
@override
Future<StoragePath> update(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath);
final updatedState = {..._subject.value}
..update(updated.id!, (_) => updated);
_subject.add(updatedState);
return updated;
}
@override
void clear() {
_subject.add(const {});
}
}

View File

@@ -0,0 +1,65 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class TagRepositoryImpl implements LabelRepository<Tag> {
final PaperlessLabelsApi _api;
final _subject = BehaviorSubject<Map<int, Tag>>.seeded(const {});
TagRepositoryImpl(this._api);
@override
Stream<Map<int, Tag>> get labels => _subject.stream.asBroadcastStream();
@override
Future<Tag> create(Tag tag) async {
final created = await _api.saveTag(tag);
final updatedState = {..._subject.value}
..putIfAbsent(created.id!, () => created);
_subject.add(updatedState);
return created;
}
@override
Future<void> delete(Tag tag) async {
await _api.deleteTag(tag);
final updatedState = {..._subject.value}
..removeWhere((k, v) => k == tag.id);
_subject.add(updatedState);
}
@override
Future<Tag?> find(int id) async {
final tag = await _api.getTag(id);
if (tag != null) {
final updatedState = {..._subject.value}..[id] = tag;
_subject.add(updatedState);
return tag;
}
return null;
}
@override
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids);
final updatedState = {..._subject.value}
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
_subject.add(updatedState);
return tags;
}
@override
Future<Tag> update(Tag tag) async {
final updated = await _api.updateTag(tag);
final updatedState = {..._subject.value}
..update(updated.id!, (_) => updated);
_subject.add(updatedState);
return updated;
}
@override
void clear() {
_subject.add(const {});
}
}

View File

@@ -0,0 +1,13 @@
import 'package:paperless_api/paperless_api.dart';
abstract class LabelRepository<T extends Label> {
Stream<Map<int, T>> get labels;
Future<T> create(T label);
Future<T?> find(int id);
Future<Iterable<T>> findAll([Iterable<int>? ids]);
Future<T> update(T label);
Future<void> delete(T label);
void clear();
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
class LabelRepositoriesProvider extends StatelessWidget {
final Widget child;
const LabelRepositoriesProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
),
],
child: child,
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:paperless_api/paperless_api.dart';
abstract class SavedViewRepository {
Stream<Map<int, SavedView>> get savedViews;
Future<SavedView> create(SavedView view);
Future<SavedView?> find(int id);
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]);
Future<int> delete(SavedView view);
void clear();
}

View File

@@ -8,6 +8,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/date_symbol_data_local.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/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -17,7 +18,6 @@ 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/labels/bloc/global_state_bloc_provider.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';
@@ -213,7 +213,29 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
builder: (_) => MultiRepositoryProvider(
providers: [
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(
context,
),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
),
],
child: DocumentEditPage(
document: cubit.state.document!,
onEdit: (updatedDocument) {

View File

@@ -0,0 +1,90 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
part 'document_upload_state.dart';
class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final LocalVault _localVault;
final PaperlessDocumentsApi _documentApi;
final LabelRepository<Tag> _tagRepository;
final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType> _documentTypeRepository;
final List<StreamSubscription> _subs = const [];
DocumentUploadCubit({
required LocalVault localVault,
required PaperlessDocumentsApi documentApi,
required LabelRepository<Tag> tagRepository,
required LabelRepository<Correspondent> correspondentRepository,
required LabelRepository<DocumentType> documentTypeRepository,
}) : _documentApi = documentApi,
_tagRepository = tagRepository,
_correspondentRepository = correspondentRepository,
_documentTypeRepository = documentTypeRepository,
_localVault = localVault,
super(
const DocumentUploadState(
tags: {},
correspondents: {},
documentTypes: {},
),
) {
_subs.add(_tagRepository.labels.listen(
(tags) => emit(state.copyWith(tags: tags)),
));
_subs.add(_correspondentRepository.labels.listen(
(correspondents) => emit(state.copyWith(correspondents: correspondents)),
));
_subs.add(_documentTypeRepository.labels.listen(
(documentTypes) => emit(state.copyWith(documentTypes: documentTypes)),
));
}
Future<void> upload(
Uint8List bytes, {
required String filename,
required String title,
required void Function(DocumentModel document)? onConsumptionFinished,
int? documentType,
int? correspondent,
Iterable<int> tags = const [],
DateTime? createdAt,
}) async {
final auth = await _localVault.loadAuthenticationInformation();
if (auth == null || !auth.isValid) {
throw const PaperlessServerException(ErrorCode.notAuthenticated);
}
await _documentApi.create(
bytes,
filename: filename,
title: title,
correspondent: correspondent,
documentType: documentType,
tags: tags,
createdAt: createdAt,
authToken: auth.token!,
serverUrl: auth.serverUrl,
);
if (onConsumptionFinished != null) {
_documentApi
.waitForConsumptionFinished(filename, title)
.then((value) => onConsumptionFinished(value));
}
}
@override
Future<void> close() async {
for (final sub in _subs) {
await sub.cancel();
}
return super.close();
}
}

View File

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

View File

@@ -0,0 +1,275 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/date_symbol_data_local.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/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_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/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 DocumentUploadPreparationPage extends StatefulWidget {
final Uint8List fileBytes;
final String? title;
final String? filename;
final void Function(DocumentModel)? onSuccessfullyConsumed;
const DocumentUploadPreparationPage({
Key? key,
required this.fileBytes,
this.title,
this.filename,
this.onSuccessfullyConsumed,
}) : super(key: key);
@override
State<DocumentUploadPreparationPage> createState() =>
_DocumentUploadPreparationPageState();
}
class _DocumentUploadPreparationPageState
extends State<DocumentUploadPreparationPage> {
static const fkFileName = "filename";
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
PaperlessValidationErrors _errors = {};
bool _isUploadLoading = false;
late bool _syncTitleAndFilename;
final _now = DateTime.now();
@override
void initState() {
super.initState();
_syncTitleAndFilename = widget.filename == null && widget.title == null;
initializeDateFormatting();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(S.of(context).documentsUploadPageTitle),
bottom: _isUploadLoading
? const PreferredSize(
child: LinearProgressIndicator(),
preferredSize: Size.fromHeight(4.0))
: null,
),
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
onPressed: _onSubmit,
label: Text(S.of(context).genericActionUploadLabel),
icon: const Icon(Icons.upload),
),
),
body: BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
builder: (context, state) {
return FormBuilder(
key: _formKey,
child: ListView(
children: [
FormBuilderTextField(
autovalidateMode: AutovalidateMode.always,
name: DocumentModel.titleKey,
initialValue:
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
validator: FormBuilderValidators.required(),
decoration: InputDecoration(
labelText: S.of(context).documentTitlePropertyLabel,
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_formKey.currentState?.fields[DocumentModel.titleKey]
?.didChange("");
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]
?.didChange("");
}
},
),
errorText: _errors[DocumentModel.titleKey],
),
onChanged: (value) {
final String transformedValue =
_formatFilename(value ?? '');
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
}
},
),
FormBuilderTextField(
autovalidateMode: AutovalidateMode.always,
readOnly: _syncTitleAndFilename,
enabled: !_syncTitleAndFilename,
name: fkFileName,
decoration: InputDecoration(
labelText: S.of(context).documentUploadFileNameLabel,
suffixText: ".pdf",
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _formKey.currentState?.fields[fkFileName]
?.didChange(''),
),
),
initialValue: widget.filename ??
"scan_${fileNameDateFormat.format(_now)}",
),
SwitchListTile(
value: _syncTitleAndFilename,
onChanged: (value) {
setState(
() => _syncTitleAndFilename = value,
);
if (_syncTitleAndFilename) {
final String transformedValue = _formatFilename(_formKey
.currentState
?.fields[DocumentModel.titleKey]
?.value as String);
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
}
}
},
title: Text(S
.of(context)
.documentUploadPageSynchronizeTitleAndFilenameLabel), //TODO: INTL
),
FormBuilderDateTimePicker(
autovalidateMode: AutovalidateMode.always,
format: DateFormat("dd. MMMM yyyy"), //TODO: INTL
inputType: InputType.date,
name: DocumentModel.createdKey,
initialValue: null,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.calendar_month_outlined),
labelText:
S.of(context).documentCreatedPropertyLabel + " *",
),
),
LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialName) =>
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
child: AddDocumentTypePage(initialName: initialName),
),
label: S.of(context).documentDocumentTypePropertyLabel + " *",
name: DocumentModel.documentTypeKey,
state: state.documentTypes,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder:
DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined),
),
LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialName) =>
RepositoryProvider.value(
value:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
child: AddCorrespondentPage(initialName: initialName),
),
label:
S.of(context).documentCorrespondentPropertyLabel + " *",
name: DocumentModel.correspondentKey,
state: state.correspondents,
queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder:
CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outline),
),
const TagFormField(
name: DocumentModel.tagsKey,
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
//Label: "Tags" + " *",
),
Text(
"* " +
S
.of(context)
.uploadPageAutomaticallInferredFieldsHintText,
style: Theme.of(context).textTheme.caption,
),
].padded(),
),
);
},
),
);
}
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final cubit = BlocProvider.of<DocumentUploadCubit>(context);
try {
setState(() => _isUploadLoading = true);
final fv = _formKey.currentState!.value;
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
final title = fv[DocumentModel.titleKey] as String;
final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter;
final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
final correspondent =
fv[DocumentModel.correspondentKey] as IdQueryParameter;
await cubit.upload(
widget.fileBytes,
filename:
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
title: title,
onConsumptionFinished: widget.onSuccessfullyConsumed,
documentType: docType.id,
correspondent: correspondent.id,
tags: tags.ids,
createdAt: createdAt,
);
showSnackBar(context, S.of(context).documentUploadSuccessText);
Navigator.pop(context, true);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (PaperlessServerExceptions) {
setState(() => _errors = PaperlessServerExceptions);
} catch (unknownError, stackTrace) {
showErrorMessage(
context, const PaperlessServerException.unknown(), stackTrace);
} finally {
setState(() {
_isUploadLoading = false;
});
}
}
}
String _padWithPdfExtension(String source) {
return source.endsWith(".pdf") ? source : '$source.pdf';
}
String _formatFilename(String source) {
return source.replaceAll(RegExp(r"[\W_]"), "_");
}
}

View File

@@ -1,11 +1,9 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
@prod
@test
@lazySingleton
part 'documents_state.dart';
class DocumentsCubit extends Cubit<DocumentsState> {
final PaperlessDocumentsApi _api;

View File

@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
part of 'documents_cubit.dart';
class DocumentsState extends Equatable {
final bool isLoaded;

View File

@@ -7,15 +7,15 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
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/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_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/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.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';
@@ -57,10 +57,133 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
return LabelsBlocProvider(
child: Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
onPressed: _onSubmit,
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().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(),
]),
),
),
),
);
}
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 {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value;
var updatedDocument = widget.document.copyWith(
@@ -91,111 +214,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
Navigator.pop(context);
}
}
},
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().padded(),
_buildCreatedAtFormField().padded(),
BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) =>
BlocProvider.value(
value: BlocProvider.of<DocumentTypeCubit>(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),
);
},
).padded(),
BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
BlocProvider.value(
value: BlocProvider.of<CorrespondentCubit>(context),
child: AddCorrespondentPage(initalValue: 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),
);
},
).padded(),
BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(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),
);
},
).padded(),
TagFormField(
initialValue: IdsTagsQuery.included(widget.document.tags),
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
name: fkTags,
).padded(),
]),
),
),
);
}
Widget _buildTitleFormField() {

View File

@@ -3,11 +3,12 @@ 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';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart';
@@ -15,11 +16,10 @@ import 'package:paperless_mobile/features/documents/view/widgets/search/document
import 'package:paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -132,9 +132,20 @@ class _DocumentsPageState extends State<DocumentsPage> {
),
body: _buildBody(connectivityState),
color: Theme.of(context).scaffoldBackgroundColor,
panelBuilder: (scrollController) => DocumentFilterPanel(
panelBuilder: (scrollController) =>
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return LabelsBlocProvider(
child: DocumentFilterPanel(
panelController: _filterPanelController,
scrollController: scrollController,
initialFilter: state.filter,
onFilterChanged: (filter) =>
BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: filter),
),
);
},
),
),
);
@@ -192,7 +203,29 @@ class _DocumentsPageState extends State<DocumentsPage> {
onRefresh: _onRefresh,
child: CustomScrollView(
slivers: [
DocumentsPageAppBar(
BlocProvider(
create: (context) => SavedViewCubit(
RepositoryProvider.of<SavedViewRepository>(context)),
child: BlocListener<SavedViewCubit, SavedViewState>(
listener: (context, state) {
final documentsCubit =
BlocProvider.of<DocumentsCubit>(context);
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(
@@ -202,12 +235,15 @@ class _DocumentsPageState extends State<DocumentsPage> {
: Icons.grid_view,
),
onPressed: () =>
BlocProvider.of<ApplicationSettingsCubit>(context)
BlocProvider.of<ApplicationSettingsCubit>(
context)
.setViewType(
settings.preferredViewType.toggle()),
),
],
),
),
),
child,
SliverToBoxAdapter(
child: SizedBox(
@@ -233,29 +269,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
DocumentModel document) {
return MaterialPageRoute(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: BlocProvider.of<DocumentsCubit>(context),
builder: (_) => BlocProvider.value(
value: DocumentDetailsCubit(getIt<PaperlessDocumentsApi>(), document),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(),
),
BlocProvider.value(
value: BlocProvider.of<CorrespondentCubit>(context),
),
BlocProvider.value(
value: BlocProvider.of<DocumentTypeCubit>(context),
),
BlocProvider.value(
value: BlocProvider.of<TagCubit>(context),
),
BlocProvider.value(
value: BlocProvider.of<StoragePathCubit>(context),
),
BlocProvider.value(
value:
DocumentDetailsCubit(getIt<PaperlessDocumentsApi>(), document),
),
],
child: const DocumentDetailsPage(),
),
);
}

View File

@@ -4,8 +4,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/empty_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class DocumentsEmptyState extends StatelessWidget {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';

View File

@@ -1,8 +1,10 @@
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/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
@@ -34,7 +36,8 @@ class DocumentListView extends StatelessWidget {
builderDelegate: PagedChildBuilderDelegate(
animateTransitions: true,
itemBuilder: (context, document, index) {
return DocumentListItem(
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
@@ -49,6 +52,7 @@ class DocumentListView extends StatelessWidget {
: false;
},
onTagSelected: onTagSelected,
),
);
},
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection

View File

@@ -4,15 +4,12 @@ import 'package:flutter_form_builder/flutter_form_builder.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/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.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/bloc/saved_view_cubit.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';
@@ -24,10 +21,15 @@ 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);
@override
@@ -63,13 +65,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
child: BlocConsumer<DocumentsCubit, DocumentsState>(
listener: (context, state) {
// Set initial values, otherwise they would not automatically update.
_patchFromFilter(state.filter);
},
builder: (context, state) {
return FormBuilder(
child: FormBuilder(
key: _formKey,
child: Column(
children: [
@@ -81,8 +77,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
alignment: Alignment.topRight,
child: TextButton.icon(
icon: const Icon(Icons.refresh),
label: Text(
S.of(context).documentsFilterPageResetFilterLabel),
label:
Text(S.of(context).documentsFilterPageResetFilterLabel),
onPressed: () => _resetFilter(context),
),
),
@@ -100,8 +96,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
),
TextButton(
onPressed: _onApplyFilter,
child: Text(
S.of(context).documentsFilterPageApplyFilterLabel),
child:
Text(S.of(context).documentsFilterPageApplyFilterLabel),
),
],
).padded(),
@@ -119,23 +115,22 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentsFilterPageSearchLabel),
child: Text(S.of(context).documentsFilterPageSearchLabel),
).padded(const EdgeInsets.only(left: 8.0)),
_buildQueryFormField(state),
_buildQueryFormField(),
Align(
alignment: Alignment.centerLeft,
child: Text(
S.of(context).documentsFilterPageAdvancedLabel),
child:
Text(S.of(context).documentsFilterPageAdvancedLabel),
).padded(const EdgeInsets.only(left: 8.0, top: 8.0)),
_buildCreatedDateRangePickerFormField(state).padded(),
_buildAddedDateRangePickerFormField(state).padded(),
_buildCorrespondentFormField(state).padded(),
_buildDocumentTypeFormField(state).padded(),
_buildStoragePathFormField(state).padded(),
_buildCreatedDateRangePickerFormField().padded(),
_buildAddedDateRangePickerFormField().padded(),
_buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField().padded(),
_buildStoragePathFormField().padded(),
TagFormField(
name: DocumentModel.tagsKey,
initialValue: state.filter.tags,
initialValue: widget.initialFilter.tags,
allowCreation: false,
).padded(),
// Required in order for the storage path field to be visible when typing
@@ -148,8 +143,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
),
],
),
);
},
),
);
}
@@ -163,15 +156,16 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
}
}
Widget _buildDocumentTypeFormField(DocumentsState docState) {
return BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
//TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField!
Widget _buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
formBuilderState: _formKey.currentState,
name: fkDocumentType,
state: state.labels,
label: S.of(context).documentDocumentTypePropertyLabel,
initialValue: docState.filter.documentType,
initialValue: widget.initialFilter.documentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined),
@@ -180,15 +174,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildCorrespondentFormField(DocumentsState docState) {
return BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
Widget _buildCorrespondentFormField() {
return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
formBuilderState: _formKey.currentState,
name: fkCorrespondent,
state: state.labels,
label: S.of(context).documentCorrespondentPropertyLabel,
initialValue: docState.filter.correspondent,
initialValue: widget.initialFilter.correspondent,
queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outline),
@@ -197,15 +191,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildStoragePathFormField(DocumentsState docState) {
return BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
Widget _buildStoragePathFormField() {
return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>(
formBuilderState: _formKey.currentState,
name: fkStoragePath,
state: state.labels,
label: S.of(context).documentStoragePathPropertyLabel,
initialValue: docState.filter.storagePath,
initialValue: widget.initialFilter.storagePath,
queryParameterIdBuilder: StoragePathQuery.fromId,
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
prefixIcon: const Icon(Icons.folder_outlined),
@@ -214,7 +208,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildQueryFormField(DocumentsState state) {
Widget _buildQueryFormField() {
final queryType =
_formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
QueryType.titleAndContent;
@@ -239,16 +233,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
prefixIcon: const Icon(Icons.search_outlined),
labelText: label,
suffixIcon: QueryTypeFormField(
initialValue: state.filter.queryType,
initialValue: widget.initialFilter.queryType,
afterSelected: (queryType) => setState(() {}),
),
),
initialValue: state.filter.queryText,
initialValue: widget.initialFilter.queryText,
).padded();
}
Widget _buildDateRangePickerHelper(
DocumentsState state, String formFieldKey) {
Widget _buildDateRangePickerHelper(String formFieldKey) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
@@ -328,13 +321,13 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
);
}
Widget _buildCreatedDateRangePickerFormField(DocumentsState state) {
Widget _buildCreatedDateRangePickerFormField() {
return Column(
children: [
FormBuilderDateRangePicker(
initialValue: _dateTimeRangeOfNullable(
state.filter.createdDateAfter,
state.filter.createdDateBefore,
widget.initialFilter.createdDateAfter,
widget.initialFilter.createdDateBefore,
),
// Workaround for theme data not being correctly passed to daterangepicker, see
// https://github.com/flutter/flutter/issues/87580
@@ -371,18 +364,18 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
),
),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkCreatedAt),
_buildDateRangePickerHelper(fkCreatedAt),
],
);
}
Widget _buildAddedDateRangePickerFormField(DocumentsState state) {
Widget _buildAddedDateRangePickerFormField() {
return Column(
children: [
FormBuilderDateRangePicker(
initialValue: _dateTimeRangeOfNullable(
state.filter.addedDateAfter,
state.filter.addedDateBefore,
widget.initialFilter.addedDateAfter,
widget.initialFilter.addedDateBefore,
),
// Workaround for theme data not being correctly passed to daterangepicker, see
// https://github.com/flutter/flutter/issues/87580
@@ -419,7 +412,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
),
),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkAddedAt),
_buildDateRangePickerHelper(fkAddedAt),
],
);
}

View File

@@ -1,14 +1,20 @@
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/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget {
const SortFieldSelectionBottomSheet({super.key});
final SortOrder initialSortOrder;
final SortField initialSortField;
final Future Function(SortField field, SortOrder order) onSubmit;
const SortFieldSelectionBottomSheet({
super.key,
required this.initialSortOrder,
required this.initialSortField,
required this.onSubmit,
});
@override
State<SortFieldSelectionBottomSheet> createState() =>
@@ -17,81 +23,60 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
class _SortFieldSelectionBottomSheetState
extends State<SortFieldSelectionBottomSheet> {
SortField? _selectedFieldLoading;
SortOrder? _selectedOrderLoading;
late SortField _currentSortField;
late SortOrder _currentSortOrder;
@override
void initState() {
super.initState();
_currentSortField = widget.initialSortField;
_currentSortOrder = widget.initialSortOrder;
}
@override
Widget build(BuildContext context) {
return ClipRRect(
child: BlocBuilder<DocumentsCubit, DocumentsState>(
bloc: getIt<DocumentsCubit>(),
builder: (context, state) {
return Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).documentsPageOrderByLabel,
style: Theme.of(context).textTheme.caption,
textAlign: TextAlign.start,
).padded(
const EdgeInsets.symmetric(horizontal: 16, vertical: 16)),
Column(
children: SortField.values
.map(
(e) => _buildSortOption(
e,
state.filter.sortOrder,
state.filter.sortField == e,
_selectedFieldLoading == e,
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
TextButton(
child: Text(S.of(context).documentsFilterPageApplyFilterLabel),
onPressed: () => widget.onSubmit(
_currentSortField,
_currentSortOrder,
),
)
.toList(),
),
],
);
},
),
Column(
children: SortField.values.map(_buildSortOption).toList(),
),
],
),
);
}
Widget _buildSortOption(
SortField field,
SortOrder order,
bool isCurrentlySelected,
bool isNextSelected,
) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text(
_localizedSortField(field),
),
trailing: isNextSelected
? (_buildOrderIcon(_selectedOrderLoading!))
: (_selectedOrderLoading == null && isCurrentlySelected
? _buildOrderIcon(order)
: null),
onTap: () async {
setState(() {
_selectedFieldLoading = field;
_selectedOrderLoading =
isCurrentlySelected ? order.toggle() : SortOrder.descending;
});
BlocProvider.of<DocumentsCubit>(context)
.updateCurrentFilter((filter) => filter.copyWith(
sortOrder: isCurrentlySelected
? order.toggle()
: SortOrder.descending,
sortField: field,
))
.whenComplete(() {
if (mounted) {
setState(() {
_selectedFieldLoading = null;
_selectedOrderLoading = null;
});
}
});
},
trailing: _currentSortField == field
? _buildOrderIcon(_currentSortOrder)
: null,
);
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class BulkDeleteConfirmationDialog extends StatelessWidget {

View File

@@ -1,9 +1,10 @@
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/bloc/documents_state.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';
@@ -79,7 +80,6 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
//TODO: replace with sorting stuff...
SavedViewSelectionWidget(height: 48, enabled: enabled),
],
),

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.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';
@@ -33,11 +32,19 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
topRight: Radius.circular(16),
),
),
builder: (context) => BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: const FractionallySizedBox(
builder: (context) => FractionallySizedBox(
heightFactor: .6,
child: SortFieldSelectionBottomSheet(),
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,31 @@
import 'dart:async';
import 'package:bloc/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_state.dart';
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
final LabelRepository<T> _repository;
StreamSubscription<Map<int, T>>? _subscription;
EditLabelCubit(LabelRepository<T> repository)
: _repository = repository,
super(const EditLabelInitial()) {
_subscription = _repository.labels
.listen((labels) => emit(EditLabelState(labels: labels)));
}
Future<void> create(T label) => _repository.create(label);
Future<void> update(T label) => _repository.update(label);
Future<void> delete(T label) => _repository.delete(label);
@override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@immutable
class EditLabelState<T> extends Equatable {
final Map<int, T> labels;
const EditLabelState({required this.labels});
@override
List<Object> get props => [labels];
}
class EditLabelInitial<T> extends EditLabelState<T> {
const EditLabelInitial() : super(labels: const {});
}

View File

@@ -0,0 +1,71 @@
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/label_form.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddLabelPage<T extends Label> extends StatelessWidget {
final String? initialName;
final Widget pageTitle;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
const AddLabelPage({
super.key,
this.initialName,
required this.pageTitle,
required this.fromJsonT,
this.additionalFields = const [],
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
RepositoryProvider.of<LabelRepository<T>>(context),
),
child: AddLabelFormWidget(
pageTitle: pageTitle,
label: fromJsonT({'name': initialName}),
additionalFields: additionalFields,
fromJsonT: fromJsonT,
),
);
}
}
class AddLabelFormWidget<T extends Label> extends StatelessWidget {
final T? label;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
final Widget pageTitle;
const AddLabelFormWidget({
super.key,
this.label,
required this.fromJsonT,
required this.additionalFields,
required this.pageTitle,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: pageTitle,
),
body: LabelForm<T>(
initialValue: label,
fromJsonT: fromJsonT,
submitButtonConfig: SubmitButtonConfig<T>(
icon: const Icon(Icons.add),
label: Text(S.of(context).genericActionCreateLabel),
onSubmit: BlocProvider.of<EditLabelCubit<T>>(context).create,
),
additionalFields: additionalFields,
),
);
}
}

View File

@@ -0,0 +1,105 @@
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/label_form.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class EditLabelPage<T extends Label> extends StatelessWidget {
final T label;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
const EditLabelPage({
super.key,
required this.label,
required this.fromJsonT,
this.additionalFields = const [],
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
RepositoryProvider.of<LabelRepository<T>>(context),
),
child: EditLabelForm(
label: label,
additionalFields: additionalFields,
fromJsonT: fromJsonT,
),
);
}
}
class EditLabelForm<T extends Label> extends StatelessWidget {
final T label;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
const EditLabelForm({
super.key,
required this.label,
required this.fromJsonT,
required this.additionalFields,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).genericActionEditLabel),
actions: [
IconButton(
onPressed: () => _onDelete(context),
icon: const Icon(Icons.delete),
),
],
),
body: LabelForm<T>(
initialValue: label,
fromJsonT: fromJsonT,
submitButtonConfig: SubmitButtonConfig<T>(
icon: const Icon(Icons.update),
label: Text(S.of(context).genericActionUpdateLabel),
onSubmit: BlocProvider.of<EditLabelCubit<T>>(context).update,
),
additionalFields: additionalFields,
),
);
}
void _onDelete(BuildContext context) {
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),
),
),
],
),
);
} else {
BlocProvider.of<EditLabelCubit<T>>(context).delete(label);
Navigator.pop(context);
}
}
}

View File

@@ -0,0 +1,26 @@
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/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddCorrespondentPage extends StatelessWidget {
final String? initialName;
const AddCorrespondentPage({Key? key, this.initialName}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
child: AddLabelPage<Correspondent>(
pageTitle: Text(S.of(context).addCorrespondentPageTitle),
fromJsonT: Correspondent.fromJson,
initialName: initialName,
),
);
}
}

View File

@@ -0,0 +1,29 @@
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/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddDocumentTypePage extends StatelessWidget {
final String? initialName;
const AddDocumentTypePage({
super.key,
this.initialName,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: AddLabelPage<DocumentType>(
pageTitle: Text(S.of(context).addDocumentTypePageTitle),
fromJsonT: DocumentType.fromJson,
initialName: initialName,
),
);
}
}

View File

@@ -0,0 +1,31 @@
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/add_label_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddStoragePathPage extends StatelessWidget {
final String? initalValue;
const AddStoragePathPage({Key? key, this.initalValue}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: AddLabelPage<StoragePath>(
pageTitle: Text(S.of(context).addStoragePathPageTitle),
fromJsonT: StoragePath.fromJson,
initialName: initalValue,
additionalFields: const [
StoragePathAutofillFormBuilderField(name: StoragePath.pathKey),
SizedBox(height: 120.0),
],
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddTagPage extends StatelessWidget {
final String? initialValue;
const AddTagPage({Key? key, this.initialValue}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context).addTagPageTitle),
fromJsonT: Tag.fromJson,
initialName: initialValue,
additionalFields: [
FormBuilderColorPickerField(
name: Tag.colorKey,
valueTransformer: (color) => "#${color?.value.toRadixString(16)}",
decoration: InputDecoration(
label: Text(S.of(context).tagColorPropertyLabel),
),
colorPickerType: ColorPickerType.materialPicker,
initialValue: null,
),
FormBuilderCheckbox(
name: Tag.isInboxTagKey,
title: Text(S.of(context).tagInboxTagPropertyLabel),
),
],
),
);
}
}

View File

@@ -0,0 +1,25 @@
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';
class EditCorrespondentPage extends StatelessWidget {
final Correspondent correspondent;
const EditCorrespondentPage({super.key, required this.correspondent});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
child: EditLabelPage<Correspondent>(
label: correspondent,
fromJsonT: Correspondent.fromJson,
),
);
}
}

View File

@@ -0,0 +1,24 @@
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/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class EditDocumentTypePage extends StatelessWidget {
final DocumentType documentType;
const EditDocumentTypePage({super.key, required this.documentType});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: EditLabelPage<DocumentType>(
label: documentType,
fromJsonT: DocumentType.fromJson,
),
);
}
}

View File

@@ -0,0 +1,32 @@
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/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 {
final StoragePath storagePath;
const EditStoragePathPage({super.key, required this.storagePath});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: EditLabelPage<StoragePath>(
label: storagePath,
fromJsonT: StoragePath.fromJson,
additionalFields: [
StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey,
initialValue: storagePath.path,
),
const SizedBox(height: 120.0),
],
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/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 {
final Tag tag;
const EditTagPage({super.key, required this.tag});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
child: EditLabelPage<Tag>(
label: tag,
fromJsonT: Tag.fromJson,
additionalFields: [
FormBuilderColorPickerField(
initialValue: tag.color,
name: Tag.colorKey,
decoration: InputDecoration(
label: Text(S.of(context).tagColorPropertyLabel),
),
colorPickerType: ColorPickerType.blockPicker,
),
FormBuilderCheckbox(
initialValue: tag.isInboxTag,
name: Tag.isInboxTagKey,
title: Text(S.of(context).tagInboxTagPropertyLabel),
),
],
),
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
@@ -9,27 +7,42 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class EditLabelPage<T extends Label> extends StatefulWidget {
final T label;
class SubmitButtonConfig<T extends Label> {
final Widget icon;
final Widget label;
final Future<void> Function(T) onSubmit;
final Future<void> Function(T) onDelete;
final T Function(JSON) fromJson;
SubmitButtonConfig({
required this.icon,
required this.label,
required this.onSubmit,
});
}
class LabelForm<T extends Label> extends StatefulWidget {
final T? initialValue;
final SubmitButtonConfig submitButtonConfig;
/// FromJson method to parse the form field values into a label instance.
final T Function(Map<String, dynamic> json) fromJsonT;
/// List of additionally rendered form fields.
final List<Widget> additionalFields;
const EditLabelPage({
const LabelForm({
Key? key,
required this.label,
required this.fromJson,
required this.onSubmit,
required this.onDelete,
required this.initialValue,
required this.fromJsonT,
this.additionalFields = const [],
required this.submitButtonConfig,
}) : super(key: key);
@override
State<EditLabelPage> createState() => _EditLabelPageState<T>();
State<LabelForm> createState() => _LabelFormState<T>();
}
class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
final _formKey = GlobalKey<FormBuilderState>();
PaperlessValidationErrors _errors = {};
@@ -38,18 +51,9 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(S.of(context).genericActionEditLabel),
actions: [
IconButton(
onPressed: _onDelete,
icon: const Icon(Icons.delete),
),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.update),
label: Text(S.of(context).genericActionUpdateLabel),
icon: widget.submitButtonConfig.icon,
label: widget.submitButtonConfig.label,
onPressed: _onSubmit,
),
body: FormBuilder(
@@ -63,7 +67,7 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
errorText: _errors[Label.nameKey],
),
validator: FormBuilderValidators.required(),
initialValue: widget.label.name,
initialValue: widget.initialValue?.name,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderTextField(
@@ -72,12 +76,13 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
labelText: S.of(context).labelMatchPropertyLabel,
errorText: _errors[Label.matchKey],
),
initialValue: widget.label.match,
initialValue: widget.initialValue?.match,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderDropdown<int?>(
//TODO: Extract to own widget.
name: Label.matchingAlgorithmKey,
initialValue: widget.label.matchingAlgorithm?.value ??
initialValue: widget.initialValue?.matchingAlgorithm?.value ??
MatchingAlgorithm.allWords.value,
decoration: InputDecoration(
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
@@ -95,7 +100,7 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
),
FormBuilderCheckbox(
name: Label.isInsensitiveKey,
initialValue: widget.label.isInsensitive,
initialValue: widget.initialValue?.isInsensitive,
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
),
...widget.additionalFields,
@@ -105,46 +110,14 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
);
}
void _onDelete() {
if ((widget.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: () {
Navigator.pop(context);
widget.onDelete(widget.label);
},
child: Text(
S.of(context).genericActionDeleteLabel,
style: TextStyle(color: Theme.of(context).errorColor),
),
),
],
),
);
} else {
widget.onDelete(widget.label);
}
}
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
try {
final mergedJson = {
...widget.label.toJson(),
...widget.initialValue?.toJson() ?? {},
..._formKey.currentState!.value
};
await widget.onSubmit(widget.fromJson(mergedJson));
await widget.submitButtonConfig.onSubmit(widget.fromJsonT(mergedJson));
Navigator.pop(context);
} on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = errorMessages);

View File

@@ -3,18 +3,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/util.dart';
@@ -59,17 +57,17 @@ class _HomePageState extends State<HomePage> {
MultiBlocProvider(
providers: [
BlocProvider.value(
value: getIt<DocumentsCubit>(),
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
),
],
child: const DocumentsPage(),
),
BlocProvider.value(
value: getIt<DocumentScannerCubit>(),
value: DocumentScannerCubit(),
child: const ScannerPage(),
),
BlocProvider.value(
value: getIt<DocumentsCubit>(),
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
child: const LabelsPage(),
),
][_currentIndex],
@@ -78,20 +76,17 @@ class _HomePageState extends State<HomePage> {
);
}
Future<void> _initializeData(BuildContext context) {
void _initializeData(BuildContext context) {
try {
return Future.wait([
RepositoryProvider.of<LabelRepository<Tag>>(context).findAll();
RepositoryProvider.of<LabelRepository<Correspondent>>(context).findAll();
RepositoryProvider.of<LabelRepository<DocumentType>>(context).findAll();
RepositoryProvider.of<LabelRepository<StoragePath>>(context).findAll();
RepositoryProvider.of<SavedViewRepository>(context).findAll();
BlocProvider.of<PaperlessServerInformationCubit>(context)
.updateInformtion(),
getIt<DocumentTypeCubit>().initialize(),
getIt<CorrespondentCubit>().initialize(),
getIt<TagCubit>().initialize(),
getIt<StoragePathCubit>().initialize(),
getIt<SavedViewCubit>().initialize(),
]);
.updateInformtion();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
return Future.error(error);
}
}
}

View File

@@ -3,15 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.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/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/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
@@ -188,11 +187,14 @@ class InfoDrawer extends StatelessWidget {
onTap: () {
try {
BlocProvider.of<AuthenticationCubit>(context).logout();
getIt<DocumentsCubit>().reset();
getIt<CorrespondentCubit>().reset();
getIt<DocumentTypeCubit>().reset();
getIt<TagCubit>().reset();
getIt<DocumentScannerCubit>().reset();
RepositoryProvider.of<LabelRepository<Tag>>(context).clear();
RepositoryProvider.of<LabelRepository<Correspondent>>(context)
.clear();
RepositoryProvider.of<LabelRepository<DocumentType>>(context)
.clear();
RepositoryProvider.of<LabelRepository<StoragePath>>(context)
.clear();
RepositoryProvider.of<SavedViewRepository>(context).clear();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
@@ -208,14 +210,15 @@ class InfoDrawer extends StatelessWidget {
Future<void> _onOpenInbox(BuildContext context) async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<InboxCubit>.value(
value: getIt<InboxCubit>()..loadInbox(),
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => InboxCubit(
RepositoryProvider.of<LabelRepository<Tag>>(context),
getIt<PaperlessDocumentsApi>(),
),
],
child: const InboxPage(),
),
),
maintainState: false,
),
);

View File

@@ -1,20 +1,21 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
@injectable
class InboxCubit extends Cubit<InboxState> {
final PaperlessLabelsApi _labelApi;
final LabelRepository<Tag> _tagsRepository;
final PaperlessDocumentsApi _documentsApi;
InboxCubit(this._labelApi, this._documentsApi) : super(const InboxState());
InboxCubit(this._tagsRepository, this._documentsApi)
: super(const InboxState());
///
/// Fetches inbox tag ids and loads the inbox items (documents).
///
Future<void> loadInbox() async {
final inboxTags = await _labelApi.getTags().then(
final inboxTags = await _tagsRepository.findAll().then(
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
);
if (inboxTags.isEmpty) {

View File

@@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.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/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
class InboxItem extends StatelessWidget {
@@ -49,22 +48,20 @@ class InboxItem extends StatelessWidget {
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentDetailsCubit>(
create: (context) => DocumentDetailsCubit(
builder: (_) => BlocProvider.value(
value: DocumentDetailsCubit(
getIt<PaperlessDocumentsApi>(),
document,
),
),
],
child: const DocumentDetailsPage(
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(
allowEdit: false,
isLabelClickable: false,
),
),
),
),
),
);
}
}

View File

@@ -1,33 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
class GlobalStateBlocProvider extends StatelessWidget {
final List<BlocProvider> additionalProviders;
final Widget child;
const GlobalStateBlocProvider({
super.key,
this.additionalProviders = const [],
required this.child,
});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: getIt<DocumentTypeCubit>()),
BlocProvider.value(value: getIt<CorrespondentCubit>()),
BlocProvider.value(value: getIt<TagCubit>()),
BlocProvider.value(value: getIt<StoragePathCubit>()),
BlocProvider.value(value: getIt<SavedViewCubit>()),
...additionalProviders,
],
child: child,
);
}
}

View File

@@ -1,59 +1,43 @@
import 'dart:async';
import 'package:flutter/foundation.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/labels/bloc/label_state.dart';
abstract class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final PaperlessLabelsApi labelsApi;
class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final LabelRepository<T> _repository;
LabelCubit(this.labelsApi) : super(LabelState.initial());
late StreamSubscription _subscription;
@protected
void loadFrom(Iterable<T> items) {
emit(
LabelState(
isLoaded: true,
labels: Map.fromIterable(items, key: (e) => (e as T).id!),
),
LabelCubit(this._repository) : super(LabelState.initial()) {
_subscription = _repository.labels.listen(
(update) => emit(LabelState(isLoaded: true, labels: update)),
);
}
///
/// Adds [item] to the current state. A new state is automatically pushed
/// due to the subscription to the repository, which updates the state on
/// operation.
///
Future<T> add(T item) async {
assert(item.id == null);
final addedItem = await save(item);
final newValues = {...state.labels};
newValues.putIfAbsent(addedItem.id!, () => addedItem);
emit(
LabelState(
isLoaded: true,
labels: newValues,
),
);
final addedItem = await _repository.create(item);
return addedItem;
}
Future<T> replace(T item) async {
assert(item.id != null);
final updatedItem = await update(item);
final updatedValues = {...state.labels};
updatedValues[item.id!] = updatedItem;
emit(
LabelState(
isLoaded: state.isLoaded,
labels: updatedValues,
),
);
final updatedItem = await _repository.update(item);
return updatedItem;
}
Future<void> remove(T item) async {
assert(item.id != null);
if (state.labels.containsKey(item.id)) {
final deletedId = await delete(item);
final updatedValues = {...state.labels}..remove(deletedId);
emit(
LabelState(isLoaded: true, labels: updatedValues),
);
await _repository.delete(item);
}
}
@@ -61,14 +45,9 @@ abstract class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
emit(LabelState(isLoaded: false, labels: {}));
}
Future<void> initialize();
@protected
Future<T> save(T item);
@protected
Future<T> update(T item);
@protected
Future<int> delete(T item);
@override
Future<void> close() {
_subscription.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,20 @@
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/labels/bloc/label_cubit.dart';
class CorrespondentBlocProvider extends StatelessWidget {
final Widget child;
const CorrespondentBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
child: child,
);
}
}

View File

@@ -0,0 +1,20 @@
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/labels/bloc/label_cubit.dart';
class DocumentTypeBlocProvider extends StatelessWidget {
final Widget child;
const DocumentTypeBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: child,
);
}
}

View File

@@ -0,0 +1,39 @@
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/labels/bloc/label_cubit.dart';
class LabelsBlocProvider extends StatelessWidget {
final Widget child;
const LabelsBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<LabelCubit<StoragePath>>(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
),
BlocProvider<LabelCubit<Correspondent>>(
create: (context) => LabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
),
BlocProvider<LabelCubit<DocumentType>>(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
),
BlocProvider<LabelCubit<Tag>>(
create: (context) => LabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
),
],
child: child,
);
}
}

View File

@@ -0,0 +1,20 @@
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/labels/bloc/label_cubit.dart';
class StoragePathBlocProvider extends StatelessWidget {
final Widget child;
const StoragePathBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: child,
);
}
}

View File

@@ -0,0 +1,20 @@
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/labels/bloc/label_cubit.dart';
class TagBlocProvider extends StatelessWidget {
final Widget child;
const TagBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
child: child,
);
}
}

View File

@@ -1,26 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart';
@prod
@test
@lazySingleton
class CorrespondentCubit extends LabelCubit<Correspondent> {
CorrespondentCubit(super.metaDataService);
@override
Future<void> initialize() async {
return labelsApi.getCorrespondents().then(loadFrom);
}
@override
Future<Correspondent> save(Correspondent item) =>
labelsApi.saveCorrespondent(item);
@override
Future<Correspondent> update(Correspondent item) =>
labelsApi.updateCorrespondent(item);
@override
Future<int> delete(Correspondent item) => labelsApi.deleteCorrespondent(item);
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddCorrespondentPage extends StatelessWidget {
final String? initalValue;
const AddCorrespondentPage({Key? key, this.initalValue}) : super(key: key);
@override
Widget build(BuildContext context) {
return AddLabelPage<Correspondent>(
addLabelStr: S.of(context).addCorrespondentPageTitle,
fromJson: Correspondent.fromJson,
cubit: BlocProvider.of<CorrespondentCubit>(context),
initialName: initalValue,
);
}
}

View File

@@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart';
import 'package:paperless_mobile/util.dart';
class EditCorrespondentPage extends StatelessWidget {
final Correspondent correspondent;
const EditCorrespondentPage({super.key, required this.correspondent});
@override
Widget build(BuildContext context) {
return EditLabelPage<Correspondent>(
label: correspondent,
onSubmit: BlocProvider.of<CorrespondentCubit>(context).replace,
onDelete: (correspondent) => _onDelete(context, correspondent),
fromJson: Correspondent.fromJson,
);
}
Future<void> _onDelete(
BuildContext context,
Correspondent correspondent,
) async {
try {
await BlocProvider.of<CorrespondentCubit>(context).remove(correspondent);
final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.correspondent.id == correspondent.id) {
await cubit.updateCurrentFilter(
(filter) => filter.copyWith(
correspondent: const CorrespondentQuery.unset(),
),
);
}
Navigator.pop(context);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -2,7 +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/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart';
@@ -22,9 +23,11 @@ class CorrespondentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
return CorrespondentBlocProvider(
child: AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
child:
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addCorrespondentToFilter(context),
@@ -39,6 +42,7 @@ class CorrespondentWidget extends StatelessWidget {
);
},
),
),
);
}

View File

@@ -1,26 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart';
@prod
@test
@lazySingleton
class DocumentTypeCubit extends LabelCubit<DocumentType> {
DocumentTypeCubit(super.metaDataService);
@override
Future<void> initialize() async {
labelsApi.getDocumentTypes().then(loadFrom);
}
@override
Future<DocumentType> save(DocumentType item) =>
labelsApi.saveDocumentType(item);
@override
Future<DocumentType> update(DocumentType item) =>
labelsApi.updateDocumentType(item);
@override
Future<int> delete(DocumentType item) => labelsApi.deleteDocumentType(item);
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddDocumentTypePage extends StatelessWidget {
final String? initialName;
const AddDocumentTypePage({Key? key, this.initialName}) : super(key: key);
@override
Widget build(BuildContext context) {
return AddLabelPage<DocumentType>(
addLabelStr: S.of(context).addDocumentTypePageTitle,
fromJson: DocumentType.fromJson,
cubit: BlocProvider.of<DocumentTypeCubit>(context),
initialName: initialName,
);
}
}

View File

@@ -1,39 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart';
import 'package:paperless_mobile/util.dart';
class EditDocumentTypePage extends StatelessWidget {
final DocumentType documentType;
const EditDocumentTypePage({super.key, required this.documentType});
@override
Widget build(BuildContext context) {
return EditLabelPage<DocumentType>(
label: documentType,
onSubmit: BlocProvider.of<DocumentTypeCubit>(context).replace,
onDelete: (docType) => _onDelete(docType, context),
fromJson: DocumentType.fromJson,
);
}
Future<void> _onDelete(DocumentType docType, BuildContext context) async {
try {
await BlocProvider.of<DocumentTypeCubit>(context).remove(docType);
final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.documentType.id == docType.id) {
cubit.updateFilter(
filter: cubit.state.filter
.copyWith(documentType: const DocumentTypeQuery.unset()),
);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} finally {
Navigator.pop(context);
}
}
}

View File

@@ -1,8 +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/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart';
@@ -19,11 +20,16 @@ class DocumentTypeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => _addDocumentTypeToFilter(context),
child: BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
child:
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return Text(
state.labels[documentTypeId]?.toString() ?? "-",
@@ -35,6 +41,7 @@ class DocumentTypeWidget extends StatelessWidget {
},
),
),
),
);
}

View File

@@ -1,25 +0,0 @@
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
@prod
@test
@lazySingleton
class StoragePathCubit extends LabelCubit<StoragePath> {
StoragePathCubit(super.metaDataService);
@override
Future<void> initialize() async {
return labelsApi.getStoragePaths().then(loadFrom);
}
@override
Future<StoragePath> save(StoragePath item) => labelsApi.saveStoragePath(item);
@override
Future<StoragePath> update(StoragePath item) =>
labelsApi.updateStoragePath(item);
@override
Future<int> delete(StoragePath item) => labelsApi.deleteStoragePath(item);
}

View File

@@ -1,26 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class AddStoragePathPage extends StatelessWidget {
final String? initalValue;
const AddStoragePathPage({Key? key, this.initalValue}) : super(key: key);
@override
Widget build(BuildContext context) {
return AddLabelPage<StoragePath>(
addLabelStr: S.of(context).addStoragePathPageTitle,
fromJson: StoragePath.fromJson,
cubit: BlocProvider.of<StoragePathCubit>(context),
initialName: initalValue,
additionalFields: const [
StoragePathAutofillFormBuilderField(name: StoragePath.pathKey),
SizedBox(height: 120.0),
],
);
}
}

View File

@@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart';
import 'package:paperless_mobile/util.dart';
class EditStoragePathPage extends StatelessWidget {
final StoragePath storagePath;
const EditStoragePathPage({super.key, required this.storagePath});
@override
Widget build(BuildContext context) {
return EditLabelPage<StoragePath>(
label: storagePath,
onSubmit: BlocProvider.of<StoragePathCubit>(context).replace,
onDelete: (correspondent) => _onDelete(correspondent, context),
fromJson: StoragePath.fromJson,
additionalFields: [
StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey,
initialValue: storagePath.path,
),
const SizedBox(height: 120.0),
],
);
}
Future<void> _onDelete(StoragePath path, BuildContext context) async {
try {
await BlocProvider.of<StoragePathCubit>(context).remove(path);
final cubit = BlocProvider.of<DocumentsCubit>(context);
if (cubit.state.filter.storagePath.id == path.id) {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
storagePath: const StoragePathQuery.unset(),
),
);
}
Navigator.pop(context);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -1,9 +1,10 @@
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/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/util.dart';
class StoragePathWidget extends StatelessWidget {
@@ -22,9 +23,13 @@ class StoragePathWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
child: BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addStoragePathToFilter(context),
@@ -39,6 +44,7 @@ class StoragePathWidget extends StatelessWidget {
);
},
),
),
);
}

View File

@@ -1,24 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart';
@prod
@test
@lazySingleton
class TagCubit extends LabelCubit<Tag> {
TagCubit(super.metaDataService);
@override
Future<void> initialize() async {
return labelsApi.getTags().then(loadFrom);
}
@override
Future<Tag> save(Tag item) => labelsApi.saveTag(item);
@override
Future<Tag> update(Tag item) => labelsApi.updateTag(item);
@override
Future<int> delete(Tag item) => labelsApi.deleteTag(item);
}

View File

@@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
class AddTagPage extends StatelessWidget {
final String? initialValue;
const AddTagPage({Key? key, this.initialValue}) : super(key: key);
@override
Widget build(BuildContext context) {
return AddLabelPage<Tag>(
addLabelStr: S.of(context).addTagPageTitle,
fromJson: Tag.fromJson,
cubit: BlocProvider.of<TagCubit>(context),
initialName: initialValue,
additionalFields: [
FormBuilderColorPickerField(
name: Tag.colorKey,
valueTransformer: (color) => "#${color?.value.toRadixString(16)}",
decoration: InputDecoration(
label: Text(S.of(context).tagColorPropertyLabel),
),
colorPickerType: ColorPickerType.materialPicker,
initialValue: null,
),
FormBuilderCheckbox(
name: Tag.isInboxTagKey,
title: Text(S.of(context).tagInboxTagPropertyLabel),
),
],
);
}
}

View File

@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/view/pages/edit_label_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class EditTagPage extends StatelessWidget {
final Tag tag;
const EditTagPage({super.key, required this.tag});
@override
Widget build(BuildContext context) {
return EditLabelPage<Tag>(
label: tag,
onSubmit: (tag) async {
await BlocProvider.of<TagCubit>(context).replace(tag);
},
onDelete: (tag) => _onDelete(tag, context),
fromJson: Tag.fromJson,
additionalFields: [
FormBuilderColorPickerField(
initialValue: tag.color,
name: Tag.colorKey,
decoration: InputDecoration(
label: Text(S.of(context).tagColorPropertyLabel),
),
colorPickerType: ColorPickerType.blockPicker,
),
FormBuilderCheckbox(
initialValue: tag.isInboxTag,
name: Tag.isInboxTagKey,
title: Text(S.of(context).tagInboxTagPropertyLabel),
),
],
);
}
Future<void> _onDelete(Tag tag, BuildContext context) async {
try {
await BlocProvider.of<TagCubit>(context).remove(tag);
final cubit = BlocProvider.of<DocumentsCubit>(context);
final currentFilter = cubit.state.filter;
late DocumentFilter updatedFilter = currentFilter;
if (currentFilter.tags is IdsTagsQuery) {
if ((currentFilter.tags as IdsTagsQuery).includedIds.contains(tag.id)) {
updatedFilter = currentFilter.copyWith(
tags: (currentFilter.tags as IdsTagsQuery).withIdsRemoved(
[tag.id!],
),
);
}
}
cubit.updateFilter(filter: updatedFilter);
Navigator.pop(context);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -3,9 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package: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/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class TagFormField extends StatefulWidget {
@@ -41,11 +44,13 @@ class _TagFormFieldState extends State<TagFormField> {
@override
void initState() {
super.initState();
final state = BlocProvider.of<TagCubit>(context).state;
_textEditingController = TextEditingController()
..addListener(() {
setState(() {
_showCreationSuffixIcon = state.labels.values
_showCreationSuffixIcon = BlocProvider.of<LabelCubit<Tag>>(context)
.state
.labels
.values
.where(
(item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(),
@@ -61,7 +66,8 @@ class _TagFormFieldState extends State<TagFormField> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TagCubit, LabelState<Tag>>(
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, tagState) {
return FormBuilderField<TagsQuery>(
builder: (field) {
@@ -89,7 +95,9 @@ class _TagFormFieldState extends State<TagFormField> {
.toList();
if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery).ids.contains(element));
(field.value as IdsTagsQuery)
.ids
.contains(element));
}
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
@@ -122,7 +130,8 @@ class _TagFormFieldState extends State<TagFormField> {
title: Text(
tag.name,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground),
color:
Theme.of(context).colorScheme.onBackground),
),
);
},
@@ -172,6 +181,7 @@ class _TagFormFieldState extends State<TagFormField> {
name: widget.name,
);
},
),
);
}
@@ -199,8 +209,8 @@ class _TagFormFieldState extends State<TagFormField> {
void _onAddTag(BuildContext context, FormFieldState<TagsQuery> field) async {
final Tag? tag = await Navigator.of(context).push<Tag>(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<TagCubit>(context),
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
child: AddTagPage(initialValue: _textEditingController.text),
),
),

View File

@@ -1,8 +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/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatefulWidget {
@@ -30,7 +31,8 @@ class TagsWidget extends StatefulWidget {
class _TagsWidgetState extends State<TagsWidget> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TagCubit, LabelState<Tag>>(
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) {
final children = widget.tagIds
.where((id) => state.labels.containsKey(id))
@@ -60,6 +62,7 @@ class _TagsWidgetState extends State<TagsWidget> {
);
}
},
),
);
}
}

View File

@@ -1,112 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class AddLabelPage<T extends Label> extends StatefulWidget {
final String? initialName;
final String addLabelStr;
final T Function(Map<String, dynamic> json) fromJson;
final LabelCubit<T> cubit;
final List<Widget> additionalFields;
const AddLabelPage({
Key? key,
this.initialName,
required this.addLabelStr,
required this.fromJson,
required this.cubit,
this.additionalFields = const [],
}) : super(key: key);
@override
State<AddLabelPage> createState() => _AddLabelPageState<T>();
}
class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
final _formKey = GlobalKey<FormBuilderState>();
PaperlessValidationErrors _errors = {};
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(widget.addLabelStr),
),
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(S.of(context).genericActionCreateLabel),
onPressed: _onSubmit,
),
),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
FormBuilderTextField(
autovalidateMode: AutovalidateMode.onUserInteraction,
name: Label.nameKey,
decoration: InputDecoration(
labelText: S.of(context).labelNamePropertyLabel,
errorText: _errors[Label.nameKey],
),
initialValue: widget.initialName,
validator: FormBuilderValidators.required(),
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderTextField(
autovalidateMode: AutovalidateMode.onUserInteraction,
name: Label.matchKey,
decoration: InputDecoration(
labelText: S.of(context).labelMatchPropertyLabel,
),
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderDropdown<int?>(
name: Label.matchingAlgorithmKey,
initialValue: MatchingAlgorithm.anyWord.value,
decoration: InputDecoration(
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
errorText: _errors[Label.matchingAlgorithmKey],
),
onChanged: (val) => setState(() => _errors = {}),
items: MatchingAlgorithm.values
.map((algo) => DropdownMenuItem<int?>(
child: Text(algo.name), //TODO: INTL
value: algo.value))
.toList(),
),
FormBuilderCheckbox(
name: Label.isInsensitiveKey,
initialValue: true,
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
),
...widget.additionalFields,
].padded(),
),
),
);
}
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
try {
final label = await widget.cubit
.add(widget.fromJson(_formKey.currentState!.value));
Navigator.pop(context, label);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (json) {
setState(() => _errors = json);
}
}
}
}

View File

@@ -1,22 +1,17 @@
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/core/repository/label_repository.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/edit_label/view/impl/add_tag_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_type_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/edit_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/edit_document_type_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/pages/edit_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/edit_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -35,10 +30,6 @@ class _LabelsPageState extends State<LabelsPage>
@override
void initState() {
super.initState();
BlocProvider.of<CorrespondentCubit>(context).initialize();
BlocProvider.of<DocumentTypeCubit>(context).initialize();
BlocProvider.of<TagCubit>(context).initialize();
_tabController = TabController(length: 4, vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index));
}
@@ -60,7 +51,12 @@ class _LabelsPageState extends State<LabelsPage>
),
actions: [
IconButton(
onPressed: _onAddPressed,
onPressed: [
_openAddCorrespondentPage,
_openAddDocumentTypePage,
_openAddTagPage,
_openAddStoragePathPage,
][_currentIndex],
icon: const Icon(Icons.add),
)
],
@@ -104,40 +100,52 @@ class _LabelsPageState extends State<LabelsPage>
body: TabBarView(
controller: _tabController,
children: [
LabelTabView<Correspondent>(
cubit: BlocProvider.of<CorrespondentCubit>(context),
BlocProvider(
create: (context) => LabelCubit(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
child: LabelTabView<Correspondent>(
filterBuilder: (label) => DocumentFilter(
correspondent: CorrespondentQuery.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onOpenEditPage: _openEditCorrespondentPage,
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel:
S.of(context).labelsPageCorrespondentEmptyStateAddNewLabel,
emptyStateDescription: S
.of(context)
.labelsPageCorrespondentEmptyStateDescriptionText,
onOpenAddNewPage: _onAddPressed,
onAddNew: _openAddCorrespondentPage,
),
LabelTabView<DocumentType>(
cubit: BlocProvider.of<DocumentTypeCubit>(context),
),
BlocProvider(
create: (context) => LabelCubit(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: LabelTabView<DocumentType>(
filterBuilder: (label) => DocumentFilter(
documentType: DocumentTypeQuery.fromId(label.id),
pageSize: label.documentCount ?? 0,
),
onOpenEditPage: _openEditDocumentTypePage,
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel:
S.of(context).labelsPageDocumentTypeEmptyStateAddNewLabel,
emptyStateDescription:
S.of(context).labelsPageDocumentTypeEmptyStateDescriptionText,
onOpenAddNewPage: _onAddPressed,
emptyStateDescription: S
.of(context)
.labelsPageDocumentTypeEmptyStateDescriptionText,
onAddNew: _openAddDocumentTypePage,
),
LabelTabView<Tag>(
cubit: BlocProvider.of<TagCubit>(context),
),
BlocProvider(
create: (context) => LabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
child: LabelTabView<Tag>(
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0,
),
onOpenEditPage: _openEditTagPage,
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag ?? false
@@ -152,11 +160,15 @@ class _LabelsPageState extends State<LabelsPage>
S.of(context).labelsPageTagsEmptyStateAddNewLabel,
emptyStateDescription:
S.of(context).labelsPageTagsEmptyStateDescriptionText,
onOpenAddNewPage: _onAddPressed,
onAddNew: _openAddTagPage,
),
LabelTabView<StoragePath>(
cubit: BlocProvider.of<StoragePathCubit>(context),
onOpenEditPage: _openEditStoragePathPage,
),
BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: LabelTabView<StoragePath>(
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath: StoragePathQuery.fromId(label.id),
pageSize: label.documentCount ?? 0,
@@ -164,9 +176,11 @@ class _LabelsPageState extends State<LabelsPage>
contentBuilder: (path) => Text(path.path ?? ""),
emptyStateActionButtonLabel:
S.of(context).labelsPageStoragePathEmptyStateAddNewLabel,
emptyStateDescription:
S.of(context).labelsPageStoragePathEmptyStateDescriptionText,
onOpenAddNewPage: _onAddPressed,
emptyStateDescription: S
.of(context)
.labelsPageStoragePathEmptyStateDescriptionText,
onAddNew: _openAddStoragePathPage,
),
),
],
),
@@ -178,12 +192,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentsCubit>.value(
value: BlocProvider.of<DocumentsCubit>(context),
),
],
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(context),
child: EditCorrespondentPage(correspondent: correspondent),
),
),
@@ -194,12 +204,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentsCubit>.value(
value: BlocProvider.of<DocumentsCubit>(context),
),
],
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(context),
child: EditDocumentTypePage(documentType: docType),
),
),
@@ -210,12 +216,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentsCubit>.value(
value: BlocProvider.of<DocumentsCubit>(context),
),
],
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
child: EditTagPage(tag: tag),
),
),
@@ -226,37 +228,61 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentsCubit>.value(
value: getIt<DocumentsCubit>(),
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
child: EditStoragePathPage(
storagePath: path,
),
],
child: EditStoragePathPage(storagePath: path),
),
),
);
}
void _onAddPressed() {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
late final Widget page;
switch (_currentIndex) {
case 0:
page = const AddCorrespondentPage();
break;
case 1:
page = const AddDocumentTypePage();
break;
case 2:
page = const AddTagPage();
break;
case 3:
page = const AddStoragePathPage();
void _openAddCorrespondentPage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(context),
child: const AddCorrespondentPage(),
),
),
);
}
return GlobalStateBlocProvider(child: page);
},
));
void _openAddDocumentTypePage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(context),
child: const AddDocumentTypePage(),
),
),
);
}
void _openAddTagPage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
child: const AddTagPage(),
),
),
);
}
void _openAddStoragePathPage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
child: const AddStoragePathPage(),
),
),
);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart';
@@ -46,12 +45,11 @@ class LabelItem<T extends Label> extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<LinkedDocumentsCubit>.value(
value: getIt<LinkedDocumentsCubit>()
..initialize(filter)),
],
builder: (context) => BlocProvider.value(
value: LinkedDocumentsCubit(
getIt<PaperlessDocumentsApi>(),
filter,
),
child: const LinkedDocumentsPage(),
),
),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
@@ -9,10 +10,9 @@ import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class LabelTabView<T extends Label> extends StatelessWidget {
final LabelCubit<T> cubit;
final DocumentFilter Function(Label) filterBuilder;
final void Function(T) onOpenEditPage;
final void Function() onOpenAddNewPage;
final void Function(T) onEdit;
final void Function() onAddNew;
/// Displayed as the subtitle of the [ListTile]
final Widget Function(T)? contentBuilder;
@@ -26,13 +26,12 @@ class LabelTabView<T extends Label> extends StatelessWidget {
const LabelTabView({
super.key,
required this.cubit,
required this.filterBuilder,
this.contentBuilder,
this.leadingBuilder,
required this.onOpenEditPage,
required this.onEdit,
required this.emptyStateDescription,
required this.onOpenAddNewPage,
required this.onAddNew,
required this.emptyStateActionButtonLabel,
});
@@ -43,10 +42,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
if (state == ConnectivityState.notConnected) {
return const OfflineWidget();
}
return RefreshIndicator(
onRefresh: cubit.initialize,
child: BlocBuilder<Cubit<LabelState<T>>, LabelState<T>>(
bloc: cubit,
return BlocBuilder<LabelCubit<T>, LabelState<T>>(
builder: (context, state) {
final labels = state.labels.values.toList()..sort();
if (labels.isEmpty) {
@@ -59,7 +55,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
textAlign: TextAlign.center,
),
TextButton(
onPressed: onOpenAddNewPage,
onPressed: onAddNew,
child: Text(emptyStateActionButtonLabel),
)
].padded(),
@@ -72,7 +68,7 @@ class LabelTabView<T extends Label> extends StatelessWidget {
name: l.name,
content:
contentBuilder?.call(l) ?? Text(l.match ?? '-'),
onOpenEditPage: onOpenEditPage,
onOpenEditPage: onEdit,
filterBuilder: filterBuilder,
leading: leadingBuilder?.call(l),
label: l,
@@ -80,7 +76,6 @@ class LabelTabView<T extends Label> extends StatelessWidget {
.toList(),
);
},
),
);
},
);

View File

@@ -1,24 +1,25 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart';
@injectable
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
final PaperlessDocumentsApi _api;
LinkedDocumentsCubit(this._api) : super(LinkedDocumentsState());
LinkedDocumentsCubit(this._api, DocumentFilter filter)
: super(LinkedDocumentsState(filter: filter)) {
_initialize();
}
Future<void> initialize(DocumentFilter filter) async {
Future<void> _initialize() async {
final documents = await _api.find(
filter.copyWith(
state.filter.copyWith(
pageSize: 100,
),
);
emit(LinkedDocumentsState(
isLoaded: true,
documents: documents,
filter: filter,
filter: state.filter,
));
}
}

View File

@@ -3,10 +3,10 @@ import 'package:paperless_api/paperless_api.dart';
class LinkedDocumentsState {
final bool isLoaded;
final PagedSearchResult<DocumentModel>? documents;
final DocumentFilter? filter;
final DocumentFilter filter;
LinkedDocumentsState({
this.filter,
required this.filter,
this.isLoaded = false,
this.documents,
});

View File

@@ -7,7 +7,6 @@ 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';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -64,15 +63,12 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (ctxt) => GlobalStateBlocProvider(
additionalProviders: [
builder: (context) =>
BlocProvider<DocumentDetailsCubit>.value(
value: DocumentDetailsCubit(
getIt<PaperlessDocumentsApi>(),
document,
),
),
],
child: const DocumentDetailsPage(
isLabelClickable: false,
allowEdit: false,

View File

@@ -1,21 +1,27 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
@prod
@test
@lazySingleton
class SavedViewCubit extends Cubit<SavedViewState> {
final PaperlessSavedViewsApi _api;
SavedViewCubit(this._api) : super(SavedViewState(value: {}));
final SavedViewRepository _repository;
StreamSubscription? _subscription;
SavedViewCubit(this._repository) : super(SavedViewState(value: {})) {
_subscription = _repository.savedViews.listen(
(savedViews) => emit(state.copyWith(value: savedViews)),
);
}
void selectView(SavedView? view) {
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
}
Future<SavedView> add(SavedView view) async {
final savedView = await _api.save(view);
final savedView = await _repository.create(view);
emit(
SavedViewState(
value: {...state.value, savedView.id!: savedView},
@@ -26,22 +32,15 @@ class SavedViewCubit extends Cubit<SavedViewState> {
}
Future<int> remove(SavedView view) async {
final id = await _api.delete(view);
final newValue = {...state.value};
newValue.removeWhere((key, value) => key == id);
emit(
SavedViewState(
value: newValue,
selectedSavedViewId: view.id == state.selectedSavedViewId
? null
: state.selectedSavedViewId,
),
);
final id = await _repository.delete(view);
if (state.selectedSavedViewId == id) {
resetSelection();
}
return id;
}
Future<void> initialize() async {
final views = await _api.getAll();
final views = await _repository.findAll();
final values = {for (var element in views) element.id!: element};
emit(SavedViewState(value: values));
}
@@ -49,4 +48,10 @@ class SavedViewCubit extends Cubit<SavedViewState> {
void resetSelection() {
emit(SavedViewState(value: state.value));
}
@override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
}

View File

@@ -15,4 +15,17 @@ class SavedViewState with EquatableMixin {
value,
selectedSavedViewId,
];
SavedViewState copyWith({
Map<int, SavedView>? value,
int? selectedSavedViewId,
bool overwriteSelectedSavedViewId = false,
}) {
return SavedViewState(
value: value ?? this.value,
selectedSavedViewId: overwriteSelectedSavedViewId
? selectedSavedViewId
: this.selectedSavedViewId,
);
}
}

View File

@@ -5,8 +5,8 @@ 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/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
@@ -94,18 +94,11 @@ class SavedViewSelectionWidget extends StatelessWidget {
void _onSelected(
bool isSelected, BuildContext context, SavedView view) async {
try {
if (isSelected) {
BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: view.toDocumentFilter());
BlocProvider.of<SavedViewCubit>(context).selectView(view);
} else {
BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).selectView(null);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
void _onDelete(BuildContext context, SavedView view) async {

View File

@@ -9,13 +9,8 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
@injectable
class DocumentScannerCubit extends Cubit<List<File>> {
final PaperlessDocumentsApi _api;
static List<File> initialState = [];
DocumentScannerCubit(this._api) : super(initialState);
DocumentScannerCubit() : super(const []);
void addScan(File file) => emit([...state, file]);
@@ -39,41 +34,9 @@ class DocumentScannerCubit extends Cubit<List<File>> {
}
}
imageCache.clear();
emit(initialState);
emit([]);
} catch (_) {
throw const PaperlessServerException(ErrorCode.scanRemoveFailed);
}
}
Future<void> uploadDocument(
Uint8List bytes,
String fileName, {
required String title,
required void Function(DocumentModel document)? onConsumptionFinished,
int? documentType,
int? correspondent,
Iterable<int> tags = const [],
DateTime? createdAt,
}) async {
final auth = getIt<AuthenticationCubit>().state.authentication;
if (auth == null) {
throw const PaperlessServerException(ErrorCode.notAuthenticated);
}
await _api.create(
bytes,
filename: fileName,
title: title,
documentType: documentType,
correspondent: correspondent,
tags: tags,
createdAt: createdAt,
authToken: auth.token!,
serverUrl: auth.serverUrl,
);
if (onConsumptionFinished != null) {
_api
.waitForConsumptionFinished(fileName, title)
.then((value) => onConsumptionFinished(value));
}
}
}

View File

@@ -1,275 +0,0 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.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/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
class DocumentUploadPage extends StatefulWidget {
final Uint8List fileBytes;
final String? title;
final String? filename;
final void Function()? afterUpload;
final void Function(DocumentModel)? onSuccessfullyConsumed;
const DocumentUploadPage({
Key? key,
required this.fileBytes,
this.afterUpload,
this.title,
this.filename,
this.onSuccessfullyConsumed,
}) : super(key: key);
@override
State<DocumentUploadPage> createState() => _DocumentUploadPageState();
}
class _DocumentUploadPageState extends State<DocumentUploadPage> {
static const fkFileName = "filename";
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
PaperlessValidationErrors _errors = {};
bool _isUploadLoading = false;
late bool _syncTitleAndFilename;
final _now = DateTime.now();
@override
void initState() {
super.initState();
_syncTitleAndFilename = widget.filename == null && widget.title == null;
initializeDateFormatting();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(S.of(context).documentsUploadPageTitle),
bottom: _isUploadLoading
? const PreferredSize(
child: LinearProgressIndicator(),
preferredSize: Size.fromHeight(4.0))
: null,
),
floatingActionButton: Visibility(
visible: MediaQuery.of(context).viewInsets.bottom == 0,
child: FloatingActionButton.extended(
onPressed: _onSubmit,
label: Text(S.of(context).genericActionUploadLabel),
icon: const Icon(Icons.upload),
),
),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
FormBuilderTextField(
autovalidateMode: AutovalidateMode.always,
name: DocumentModel.titleKey,
initialValue:
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
validator: FormBuilderValidators.required(),
decoration: InputDecoration(
labelText: S.of(context).documentTitlePropertyLabel,
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_formKey.currentState?.fields[DocumentModel.titleKey]
?.didChange("");
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]?.didChange("");
}
},
),
errorText: _errors[DocumentModel.titleKey],
),
onChanged: (value) {
final String transformedValue = _formatFilename(value ?? '');
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
}
},
),
FormBuilderTextField(
autovalidateMode: AutovalidateMode.always,
readOnly: _syncTitleAndFilename,
enabled: !_syncTitleAndFilename,
name: fkFileName,
decoration: InputDecoration(
labelText: S.of(context).documentUploadFileNameLabel,
suffixText: ".pdf",
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () =>
_formKey.currentState?.fields[fkFileName]?.didChange(''),
),
),
initialValue:
widget.filename ?? "scan_${fileNameDateFormat.format(_now)}",
),
SwitchListTile(
value: _syncTitleAndFilename,
onChanged: (value) {
setState(
() => _syncTitleAndFilename = value,
);
if (_syncTitleAndFilename) {
final String transformedValue = _formatFilename(_formKey
.currentState
?.fields[DocumentModel.titleKey]
?.value as String);
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
}
}
},
title: Text(S
.of(context)
.documentUploadPageSynchronizeTitleAndFilenameLabel), //TODO: INTL
),
FormBuilderDateTimePicker(
autovalidateMode: AutovalidateMode.always,
format: DateFormat("dd. MMMM yyyy"), //TODO: INTL
inputType: InputType.date,
name: DocumentModel.createdKey,
initialValue: null,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.calendar_month_outlined),
labelText: S.of(context).documentCreatedPropertyLabel + " *",
),
),
BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
bloc: getIt<DocumentTypeCubit>(), //TODO: Use provider
builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
BlocProvider.value(
value: BlocProvider.of<DocumentTypeCubit>(context),
child: AddDocumentTypePage(initialName: initialValue),
),
label: S.of(context).documentDocumentTypePropertyLabel + " *",
name: DocumentModel.documentTypeKey,
state: state.labels,
queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder:
DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined),
);
},
),
BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
bloc: getIt<CorrespondentCubit>(), //TODO: Use provider
builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) =>
BlocProvider.value(
value: BlocProvider.of<CorrespondentCubit>(context),
child: AddCorrespondentPage(initalValue: initialValue),
),
label:
S.of(context).documentCorrespondentPropertyLabel + " *",
name: DocumentModel.correspondentKey,
state: state.labels,
queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder:
CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outline),
);
},
),
const TagFormField(
name: DocumentModel.tagsKey,
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
//Label: "Tags" + " *",
),
Text(
"* " + S.of(context).uploadPageAutomaticallInferredFieldsHintText,
style: Theme.of(context).textTheme.caption,
),
].padded(),
),
),
);
}
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final cubit = BlocProvider.of<DocumentScannerCubit>(context);
try {
setState(() => _isUploadLoading = true);
final fv = _formKey.currentState!.value;
final createdAt = fv[DocumentModel.createdKey] as DateTime?;
final title = fv[DocumentModel.titleKey] as String;
final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter;
final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
final correspondent =
fv[DocumentModel.correspondentKey] as IdQueryParameter;
await cubit.uploadDocument(
widget.fileBytes,
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
onConsumptionFinished: widget.onSuccessfullyConsumed,
title: title,
documentType: docType.id,
correspondent: correspondent.id,
tags: tags.ids,
createdAt: createdAt,
);
cubit.reset(); //TODO: Access via provider
showSnackBar(context, S.of(context).documentUploadSuccessText);
Navigator.pop(context);
widget.afterUpload?.call();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (PaperlessServerExceptions) {
setState(() => _errors = PaperlessServerExceptions);
} catch (unknownError, stackTrace) {
showErrorMessage(
context, const PaperlessServerException.unknown(), stackTrace);
} finally {
setState(() {
_isUploadLoading = false;
});
}
}
}
String _padWithPdfExtension(String source) {
return source.endsWith(".pdf") ? source : '$source.pdf';
}
String _formatFilename(String source) {
return source.replaceAll(RegExp(r"[\W_]"), "_");
}
}

View File

@@ -9,13 +9,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/core/global/constants.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/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/document_upload_page.dart';
import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
@@ -124,17 +128,29 @@ class _ScannerPageState extends State<ScannerPage>
final bytes = await doc.save();
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentScannerCubit>.value(
value: BlocProvider.of<DocumentScannerCubit>(context),
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
],
child: DocumentUploadPage(
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
),
),
),
),
);
}
@@ -243,15 +259,27 @@ class _ScannerPageState extends State<ScannerPage>
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider<DocumentScannerCubit>.value(
value: BlocProvider.of<DocumentScannerCubit>(context),
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
],
child: DocumentUploadPage(
filename: filename,
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
child: DocumentUploadPreparationPage(
fileBytes: fileBytes,
filename: filename,
),
),
),
),

View File

@@ -13,26 +13,31 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/storage_path_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/document_upload_page.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
void main() async {
Bloc.observer = BlocChangesObserver();
@@ -52,7 +57,30 @@ void main() async {
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
runApp(const PaperlessMobileEntrypoint());
// Create repositories
final LabelRepository<Tag> tagRepository =
TagRepositoryImpl(getIt<PaperlessLabelsApi>());
final LabelRepository<Correspondent> correspondentRepository =
CorrespondentRepositoryImpl(getIt<PaperlessLabelsApi>());
final LabelRepository<DocumentType> documentTypeRepository =
DocumentTypeRepositoryImpl(getIt<PaperlessLabelsApi>());
final LabelRepository<StoragePath> storagePathRepository =
StoragePathRepositoryImpl(getIt<PaperlessLabelsApi>());
final SavedViewRepository savedViewRepository =
SavedViewRepositoryImpl(getIt<PaperlessSavedViewsApi>());
runApp(
MultiRepositoryProvider(
providers: [
RepositoryProvider.value(value: tagRepository),
RepositoryProvider.value(value: correspondentRepository),
RepositoryProvider.value(value: documentTypeRepository),
RepositoryProvider.value(value: storagePathRepository),
RepositoryProvider.value(value: savedViewRepository),
],
child: const PaperlessMobileEntrypoint(),
),
);
}
class PaperlessMobileEntrypoint extends StatefulWidget {
@@ -71,9 +99,6 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
BlocProvider<ConnectivityCubit>.value(
value: getIt<ConnectivityCubit>(),
),
BlocProvider<AuthenticationCubit>.value(
value: getIt<AuthenticationCubit>(),
),
BlocProvider<PaperlessServerInformationCubit>.value(
value: getIt<PaperlessServerInformationCubit>(),
),
@@ -126,7 +151,10 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
],
home: const AuthenticationWrapper(),
home: BlocProvider<AuthenticationCubit>.value(
value: getIt<AuthenticationCubit>(),
child: const AuthenticationWrapper(),
),
);
},
),
@@ -177,21 +205,21 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
}
final filename = extractFilenameFromPath(file.path);
final bytes = File(file.path).readAsBytesSync();
Navigator.push(
final success = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GlobalStateBlocProvider(
additionalProviders: [
BlocProvider.value(value: getIt<DocumentScannerCubit>()),
],
child: DocumentUploadPage(
builder: (context) => BlocProvider.value(
value: getIt<DocumentScannerCubit>(),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
afterUpload: SystemNavigator.pop,
filename: filename,
),
),
),
);
if (success) {
SystemNavigator.pop();
}
}
@override
@@ -232,17 +260,12 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
},
builder: (context, authentication) {
if (authentication.isAuthenticated) {
return GlobalStateBlocProvider(
additionalProviders: [
BlocProvider.value(value: getIt<DocumentsCubit>()),
],
child: const HomePage(),
);
return const HomePage();
} else {
if (authentication.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return BiometricAuthenticationPage();
}
// if (authentication.wasLoginStored &&
// !(authentication.wasLocalAuthenticationSuccessful ?? false)) {
// return const BiometricAuthenticationPage();
// }
return const LoginPage();
}
},

View File

@@ -14,25 +14,25 @@ import 'package:paperless_api/src/models/labels/tag_model.dart';
///
abstract class PaperlessLabelsApi {
Future<Correspondent?> getCorrespondent(int id);
Future<List<Correspondent>> getCorrespondents();
Future<List<Correspondent>> getCorrespondents([Iterable<int>? ids]);
Future<Correspondent> saveCorrespondent(Correspondent correspondent);
Future<Correspondent> updateCorrespondent(Correspondent correspondent);
Future<int> deleteCorrespondent(Correspondent correspondent);
Future<Tag?> getTag(int id);
Future<List<Tag>> getTags({List<int>? ids});
Future<List<Tag>> getTags([Iterable<int>? ids]);
Future<Tag> saveTag(Tag tag);
Future<Tag> updateTag(Tag tag);
Future<int> deleteTag(Tag tag);
Future<DocumentType?> getDocumentType(int id);
Future<List<DocumentType>> getDocumentTypes();
Future<List<DocumentType>> getDocumentTypes([Iterable<int>? ids]);
Future<DocumentType> saveDocumentType(DocumentType type);
Future<DocumentType> updateDocumentType(DocumentType documentType);
Future<int> deleteDocumentType(DocumentType documentType);
Future<StoragePath?> getStoragePath(int id);
Future<List<StoragePath>> getStoragePaths();
Future<List<StoragePath>> getStoragePaths([Iterable<int>? ids]);
Future<StoragePath> saveStoragePath(StoragePath path);
Future<StoragePath> updateStoragePath(StoragePath path);
Future<int> deleteStoragePath(StoragePath path);

View File

@@ -35,7 +35,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
}
@override
Future<List<Tag>> getTags({List<int>? ids}) async {
Future<List<Tag>> getTags([Iterable<int>? ids]) async {
final results = await getCollection(
"/api/tags/?page=1&page_size=100000",
Tag.fromJson,
@@ -59,23 +59,31 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
}
@override
Future<List<Correspondent>> getCorrespondents() {
return getCollection(
Future<List<Correspondent>> getCorrespondents([Iterable<int>? ids]) async {
final results = await getCollection(
"/api/correspondents/?page=1&page_size=100000",
Correspondent.fromJson,
ErrorCode.correspondentLoadFailed,
client: client,
);
return results
.where((element) => ids?.contains(element.id) ?? true)
.toList();
}
@override
Future<List<DocumentType>> getDocumentTypes() {
return getCollection(
Future<List<DocumentType>> getDocumentTypes([Iterable<int>? ids]) async {
final results = await getCollection(
"/api/document_types/?page=1&page_size=100000",
DocumentType.fromJson,
ErrorCode.documentTypeLoadFailed,
client: client,
);
return results
.where((element) => ids?.contains(element.id) ?? true)
.toList();
}
@override
@@ -261,13 +269,17 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
}
@override
Future<List<StoragePath>> getStoragePaths() {
return getCollection(
Future<List<StoragePath>> getStoragePaths([Iterable<int>? ids]) async {
final results = await getCollection(
"/api/storage_paths/?page=1&page_size=100000",
StoragePath.fromJson,
ErrorCode.storagePathLoadFailed,
client: client,
);
return results
.where((element) => ids?.contains(element.id) ?? true)
.toList();
}
@override

View File

@@ -1,7 +1,8 @@
import 'package:paperless_api/src/models/saved_view_model.dart';
abstract class PaperlessSavedViewsApi {
Future<List<SavedView>> getAll();
Future<SavedView> find(int id);
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]);
Future<SavedView> save(SavedView view);
Future<int> delete(SavedView view);

View File

@@ -14,13 +14,15 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
PaperlessSavedViewsApiImpl(this.client);
@override
Future<List<SavedView>> getAll() {
return getCollection(
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final result = await getCollection(
"/api/saved_views/",
SavedView.fromJson,
ErrorCode.loadSavedViewsError,
client: client,
);
return result.where((view) => ids?.contains(view.id!) ?? true);
}
@override
@@ -51,4 +53,14 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
httpStatusCode: response.statusCode,
);
}
@override
Future<SavedView> find(int id) {
return getSingleResult(
"/api/saved_views/$id/",
SavedView.fromJson,
ErrorCode.loadSavedViewsError,
client: client,
);
}
}

View File

@@ -1147,12 +1147,12 @@ packages:
source: hosted
version: "1.4.5"
rxdart:
dependency: transitive
dependency: "direct main"
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.4"
version: "0.27.7"
share_plus:
dependency: "direct main"
description:

View File

@@ -81,6 +81,7 @@ dependencies:
paperless_api:
path: packages/paperless_api
hive: ^2.2.3
rxdart: ^0.27.7
dev_dependencies:
integration_test: