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/widgets.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:paperless_mobile/di_initializer.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:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
class BlocChangesObserver extends BlocObserver { class BlocChangesObserver extends BlocObserver {
@override @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/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart'; import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -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/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart'; import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/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/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart'; import 'package:paperless_mobile/features/labels/document_type/view/widgets/document_type_widget.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_widget.dart';
@@ -213,7 +213,29 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Navigator.push<bool>( Navigator.push<bool>(
context, context,
MaterialPageRoute( 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( child: DocumentEditPage(
document: cubit.state.document!, document: cubit.state.document!,
onEdit: (updatedDocument) { 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:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
@prod part 'documents_state.dart';
@test
@lazySingleton
class DocumentsCubit extends Cubit<DocumentsState> { class DocumentsCubit extends Cubit<DocumentsState> {
final PaperlessDocumentsApi _api; final PaperlessDocumentsApi _api;

View File

@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart'; part of 'documents_cubit.dart';
import 'package:paperless_api/paperless_api.dart';
class DocumentsState extends Equatable { class DocumentsState extends Equatable {
final bool isLoaded; 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:form_builder_validators/form_builder_validators.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart'; import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_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_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/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/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -57,10 +57,133 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return LabelsBlocProvider(
child: Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended( 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) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final values = _formKey.currentState!.value; final values = _formKey.currentState!.value;
var updatedDocument = widget.document.copyWith( var updatedDocument = widget.document.copyWith(
@@ -91,111 +214,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
Navigator.pop(context); 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() { 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart'; import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart'; import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart'; import 'package:paperless_mobile/features/documents/view/widgets/list/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/selection/documents_page_app_bar.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart'; import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.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/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -132,9 +132,20 @@ class _DocumentsPageState extends State<DocumentsPage> {
), ),
body: _buildBody(connectivityState), body: _buildBody(connectivityState),
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
panelBuilder: (scrollController) => DocumentFilterPanel( panelBuilder: (scrollController) =>
BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return LabelsBlocProvider(
child: DocumentFilterPanel(
panelController: _filterPanelController, panelController: _filterPanelController,
scrollController: scrollController, 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, onRefresh: _onRefresh,
child: CustomScrollView( child: CustomScrollView(
slivers: [ 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: [ actions: [
const SortDocumentsButton(), const SortDocumentsButton(),
IconButton( IconButton(
@@ -202,12 +235,15 @@ class _DocumentsPageState extends State<DocumentsPage> {
: Icons.grid_view, : Icons.grid_view,
), ),
onPressed: () => onPressed: () =>
BlocProvider.of<ApplicationSettingsCubit>(context) BlocProvider.of<ApplicationSettingsCubit>(
context)
.setViewType( .setViewType(
settings.preferredViewType.toggle()), settings.preferredViewType.toggle()),
), ),
], ],
), ),
),
),
child, child,
SliverToBoxAdapter( SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
@@ -233,29 +269,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute( MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
DocumentModel document) { DocumentModel document) {
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => MultiBlocProvider( builder: (_) => BlocProvider.value(
providers: [ value: DocumentDetailsCubit(getIt<PaperlessDocumentsApi>(), document),
BlocProvider.value( child: const LabelRepositoriesProvider(
value: BlocProvider.of<DocumentsCubit>(context), 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/core/widgets/empty_state.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class DocumentsEmptyState extends StatelessWidget { class DocumentsEmptyState extends StatelessWidget {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/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:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart'; import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/core/widgets/offline_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:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
@@ -34,7 +36,8 @@ class DocumentListView extends StatelessWidget {
builderDelegate: PagedChildBuilderDelegate( builderDelegate: PagedChildBuilderDelegate(
animateTransitions: true, animateTransitions: true,
itemBuilder: (context, document, index) { itemBuilder: (context, document, index) {
return DocumentListItem( return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable, isLabelClickable: isLabelClickable,
document: document, document: document,
onTap: onTap, onTap: onTap,
@@ -49,6 +52,7 @@ class DocumentListView extends StatelessWidget {
: false; : false;
}, },
onTagSelected: onTagSelected, onTagSelected: onTagSelected,
),
); );
}, },
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection 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_api/paperless_api.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/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/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/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/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/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/features/saved_view/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:paperless_mobile/generated/l10n.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -24,10 +21,15 @@ class DocumentFilterPanel extends StatefulWidget {
final PanelController panelController; final PanelController panelController;
final ScrollController scrollController; final ScrollController scrollController;
final DocumentFilter initialFilter;
final void Function(DocumentFilter filter) onFilterChanged;
const DocumentFilterPanel({ const DocumentFilterPanel({
Key? key, Key? key,
required this.panelController, required this.panelController,
required this.scrollController, required this.scrollController,
required this.onFilterChanged,
required this.initialFilter,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -63,13 +65,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
child: BlocConsumer<DocumentsCubit, DocumentsState>( child: FormBuilder(
listener: (context, state) {
// Set initial values, otherwise they would not automatically update.
_patchFromFilter(state.filter);
},
builder: (context, state) {
return FormBuilder(
key: _formKey, key: _formKey,
child: Column( child: Column(
children: [ children: [
@@ -81,8 +77,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: TextButton.icon( child: TextButton.icon(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
label: Text( label:
S.of(context).documentsFilterPageResetFilterLabel), Text(S.of(context).documentsFilterPageResetFilterLabel),
onPressed: () => _resetFilter(context), onPressed: () => _resetFilter(context),
), ),
), ),
@@ -100,8 +96,8 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
TextButton( TextButton(
onPressed: _onApplyFilter, onPressed: _onApplyFilter,
child: Text( child:
S.of(context).documentsFilterPageApplyFilterLabel), Text(S.of(context).documentsFilterPageApplyFilterLabel),
), ),
], ],
).padded(), ).padded(),
@@ -119,23 +115,22 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
children: [ children: [
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(S.of(context).documentsFilterPageSearchLabel),
S.of(context).documentsFilterPageSearchLabel),
).padded(const EdgeInsets.only(left: 8.0)), ).padded(const EdgeInsets.only(left: 8.0)),
_buildQueryFormField(state), _buildQueryFormField(),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child:
S.of(context).documentsFilterPageAdvancedLabel), Text(S.of(context).documentsFilterPageAdvancedLabel),
).padded(const EdgeInsets.only(left: 8.0, top: 8.0)), ).padded(const EdgeInsets.only(left: 8.0, top: 8.0)),
_buildCreatedDateRangePickerFormField(state).padded(), _buildCreatedDateRangePickerFormField().padded(),
_buildAddedDateRangePickerFormField(state).padded(), _buildAddedDateRangePickerFormField().padded(),
_buildCorrespondentFormField(state).padded(), _buildCorrespondentFormField().padded(),
_buildDocumentTypeFormField(state).padded(), _buildDocumentTypeFormField().padded(),
_buildStoragePathFormField(state).padded(), _buildStoragePathFormField().padded(),
TagFormField( TagFormField(
name: DocumentModel.tagsKey, name: DocumentModel.tagsKey,
initialValue: state.filter.tags, initialValue: widget.initialFilter.tags,
allowCreation: false, allowCreation: false,
).padded(), ).padded(),
// Required in order for the storage path field to be visible when typing // Required in order for the storage path field to be visible when typing
@@ -148,8 +143,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
], ],
), ),
);
},
), ),
); );
} }
@@ -163,15 +156,16 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
} }
} }
Widget _buildDocumentTypeFormField(DocumentsState docState) { //TODO: Check if the blocs can be found in the context, otherwise just provide repository and create new bloc inside LabelFormField!
return BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>( Widget _buildDocumentTypeFormField() {
return BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) { builder: (context, state) {
return LabelFormField<DocumentType, DocumentTypeQuery>( return LabelFormField<DocumentType, DocumentTypeQuery>(
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
name: fkDocumentType, name: fkDocumentType,
state: state.labels, state: state.labels,
label: S.of(context).documentDocumentTypePropertyLabel, label: S.of(context).documentDocumentTypePropertyLabel,
initialValue: docState.filter.documentType, initialValue: widget.initialFilter.documentType,
queryParameterIdBuilder: DocumentTypeQuery.fromId, queryParameterIdBuilder: DocumentTypeQuery.fromId,
queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned, queryParameterNotAssignedBuilder: DocumentTypeQuery.notAssigned,
prefixIcon: const Icon(Icons.description_outlined), prefixIcon: const Icon(Icons.description_outlined),
@@ -180,15 +174,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildCorrespondentFormField(DocumentsState docState) { Widget _buildCorrespondentFormField() {
return BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>( return BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) { builder: (context, state) {
return LabelFormField<Correspondent, CorrespondentQuery>( return LabelFormField<Correspondent, CorrespondentQuery>(
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
name: fkCorrespondent, name: fkCorrespondent,
state: state.labels, state: state.labels,
label: S.of(context).documentCorrespondentPropertyLabel, label: S.of(context).documentCorrespondentPropertyLabel,
initialValue: docState.filter.correspondent, initialValue: widget.initialFilter.correspondent,
queryParameterIdBuilder: CorrespondentQuery.fromId, queryParameterIdBuilder: CorrespondentQuery.fromId,
queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned, queryParameterNotAssignedBuilder: CorrespondentQuery.notAssigned,
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
@@ -197,15 +191,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildStoragePathFormField(DocumentsState docState) { Widget _buildStoragePathFormField() {
return BlocBuilder<StoragePathCubit, LabelState<StoragePath>>( return BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) { builder: (context, state) {
return LabelFormField<StoragePath, StoragePathQuery>( return LabelFormField<StoragePath, StoragePathQuery>(
formBuilderState: _formKey.currentState, formBuilderState: _formKey.currentState,
name: fkStoragePath, name: fkStoragePath,
state: state.labels, state: state.labels,
label: S.of(context).documentStoragePathPropertyLabel, label: S.of(context).documentStoragePathPropertyLabel,
initialValue: docState.filter.storagePath, initialValue: widget.initialFilter.storagePath,
queryParameterIdBuilder: StoragePathQuery.fromId, queryParameterIdBuilder: StoragePathQuery.fromId,
queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned, queryParameterNotAssignedBuilder: StoragePathQuery.notAssigned,
prefixIcon: const Icon(Icons.folder_outlined), prefixIcon: const Icon(Icons.folder_outlined),
@@ -214,7 +208,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildQueryFormField(DocumentsState state) { Widget _buildQueryFormField() {
final queryType = final queryType =
_formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ?? _formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
QueryType.titleAndContent; QueryType.titleAndContent;
@@ -239,16 +233,15 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
prefixIcon: const Icon(Icons.search_outlined), prefixIcon: const Icon(Icons.search_outlined),
labelText: label, labelText: label,
suffixIcon: QueryTypeFormField( suffixIcon: QueryTypeFormField(
initialValue: state.filter.queryType, initialValue: widget.initialFilter.queryType,
afterSelected: (queryType) => setState(() {}), afterSelected: (queryType) => setState(() {}),
), ),
), ),
initialValue: state.filter.queryText, initialValue: widget.initialFilter.queryText,
).padded(); ).padded();
} }
Widget _buildDateRangePickerHelper( Widget _buildDateRangePickerHelper(String formFieldKey) {
DocumentsState state, String formFieldKey) {
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@@ -328,13 +321,13 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildCreatedDateRangePickerFormField(DocumentsState state) { Widget _buildCreatedDateRangePickerFormField() {
return Column( return Column(
children: [ children: [
FormBuilderDateRangePicker( FormBuilderDateRangePicker(
initialValue: _dateTimeRangeOfNullable( initialValue: _dateTimeRangeOfNullable(
state.filter.createdDateAfter, widget.initialFilter.createdDateAfter,
state.filter.createdDateBefore, widget.initialFilter.createdDateBefore,
), ),
// Workaround for theme data not being correctly passed to daterangepicker, see // Workaround for theme data not being correctly passed to daterangepicker, see
// https://github.com/flutter/flutter/issues/87580 // https://github.com/flutter/flutter/issues/87580
@@ -371,18 +364,18 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
), ),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkCreatedAt), _buildDateRangePickerHelper(fkCreatedAt),
], ],
); );
} }
Widget _buildAddedDateRangePickerFormField(DocumentsState state) { Widget _buildAddedDateRangePickerFormField() {
return Column( return Column(
children: [ children: [
FormBuilderDateRangePicker( FormBuilderDateRangePicker(
initialValue: _dateTimeRangeOfNullable( initialValue: _dateTimeRangeOfNullable(
state.filter.addedDateAfter, widget.initialFilter.addedDateAfter,
state.filter.addedDateBefore, widget.initialFilter.addedDateBefore,
), ),
// Workaround for theme data not being correctly passed to daterangepicker, see // Workaround for theme data not being correctly passed to daterangepicker, see
// https://github.com/flutter/flutter/issues/87580 // https://github.com/flutter/flutter/issues/87580
@@ -419,7 +412,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
), ),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkAddedAt), _buildDateRangePickerHelper(fkAddedAt),
], ],
); );
} }

View File

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

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.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'; import 'package:paperless_mobile/generated/l10n.dart';
class BulkDeleteConfirmationDialog extends StatelessWidget { class BulkDeleteConfirmationDialog extends StatelessWidget {

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart'; import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -79,7 +80,6 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
//TODO: replace with sorting stuff...
SavedViewSelectionWidget(height: 48, enabled: enabled), SavedViewSelectionWidget(height: 48, enabled: enabled),
], ],
), ),

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
@@ -33,11 +32,19 @@ class _SortDocumentsButtonState extends State<SortDocumentsButton> {
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
), ),
builder: (context) => BlocProvider.value( builder: (context) => FractionallySizedBox(
value: getIt<DocumentsCubit>(),
child: const FractionallySizedBox(
heightFactor: .6, 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/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
class EditLabelPage<T extends Label> extends StatefulWidget { class SubmitButtonConfig<T extends Label> {
final T label; final Widget icon;
final Widget label;
final Future<void> Function(T) onSubmit; 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; final List<Widget> additionalFields;
const EditLabelPage({ const LabelForm({
Key? key, Key? key,
required this.label, required this.initialValue,
required this.fromJson, required this.fromJsonT,
required this.onSubmit,
required this.onDelete,
this.additionalFields = const [], this.additionalFields = const [],
required this.submitButtonConfig,
}) : super(key: key); }) : super(key: key);
@override @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>(); final _formKey = GlobalKey<FormBuilderState>();
PaperlessValidationErrors _errors = {}; PaperlessValidationErrors _errors = {};
@@ -38,18 +51,9 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(S.of(context).genericActionEditLabel),
actions: [
IconButton(
onPressed: _onDelete,
icon: const Icon(Icons.delete),
),
],
),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.update), icon: widget.submitButtonConfig.icon,
label: Text(S.of(context).genericActionUpdateLabel), label: widget.submitButtonConfig.label,
onPressed: _onSubmit, onPressed: _onSubmit,
), ),
body: FormBuilder( body: FormBuilder(
@@ -63,7 +67,7 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
errorText: _errors[Label.nameKey], errorText: _errors[Label.nameKey],
), ),
validator: FormBuilderValidators.required(), validator: FormBuilderValidators.required(),
initialValue: widget.label.name, initialValue: widget.initialValue?.name,
onChanged: (val) => setState(() => _errors = {}), onChanged: (val) => setState(() => _errors = {}),
), ),
FormBuilderTextField( FormBuilderTextField(
@@ -72,12 +76,13 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
labelText: S.of(context).labelMatchPropertyLabel, labelText: S.of(context).labelMatchPropertyLabel,
errorText: _errors[Label.matchKey], errorText: _errors[Label.matchKey],
), ),
initialValue: widget.label.match, initialValue: widget.initialValue?.match,
onChanged: (val) => setState(() => _errors = {}), onChanged: (val) => setState(() => _errors = {}),
), ),
FormBuilderDropdown<int?>( FormBuilderDropdown<int?>(
//TODO: Extract to own widget.
name: Label.matchingAlgorithmKey, name: Label.matchingAlgorithmKey,
initialValue: widget.label.matchingAlgorithm?.value ?? initialValue: widget.initialValue?.matchingAlgorithm?.value ??
MatchingAlgorithm.allWords.value, MatchingAlgorithm.allWords.value,
decoration: InputDecoration( decoration: InputDecoration(
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel, labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
@@ -95,7 +100,7 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
), ),
FormBuilderCheckbox( FormBuilderCheckbox(
name: Label.isInsensitiveKey, name: Label.isInsensitiveKey,
initialValue: widget.label.isInsensitive, initialValue: widget.initialValue?.isInsensitive,
title: Text(S.of(context).labelIsInsensivitePropertyLabel), title: Text(S.of(context).labelIsInsensivitePropertyLabel),
), ),
...widget.additionalFields, ...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 { void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
try { try {
final mergedJson = { final mergedJson = {
...widget.label.toJson(), ...widget.initialValue?.toJson() ?? {},
..._formKey.currentState!.value ..._formKey.currentState!.value
}; };
await widget.onSubmit(widget.fromJson(mergedJson)); await widget.submitButtonConfig.onSubmit(widget.fromJsonT(mergedJson));
Navigator.pop(context); Navigator.pop(context);
} on PaperlessValidationErrors catch (errorMessages) { } on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_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/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.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/bottom_navigation_bar.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/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/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/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart'; import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -59,17 +57,17 @@ class _HomePageState extends State<HomePage> {
MultiBlocProvider( MultiBlocProvider(
providers: [ providers: [
BlocProvider.value( BlocProvider.value(
value: getIt<DocumentsCubit>(), value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
), ),
], ],
child: const DocumentsPage(), child: const DocumentsPage(),
), ),
BlocProvider.value( BlocProvider.value(
value: getIt<DocumentScannerCubit>(), value: DocumentScannerCubit(),
child: const ScannerPage(), child: const ScannerPage(),
), ),
BlocProvider.value( BlocProvider.value(
value: getIt<DocumentsCubit>(), value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
child: const LabelsPage(), child: const LabelsPage(),
), ),
][_currentIndex], ][_currentIndex],
@@ -78,20 +76,17 @@ class _HomePageState extends State<HomePage> {
); );
} }
Future<void> _initializeData(BuildContext context) { void _initializeData(BuildContext context) {
try { 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) BlocProvider.of<PaperlessServerInformationCubit>(context)
.updateInformtion(), .updateInformtion();
getIt<DocumentTypeCubit>().initialize(),
getIt<CorrespondentCubit>().initialize(),
getIt<TagCubit>().initialize(),
getIt<StoragePathCubit>().initialize(),
getIt<SavedViewCubit>().initialize(),
]);
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.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/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart'; import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart'; import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
import 'package:paperless_mobile/features/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/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
@@ -188,11 +187,14 @@ class InfoDrawer extends StatelessWidget {
onTap: () { onTap: () {
try { try {
BlocProvider.of<AuthenticationCubit>(context).logout(); BlocProvider.of<AuthenticationCubit>(context).logout();
getIt<DocumentsCubit>().reset(); RepositoryProvider.of<LabelRepository<Tag>>(context).clear();
getIt<CorrespondentCubit>().reset(); RepositoryProvider.of<LabelRepository<Correspondent>>(context)
getIt<DocumentTypeCubit>().reset(); .clear();
getIt<TagCubit>().reset(); RepositoryProvider.of<LabelRepository<DocumentType>>(context)
getIt<DocumentScannerCubit>().reset(); .clear();
RepositoryProvider.of<LabelRepository<StoragePath>>(context)
.clear();
RepositoryProvider.of<SavedViewRepository>(context).clear();
} on PaperlessServerException catch (error, stackTrace) { } on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} }
@@ -208,14 +210,15 @@ class InfoDrawer extends StatelessWidget {
Future<void> _onOpenInbox(BuildContext context) async { Future<void> _onOpenInbox(BuildContext context) async {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider( builder: (_) => LabelRepositoriesProvider(
additionalProviders: [ child: BlocProvider(
BlocProvider<InboxCubit>.value( create: (context) => InboxCubit(
value: getIt<InboxCubit>()..loadInbox(), RepositoryProvider.of<LabelRepository<Tag>>(context),
getIt<PaperlessDocumentsApi>(),
), ),
],
child: const InboxPage(), child: const InboxPage(),
), ),
),
maintainState: false, maintainState: false,
), ),
); );

View File

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

View File

@@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart'; import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.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'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
class InboxItem extends StatelessWidget { class InboxItem extends StatelessWidget {
@@ -49,22 +48,20 @@ class InboxItem extends StatelessWidget {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider( builder: (_) => BlocProvider.value(
additionalProviders: [ value: DocumentDetailsCubit(
BlocProvider<DocumentDetailsCubit>(
create: (context) => DocumentDetailsCubit(
getIt<PaperlessDocumentsApi>(), getIt<PaperlessDocumentsApi>(),
document, document,
), ),
), child: const LabelRepositoriesProvider(
], child: DocumentDetailsPage(
child: const DocumentDetailsPage(
allowEdit: false, allowEdit: false,
isLabelClickable: 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/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
abstract class LabelCubit<T extends Label> extends Cubit<LabelState<T>> { class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final PaperlessLabelsApi labelsApi; final LabelRepository<T> _repository;
LabelCubit(this.labelsApi) : super(LabelState.initial()); late StreamSubscription _subscription;
@protected LabelCubit(this._repository) : super(LabelState.initial()) {
void loadFrom(Iterable<T> items) { _subscription = _repository.labels.listen(
emit( (update) => emit(LabelState(isLoaded: true, labels: update)),
LabelState(
isLoaded: true,
labels: Map.fromIterable(items, key: (e) => (e as T).id!),
),
); );
} }
///
/// 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 { Future<T> add(T item) async {
assert(item.id == null); assert(item.id == null);
final addedItem = await save(item); final addedItem = await _repository.create(item);
final newValues = {...state.labels};
newValues.putIfAbsent(addedItem.id!, () => addedItem);
emit(
LabelState(
isLoaded: true,
labels: newValues,
),
);
return addedItem; return addedItem;
} }
Future<T> replace(T item) async { Future<T> replace(T item) async {
assert(item.id != null); assert(item.id != null);
final updatedItem = await update(item); final updatedItem = await _repository.update(item);
final updatedValues = {...state.labels};
updatedValues[item.id!] = updatedItem;
emit(
LabelState(
isLoaded: state.isLoaded,
labels: updatedValues,
),
);
return updatedItem; return updatedItem;
} }
Future<void> remove(T item) async { Future<void> remove(T item) async {
assert(item.id != null); assert(item.id != null);
if (state.labels.containsKey(item.id)) { if (state.labels.containsKey(item.id)) {
final deletedId = await delete(item); await _repository.delete(item);
final updatedValues = {...state.labels}..remove(deletedId);
emit(
LabelState(isLoaded: true, labels: updatedValues),
);
} }
} }
@@ -61,14 +45,9 @@ abstract class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
emit(LabelState(isLoaded: false, labels: {})); emit(LabelState(isLoaded: false, labels: {}));
} }
Future<void> initialize(); @override
Future<void> close() {
@protected _subscription.cancel();
Future<T> save(T item); return super.close();
}
@protected
Future<T> update(T item);
@protected
Future<int> delete(T item);
} }

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:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/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/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -22,9 +23,11 @@ class CorrespondentWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AbsorbPointer( return CorrespondentBlocProvider(
child: AbsorbPointer(
absorbing: !isClickable, absorbing: !isClickable,
child: BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>( child:
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) { builder: (context, state) {
return GestureDetector( return GestureDetector(
onTap: () => _addCorrespondentToFilter(context), 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.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/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -19,11 +20,16 @@ class DocumentTypeWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AbsorbPointer( return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: AbsorbPointer(
absorbing: !isClickable, absorbing: !isClickable,
child: GestureDetector( child: GestureDetector(
onTap: () => _addDocumentTypeToFilter(context), onTap: () => _addDocumentTypeToFilter(context),
child: BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>( child:
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) { builder: (context, state) {
return Text( return Text(
state.labels[documentTypeId]?.toString() ?? "-", 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.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/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
class StoragePathWidget extends StatelessWidget { class StoragePathWidget extends StatelessWidget {
@@ -22,9 +23,13 @@ class StoragePathWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AbsorbPointer( return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: AbsorbPointer(
absorbing: !isClickable, absorbing: !isClickable,
child: BlocBuilder<StoragePathCubit, LabelState<StoragePath>>( child: BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) { builder: (context, state) {
return GestureDetector( return GestureDetector(
onTap: () => _addStoragePathToFilter(context), 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_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart'; import 'package:paperless_mobile/features/labels/bloc/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/pages/add_tag_page.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class TagFormField extends StatefulWidget { class TagFormField extends StatefulWidget {
@@ -41,11 +44,13 @@ class _TagFormFieldState extends State<TagFormField> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final state = BlocProvider.of<TagCubit>(context).state;
_textEditingController = TextEditingController() _textEditingController = TextEditingController()
..addListener(() { ..addListener(() {
setState(() { setState(() {
_showCreationSuffixIcon = state.labels.values _showCreationSuffixIcon = BlocProvider.of<LabelCubit<Tag>>(context)
.state
.labels
.values
.where( .where(
(item) => item.name.toLowerCase().startsWith( (item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(), _textEditingController.text.toLowerCase(),
@@ -61,7 +66,8 @@ class _TagFormFieldState extends State<TagFormField> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<TagCubit, LabelState<Tag>>( return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, tagState) { builder: (context, tagState) {
return FormBuilderField<TagsQuery>( return FormBuilderField<TagsQuery>(
builder: (field) { builder: (field) {
@@ -89,7 +95,9 @@ class _TagFormFieldState extends State<TagFormField> {
.toList(); .toList();
if (field.value is IdsTagsQuery) { if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) => suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery).ids.contains(element)); (field.value as IdsTagsQuery)
.ids
.contains(element));
} }
if (widget.notAssignedSelectable && if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) { field.value is! OnlyNotAssignedTagsQuery) {
@@ -122,7 +130,8 @@ class _TagFormFieldState extends State<TagFormField> {
title: Text( title: Text(
tag.name, tag.name,
style: TextStyle( 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, name: widget.name,
); );
}, },
),
); );
} }
@@ -199,8 +209,8 @@ class _TagFormFieldState extends State<TagFormField> {
void _onAddTag(BuildContext context, FormFieldState<TagsQuery> field) async { void _onAddTag(BuildContext context, FormFieldState<TagsQuery> field) async {
final Tag? tag = await Navigator.of(context).push<Tag>( final Tag? tag = await Navigator.of(context).push<Tag>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => BlocProvider.value( builder: (_) => RepositoryProvider.value(
value: BlocProvider.of<TagCubit>(context), value: RepositoryProvider.of<LabelRepository<Tag>>(context),
child: AddTagPage(initialValue: _textEditingController.text), child: AddTagPage(initialValue: _textEditingController.text),
), ),
), ),

View File

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

View File

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

View File

@@ -1,24 +1,25 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart';
@injectable
class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> { class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
final PaperlessDocumentsApi _api; 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( final documents = await _api.find(
filter.copyWith( state.filter.copyWith(
pageSize: 100, pageSize: 100,
), ),
); );
emit(LinkedDocumentsState( emit(LinkedDocumentsState(
isLoaded: true, isLoaded: true,
documents: documents, documents: documents,
filter: filter, filter: state.filter,
)); ));
} }
} }

View File

@@ -3,10 +3,10 @@ import 'package:paperless_api/paperless_api.dart';
class LinkedDocumentsState { class LinkedDocumentsState {
final bool isLoaded; final bool isLoaded;
final PagedSearchResult<DocumentModel>? documents; final PagedSearchResult<DocumentModel>? documents;
final DocumentFilter? filter; final DocumentFilter filter;
LinkedDocumentsState({ LinkedDocumentsState({
this.filter, required this.filter,
this.isLoaded = false, this.isLoaded = false,
this.documents, 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/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart'; import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.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/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart'; import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
@@ -64,15 +63,12 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (ctxt) => GlobalStateBlocProvider( builder: (context) =>
additionalProviders: [
BlocProvider<DocumentDetailsCubit>.value( BlocProvider<DocumentDetailsCubit>.value(
value: DocumentDetailsCubit( value: DocumentDetailsCubit(
getIt<PaperlessDocumentsApi>(), getIt<PaperlessDocumentsApi>(),
document, document,
), ),
),
],
child: const DocumentDetailsPage( child: const DocumentDetailsPage(
isLabelClickable: false, isLabelClickable: false,
allowEdit: false, allowEdit: false,

View File

@@ -1,21 +1,27 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.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> { class SavedViewCubit extends Cubit<SavedViewState> {
final PaperlessSavedViewsApi _api; final SavedViewRepository _repository;
SavedViewCubit(this._api) : super(SavedViewState(value: {})); StreamSubscription? _subscription;
SavedViewCubit(this._repository) : super(SavedViewState(value: {})) {
_subscription = _repository.savedViews.listen(
(savedViews) => emit(state.copyWith(value: savedViews)),
);
}
void selectView(SavedView? view) { void selectView(SavedView? view) {
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id)); emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
} }
Future<SavedView> add(SavedView view) async { Future<SavedView> add(SavedView view) async {
final savedView = await _api.save(view); final savedView = await _repository.create(view);
emit( emit(
SavedViewState( SavedViewState(
value: {...state.value, savedView.id!: savedView}, value: {...state.value, savedView.id!: savedView},
@@ -26,22 +32,15 @@ class SavedViewCubit extends Cubit<SavedViewState> {
} }
Future<int> remove(SavedView view) async { Future<int> remove(SavedView view) async {
final id = await _api.delete(view); final id = await _repository.delete(view);
final newValue = {...state.value}; if (state.selectedSavedViewId == id) {
newValue.removeWhere((key, value) => key == id); resetSelection();
emit( }
SavedViewState(
value: newValue,
selectedSavedViewId: view.id == state.selectedSavedViewId
? null
: state.selectedSavedViewId,
),
);
return id; return id;
} }
Future<void> initialize() async { Future<void> initialize() async {
final views = await _api.getAll(); final views = await _repository.findAll();
final values = {for (var element in views) element.id!: element}; final values = {for (var element in views) element.id!: element};
emit(SavedViewState(value: values)); emit(SavedViewState(value: values));
} }
@@ -49,4 +48,10 @@ class SavedViewCubit extends Cubit<SavedViewState> {
void resetSelection() { void resetSelection() {
emit(SavedViewState(value: state.value)); 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, value,
selectedSavedViewId, 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/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart'; import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_cubit.dart'; import 'package:paperless_mobile/features/saved_view/cubit/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_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -94,18 +94,11 @@ class SavedViewSelectionWidget extends StatelessWidget {
void _onSelected( void _onSelected(
bool isSelected, BuildContext context, SavedView view) async { bool isSelected, BuildContext context, SavedView view) async {
try {
if (isSelected) { if (isSelected) {
BlocProvider.of<DocumentsCubit>(context)
.updateFilter(filter: view.toDocumentFilter());
BlocProvider.of<SavedViewCubit>(context).selectView(view); BlocProvider.of<SavedViewCubit>(context).selectView(view);
} else { } else {
BlocProvider.of<DocumentsCubit>(context).updateFilter();
BlocProvider.of<SavedViewCubit>(context).selectView(null); BlocProvider.of<SavedViewCubit>(context).selectView(null);
} }
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
} }
void _onDelete(BuildContext context, SavedView view) async { 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/di_initializer.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
@injectable
class DocumentScannerCubit extends Cubit<List<File>> { class DocumentScannerCubit extends Cubit<List<File>> {
final PaperlessDocumentsApi _api; DocumentScannerCubit() : super(const []);
static List<File> initialState = [];
DocumentScannerCubit(this._api) : super(initialState);
void addScan(File file) => emit([...state, file]); void addScan(File file) => emit([...state, file]);
@@ -39,41 +34,9 @@ class DocumentScannerCubit extends Cubit<List<File>> {
} }
} }
imageCache.clear(); imageCache.clear();
emit(initialState); emit([]);
} catch (_) { } catch (_) {
throw const PaperlessServerException(ErrorCode.scanRemoveFailed); 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:flutter_bloc/flutter_bloc.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:paperless_api/paperless_api.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/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/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/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.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/features/scan/view/widgets/grid_image_item_widget.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
@@ -124,17 +128,29 @@ class _ScannerPageState extends State<ScannerPage>
final bytes = await doc.save(); final bytes = await doc.save();
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider( builder: (_) => LabelRepositoriesProvider(
additionalProviders: [ child: BlocProvider(
BlocProvider<DocumentScannerCubit>.value( create: (context) => DocumentUploadCubit(
value: BlocProvider.of<DocumentScannerCubit>(context), localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
), ),
], documentTypeRepository:
child: DocumentUploadPage( RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes, fileBytes: bytes,
), ),
), ),
), ),
),
); );
} }
@@ -243,15 +259,27 @@ class _ScannerPageState extends State<ScannerPage>
} }
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => GlobalStateBlocProvider( builder: (_) => LabelRepositoriesProvider(
additionalProviders: [ child: BlocProvider(
BlocProvider<DocumentScannerCubit>.value( create: (context) => DocumentUploadCubit(
value: BlocProvider.of<DocumentScannerCubit>(context), localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
), ),
], documentTypeRepository:
child: DocumentUploadPage( RepositoryProvider.of<LabelRepository<DocumentType>>(
filename: filename, context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
),
child: DocumentUploadPreparationPage(
fileBytes: fileBytes, 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_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart'; import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/global_state_bloc_provider.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/global/constants.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/global/http_self_signed_certificate_override.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.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/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.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/home/view/home_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart'; import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/login_page.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/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/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
void main() async { void main() async {
Bloc.observer = BlocChangesObserver(); Bloc.observer = BlocChangesObserver();
@@ -52,7 +57,30 @@ void main() async {
await getIt<ApplicationSettingsCubit>().initialize(); await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().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 { class PaperlessMobileEntrypoint extends StatefulWidget {
@@ -71,9 +99,6 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
BlocProvider<ConnectivityCubit>.value( BlocProvider<ConnectivityCubit>.value(
value: getIt<ConnectivityCubit>(), value: getIt<ConnectivityCubit>(),
), ),
BlocProvider<AuthenticationCubit>.value(
value: getIt<AuthenticationCubit>(),
),
BlocProvider<PaperlessServerInformationCubit>.value( BlocProvider<PaperlessServerInformationCubit>.value(
value: getIt<PaperlessServerInformationCubit>(), value: getIt<PaperlessServerInformationCubit>(),
), ),
@@ -126,7 +151,10 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.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 filename = extractFilenameFromPath(file.path);
final bytes = File(file.path).readAsBytesSync(); final bytes = File(file.path).readAsBytesSync();
Navigator.push( final success = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => GlobalStateBlocProvider( builder: (context) => BlocProvider.value(
additionalProviders: [ value: getIt<DocumentScannerCubit>(),
BlocProvider.value(value: getIt<DocumentScannerCubit>()), child: DocumentUploadPreparationPage(
],
child: DocumentUploadPage(
fileBytes: bytes, fileBytes: bytes,
afterUpload: SystemNavigator.pop,
filename: filename, filename: filename,
), ),
), ),
), ),
); );
if (success) {
SystemNavigator.pop();
}
} }
@override @override
@@ -232,17 +260,12 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
}, },
builder: (context, authentication) { builder: (context, authentication) {
if (authentication.isAuthenticated) { if (authentication.isAuthenticated) {
return GlobalStateBlocProvider( return const HomePage();
additionalProviders: [
BlocProvider.value(value: getIt<DocumentsCubit>()),
],
child: const HomePage(),
);
} else { } else {
if (authentication.wasLoginStored && // if (authentication.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) { // !(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return BiometricAuthenticationPage(); // return const BiometricAuthenticationPage();
} // }
return const LoginPage(); return const LoginPage();
} }
}, },

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import 'package:paperless_api/src/models/saved_view_model.dart'; import 'package:paperless_api/src/models/saved_view_model.dart';
abstract class PaperlessSavedViewsApi { 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<SavedView> save(SavedView view);
Future<int> delete(SavedView view); Future<int> delete(SavedView view);

View File

@@ -14,13 +14,15 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
PaperlessSavedViewsApiImpl(this.client); PaperlessSavedViewsApiImpl(this.client);
@override @override
Future<List<SavedView>> getAll() { Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
return getCollection( final result = await getCollection(
"/api/saved_views/", "/api/saved_views/",
SavedView.fromJson, SavedView.fromJson,
ErrorCode.loadSavedViewsError, ErrorCode.loadSavedViewsError,
client: client, client: client,
); );
return result.where((view) => ids?.contains(view.id!) ?? true);
} }
@override @override
@@ -51,4 +53,14 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
httpStatusCode: response.statusCode, 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 source: hosted
version: "1.4.5" version: "1.4.5"
rxdart: rxdart:
dependency: transitive dependency: "direct main"
description: description:
name: rxdart name: rxdart
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.27.4" version: "0.27.7"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View File

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