mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-15 06:12:29 -06:00
WIP - more decoupling of blocs
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user