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

View File

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

View File

@@ -0,0 +1,20 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class CorrespondentBlocProvider extends StatelessWidget {
final Widget child;
const CorrespondentBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
child: child,
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class DocumentTypeBlocProvider extends StatelessWidget {
final Widget child;
const DocumentTypeBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: child,
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class LabelsBlocProvider extends StatelessWidget {
final Widget child;
const LabelsBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<LabelCubit<StoragePath>>(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
),
BlocProvider<LabelCubit<Correspondent>>(
create: (context) => LabelCubit<Correspondent>(
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
),
BlocProvider<LabelCubit<DocumentType>>(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
),
BlocProvider<LabelCubit<Tag>>(
create: (context) => LabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
),
],
child: child,
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class StoragePathBlocProvider extends StatelessWidget {
final Widget child;
const StoragePathBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: child,
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
class TagBlocProvider extends StatelessWidget {
final Widget child;
const TagBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Tag>(
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
child: child,
);
}
}

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/correspondent_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart';
@@ -22,22 +23,25 @@ class CorrespondentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<CorrespondentCubit, LabelState<Correspondent>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addCorrespondentToFilter(context),
child: Text(
(state.getLabel(correspondentId)?.name) ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
);
},
return CorrespondentBlocProvider(
child: AbsorbPointer(
absorbing: !isClickable,
child:
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addCorrespondentToFilter(context),
child: Text(
(state.getLabel(correspondentId)?.name) ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
);
},
),
),
);
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart';
@@ -19,20 +20,26 @@ class DocumentTypeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => _addDocumentTypeToFilter(context),
child: BlocBuilder<DocumentTypeCubit, LabelState<DocumentType>>(
builder: (context, state) {
return Text(
state.labels[documentTypeId]?.toString() ?? "-",
style: Theme.of(context)
.textTheme
.bodyText2!
.copyWith(color: Theme.of(context).colorScheme.tertiary),
);
},
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
child: AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => _addDocumentTypeToFilter(context),
child:
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return Text(
state.labels[documentTypeId]?.toString() ?? "-",
style: Theme.of(context)
.textTheme
.bodyText2!
.copyWith(color: Theme.of(context).colorScheme.tertiary),
);
},
),
),
),
);

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/util.dart';
class StoragePathWidget extends StatelessWidget {
@@ -22,22 +23,27 @@ class StoragePathWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<StoragePathCubit, LabelState<StoragePath>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addStoragePathToFilter(context),
child: Text(
state.getLabel(pathId)?.name ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
);
},
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
child: AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addStoragePathToFilter(context),
child: Text(
state.getLabel(pathId)?.name ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
);
},
),
),
);
}

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class TagFormField extends StatefulWidget {
@@ -41,11 +44,13 @@ class _TagFormFieldState extends State<TagFormField> {
@override
void initState() {
super.initState();
final state = BlocProvider.of<TagCubit>(context).state;
_textEditingController = TextEditingController()
..addListener(() {
setState(() {
_showCreationSuffixIcon = state.labels.values
_showCreationSuffixIcon = BlocProvider.of<LabelCubit<Tag>>(context)
.state
.labels
.values
.where(
(item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(),
@@ -61,117 +66,122 @@ class _TagFormFieldState extends State<TagFormField> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TagCubit, LabelState<Tag>>(
builder: (context, tagState) {
return FormBuilderField<TagsQuery>(
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypeAheadField<int>(
textFieldConfiguration: TextFieldConfiguration(
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.label_outline,
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, tagState) {
return FormBuilderField<TagsQuery>(
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypeAheadField<int>(
textFieldConfiguration: TextFieldConfiguration(
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.label_outline,
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context).documentTagsPropertyLabel,
hintText: S.of(context).tagFormFieldSearchHintText,
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context).documentTagsPropertyLabel,
hintText: S.of(context).tagFormFieldSearchHintText,
controller: _textEditingController,
),
controller: _textEditingController,
),
suggestionsCallback: (query) {
final suggestions = tagState.labels.values
.where((element) => element.name
.toLowerCase()
.startsWith(query.toLowerCase()))
.map((e) => e.id!)
.toList();
if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery).ids.contains(element));
}
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
suggestions.insert(0, _onlyNotAssignedId);
}
if (widget.anyAssignedSelectable &&
field.value is! AnyAssignedTagsQuery) {
suggestions.insert(0, _anyAssignedId);
}
return suggestions;
},
getImmediateSuggestions: true,
animationStart: 1,
itemBuilder: (context, data) {
if (data == _onlyNotAssignedId) {
suggestionsCallback: (query) {
final suggestions = tagState.labels.values
.where((element) => element.name
.toLowerCase()
.startsWith(query.toLowerCase()))
.map((e) => e.id!)
.toList();
if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery)
.ids
.contains(element));
}
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
suggestions.insert(0, _onlyNotAssignedId);
}
if (widget.anyAssignedSelectable &&
field.value is! AnyAssignedTagsQuery) {
suggestions.insert(0, _anyAssignedId);
}
return suggestions;
},
getImmediateSuggestions: true,
animationStart: 1,
itemBuilder: (context, data) {
if (data == _onlyNotAssignedId) {
return ListTile(
title: Text(S.of(context).labelNotAssignedText),
);
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = tagState.getLabel(data)!;
return ListTile(
title: Text(S.of(context).labelNotAssignedText),
leading: Icon(
Icons.circle,
color: tag.color,
),
title: Text(
tag.name,
style: TextStyle(
color:
Theme.of(context).colorScheme.onBackground),
),
);
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = tagState.getLabel(data)!;
return ListTile(
leading: Icon(
Icons.circle,
color: tag.color,
),
title: Text(
tag.name,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground),
),
);
},
onSuggestionSelected: (id) {
if (id == _onlyNotAssignedId) {
//Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(tagsQuery
.withIdQueriesAdded([IncludeTagIdQuery(id)]));
}
_textEditingController.clear();
},
direction: AxisDirection.up,
),
if (field.value is OnlyNotAssignedTagsQuery) ...[
_buildNotAssignedTag(field)
] else if (field.value is AnyAssignedTagsQuery) ...[
_buildAnyAssignedTag(field)
] else ...[
// field.value is IdsTagsQuery
Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
spacing: 8.0,
children: ((field.value as IdsTagsQuery).queries)
.map(
(query) => _buildTag(
field,
query,
tagState.getLabel(query.id),
),
)
.toList(),
},
onSuggestionSelected: (id) {
if (id == _onlyNotAssignedId) {
//Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(tagsQuery
.withIdQueriesAdded([IncludeTagIdQuery(id)]));
}
_textEditingController.clear();
},
direction: AxisDirection.up,
),
]
],
);
},
initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name,
);
},
if (field.value is OnlyNotAssignedTagsQuery) ...[
_buildNotAssignedTag(field)
] else if (field.value is AnyAssignedTagsQuery) ...[
_buildAnyAssignedTag(field)
] else ...[
// field.value is IdsTagsQuery
Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
spacing: 8.0,
children: ((field.value as IdsTagsQuery).queries)
.map(
(query) => _buildTag(
field,
query,
tagState.getLabel(query.id),
),
)
.toList(),
),
]
],
);
},
initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name,
);
},
),
);
}
@@ -199,8 +209,8 @@ class _TagFormFieldState extends State<TagFormField> {
void _onAddTag(BuildContext context, FormFieldState<TagsQuery> field) async {
final Tag? tag = await Navigator.of(context).push<Tag>(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<TagCubit>(context),
builder: (_) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
child: AddTagPage(initialValue: _textEditingController.text),
),
),

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatefulWidget {
@@ -30,36 +31,38 @@ class TagsWidget extends StatefulWidget {
class _TagsWidgetState extends State<TagsWidget> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TagCubit, LabelState<Tag>>(
builder: (context, state) {
final children = widget.tagIds
.where((id) => state.labels.containsKey(id))
.map(
(id) => TagWidget(
tag: state.getLabel(id)!,
afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable,
isSelected: widget.isSelectedPredicate(id),
onSelected: () => widget.onTagSelected(id),
),
)
.toList();
if (widget.isMultiLine) {
return Wrap(
runAlignment: WrapAlignment.start,
children: children,
runSpacing: 8,
spacing: 4,
);
} else {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) {
final children = widget.tagIds
.where((id) => state.labels.containsKey(id))
.map(
(id) => TagWidget(
tag: state.getLabel(id)!,
afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable,
isSelected: widget.isSelectedPredicate(id),
onSelected: () => widget.onTagSelected(id),
),
)
.toList();
if (widget.isMultiLine) {
return Wrap(
runAlignment: WrapAlignment.start,
children: children,
),
);
}
},
runSpacing: 8,
spacing: 4,
);
} else {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: children,
),
);
}
},
),
);
}
}

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,156 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class EditLabelPage<T extends Label> extends StatefulWidget {
final T label;
final Future<void> Function(T) onSubmit;
final Future<void> Function(T) onDelete;
final T Function(JSON) fromJson;
final List<Widget> additionalFields;
const EditLabelPage({
Key? key,
required this.label,
required this.fromJson,
required this.onSubmit,
required this.onDelete,
this.additionalFields = const [],
}) : super(key: key);
@override
State<EditLabelPage> createState() => _EditLabelPageState<T>();
}
class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
final _formKey = GlobalKey<FormBuilderState>();
PaperlessValidationErrors _errors = {};
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(S.of(context).genericActionEditLabel),
actions: [
IconButton(
onPressed: _onDelete,
icon: const Icon(Icons.delete),
),
],
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.update),
label: Text(S.of(context).genericActionUpdateLabel),
onPressed: _onSubmit,
),
body: FormBuilder(
key: _formKey,
child: ListView(
children: [
FormBuilderTextField(
name: Label.nameKey,
decoration: InputDecoration(
labelText: S.of(context).labelNamePropertyLabel,
errorText: _errors[Label.nameKey],
),
validator: FormBuilderValidators.required(),
initialValue: widget.label.name,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderTextField(
name: Label.matchKey,
decoration: InputDecoration(
labelText: S.of(context).labelMatchPropertyLabel,
errorText: _errors[Label.matchKey],
),
initialValue: widget.label.match,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderDropdown<int?>(
name: Label.matchingAlgorithmKey,
initialValue: widget.label.matchingAlgorithm?.value ??
MatchingAlgorithm.allWords.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: widget.label.isInsensitive,
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
),
...widget.additionalFields,
].padded(),
),
),
);
}
void _onDelete() {
if ((widget.label.documentCount ?? 0) > 0) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(S.of(context).editLabelPageConfirmDeletionDialogTitle),
content: Text(
S.of(context).editLabelPageDeletionDialogText,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(S.of(context).genericActionCancelLabel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
widget.onDelete(widget.label);
},
child: Text(
S.of(context).genericActionDeleteLabel,
style: TextStyle(color: Theme.of(context).errorColor),
),
),
],
),
);
} else {
widget.onDelete(widget.label);
}
}
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
try {
final mergedJson = {
...widget.label.toJson(),
..._formKey.currentState!.value
};
await widget.onSubmit(widget.fromJson(mergedJson));
Navigator.pop(context);
} on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = errorMessages);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
}

View File

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

View File

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

View File

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