WIP - more decoupling of blocs

This commit is contained in:
Anton Stubenbord
2022-12-12 01:29:34 +01:00
parent e2a20cea75
commit 2f31d9c053
51 changed files with 1083 additions and 800 deletions

View File

@@ -11,9 +11,13 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
late StreamSubscription _subscription;
LabelCubit(this._repository) : super(LabelState.initial()) {
LabelCubit(LabelRepository<T> repository)
: _repository = repository,
super(LabelState(labels: repository.current, isLoaded: true)) {
_subscription = _repository.labels.listen(
(update) => emit(LabelState(isLoaded: true, labels: update)),
(update) => emit(
LabelState(isLoaded: true, labels: update),
),
);
}
@@ -28,6 +32,10 @@ class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
return addedItem;
}
Future<void> reload() {
return _repository.findAll();
}
Future<T> replace(T item) async {
assert(item.id != null);
final updatedItem = await _repository.update(item);

View File

@@ -4,11 +4,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class TagFormField extends StatefulWidget {
@@ -18,6 +14,7 @@ class TagFormField extends StatefulWidget {
final bool notAssignedSelectable;
final bool anyAssignedSelectable;
final bool excludeAllowed;
final Map<int, Tag> selectableOptions;
const TagFormField({
super.key,
@@ -27,6 +24,7 @@ class TagFormField extends StatefulWidget {
this.notAssignedSelectable = true,
this.anyAssignedSelectable = true,
this.excludeAllowed = true,
required this.selectableOptions,
});
@override
@@ -47,10 +45,7 @@ class _TagFormFieldState extends State<TagFormField> {
_textEditingController = TextEditingController()
..addListener(() {
setState(() {
_showCreationSuffixIcon = BlocProvider.of<LabelCubit<Tag>>(context)
.state
.labels
.values
_showCreationSuffixIcon = widget.selectableOptions.values
.where(
(item) => item.name.toLowerCase().startsWith(
_textEditingController.text.toLowerCase(),
@@ -66,122 +61,126 @@ class _TagFormFieldState extends State<TagFormField> {
@override
Widget build(BuildContext context) {
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, tagState) {
return FormBuilderField<TagsQuery>(
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypeAheadField<int>(
textFieldConfiguration: TextFieldConfiguration(
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.label_outline,
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context).documentTagsPropertyLabel,
hintText: S.of(context).tagFormFieldSearchHintText,
),
controller: _textEditingController,
),
suggestionsCallback: (query) {
final suggestions = tagState.labels.values
.where((element) => element.name
.toLowerCase()
.startsWith(query.toLowerCase()))
.map((e) => e.id!)
.toList();
if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery)
.ids
.contains(element));
}
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
suggestions.insert(0, _onlyNotAssignedId);
}
if (widget.anyAssignedSelectable &&
field.value is! AnyAssignedTagsQuery) {
suggestions.insert(0, _anyAssignedId);
}
return suggestions;
},
getImmediateSuggestions: true,
animationStart: 1,
itemBuilder: (context, data) {
if (data == _onlyNotAssignedId) {
return ListTile(
title: Text(S.of(context).labelNotAssignedText),
);
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = tagState.getLabel(data)!;
return ListTile(
leading: Icon(
Icons.circle,
color: tag.color,
),
title: Text(
tag.name,
style: TextStyle(
color:
Theme.of(context).colorScheme.onBackground),
),
);
},
onSuggestionSelected: (id) {
if (id == _onlyNotAssignedId) {
//Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(tagsQuery
.withIdQueriesAdded([IncludeTagIdQuery(id)]));
}
_textEditingController.clear();
},
direction: AxisDirection.up,
final isEnabled = widget.selectableOptions.values.fold<bool>(
false,
(previousValue, element) =>
previousValue || (element.documentCount ?? 0) > 0) ||
widget.allowCreation;
return FormBuilderField<TagsQuery>(
enabled: isEnabled,
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TypeAheadField<int>(
textFieldConfiguration: TextFieldConfiguration(
enabled: isEnabled,
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.label_outline,
),
if (field.value is OnlyNotAssignedTagsQuery) ...[
_buildNotAssignedTag(field)
] else if (field.value is AnyAssignedTagsQuery) ...[
_buildAnyAssignedTag(field)
] else ...[
// field.value is IdsTagsQuery
Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
spacing: 8.0,
children: ((field.value as IdsTagsQuery).queries)
.map(
(query) => _buildTag(
field,
query,
tagState.getLabel(query.id),
),
)
.toList(),
),
]
],
);
},
initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name,
);
},
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context).documentTagsPropertyLabel,
hintText: S.of(context).tagFormFieldSearchHintText,
),
controller: _textEditingController,
),
suggestionsCallback: (query) {
final suggestions = widget.selectableOptions.entries
.where(
(entry) => entry.value.name
.toLowerCase()
.startsWith(query.toLowerCase()),
)
.where((entry) =>
widget.allowCreation ||
(entry.value.documentCount ?? 0) > 0)
.map((entry) => entry.key)
.toList();
if (field.value is IdsTagsQuery) {
suggestions.removeWhere((element) =>
(field.value as IdsTagsQuery).ids.contains(element));
}
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
suggestions.insert(0, _onlyNotAssignedId);
}
if (widget.anyAssignedSelectable &&
field.value is! AnyAssignedTagsQuery) {
suggestions.insert(0, _anyAssignedId);
}
return suggestions;
},
getImmediateSuggestions: true,
animationStart: 1,
itemBuilder: (context, data) {
if (data == _onlyNotAssignedId) {
return ListTile(
title: Text(S.of(context).labelNotAssignedText),
);
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
}
final tag = widget.selectableOptions[data]!;
return ListTile(
leading: Icon(
Icons.circle,
color: tag.color,
),
title: Text(
tag.name,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground),
),
);
},
onSuggestionSelected: (id) {
if (id == _onlyNotAssignedId) {
//Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(
tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(id)]));
}
_textEditingController.clear();
},
direction: AxisDirection.up,
),
if (field.value is OnlyNotAssignedTagsQuery) ...[
_buildNotAssignedTag(field)
] else if (field.value is AnyAssignedTagsQuery) ...[
_buildAnyAssignedTag(field)
] else ...[
// field.value is IdsTagsQuery
Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
spacing: 8.0,
children: ((field.value as IdsTagsQuery).queries)
.map(
(query) => _buildTag(
field,
query,
widget.selectableOptions[query.id],
),
)
.toList(),
),
]
],
);
},
initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name,
);
}

View File

@@ -30,6 +30,7 @@ class _LabelsPageState extends State<LabelsPage>
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index));
}

View File

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

View File

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