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

@@ -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

@@ -0,0 +1,129 @@
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 SubmitButtonConfig<T extends Label> {
final Widget icon;
final Widget label;
final Future<void> Function(T) onSubmit;
SubmitButtonConfig({
required this.icon,
required this.label,
required this.onSubmit,
});
}
class LabelForm<T extends Label> extends StatefulWidget {
final T? initialValue;
final SubmitButtonConfig submitButtonConfig;
/// FromJson method to parse the form field values into a label instance.
final T Function(Map<String, dynamic> json) fromJsonT;
/// List of additionally rendered form fields.
final List<Widget> additionalFields;
const LabelForm({
Key? key,
required this.initialValue,
required this.fromJsonT,
this.additionalFields = const [],
required this.submitButtonConfig,
}) : super(key: key);
@override
State<LabelForm> createState() => _LabelFormState<T>();
}
class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
final _formKey = GlobalKey<FormBuilderState>();
PaperlessValidationErrors _errors = {};
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
icon: widget.submitButtonConfig.icon,
label: widget.submitButtonConfig.label,
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.initialValue?.name,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderTextField(
name: Label.matchKey,
decoration: InputDecoration(
labelText: S.of(context).labelMatchPropertyLabel,
errorText: _errors[Label.matchKey],
),
initialValue: widget.initialValue?.match,
onChanged: (val) => setState(() => _errors = {}),
),
FormBuilderDropdown<int?>(
//TODO: Extract to own widget.
name: Label.matchingAlgorithmKey,
initialValue: widget.initialValue?.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.initialValue?.isInsensitive,
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
),
...widget.additionalFields,
].padded(),
),
),
);
}
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
try {
final mergedJson = {
...widget.initialValue?.toJson() ?? {},
..._formKey.currentState!.value
};
await widget.submitButtonConfig.onSubmit(widget.fromJsonT(mergedJson));
Navigator.pop(context);
} on PaperlessValidationErrors catch (errorMessages) {
setState(() => _errors = errorMessages);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}
}