mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-15 06:12:29 -06:00
feat: Make label fields less restrictive, improve change detection in document edit page
This commit is contained in:
@@ -5,7 +5,8 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
final IdQueryParameter? initialValue;
|
||||
/// If null, this will resolve to [UnsetIdQueryParameter].
|
||||
final IdQueryParameter initialValue;
|
||||
|
||||
final Map<int, T> options;
|
||||
final Future<T?> Function(String? initialName)? onCreateNewLabel;
|
||||
@@ -20,7 +21,7 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
|
||||
FullscreenLabelForm({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
this.initialValue = const UnsetIdQueryParameter(),
|
||||
required this.options,
|
||||
required this.onCreateNewLabel,
|
||||
this.showNotAssignedOption = true,
|
||||
@@ -31,12 +32,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
this.autofocus = true,
|
||||
this.allowSelectUnassigned = true,
|
||||
required this.canCreateNewLabel,
|
||||
}) : assert(
|
||||
!(initialValue?.isOnlyAssigned ?? false) || showAnyAssignedOption,
|
||||
),
|
||||
assert(
|
||||
!(initialValue?.isOnlyNotAssigned ?? false) || showNotAssignedOption,
|
||||
),
|
||||
}) : assert(!(initialValue.isOnlyAssigned) || showAnyAssignedOption),
|
||||
assert(!(initialValue.isOnlyNotAssigned) || showNotAssignedOption),
|
||||
assert((addNewLabelText != null) == (onCreateNewLabel != null));
|
||||
|
||||
@override
|
||||
@@ -52,9 +49,9 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController.addListener(() => setState(() {
|
||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||
}));
|
||||
_textEditingController.addListener(() {
|
||||
setState(() => _showClearIcon = _textEditingController.text.isNotEmpty);
|
||||
});
|
||||
if (widget.autofocus) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
//Delay keyboard popup to ensure open animation is finished before.
|
||||
@@ -130,36 +127,42 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
child: const Icon(Icons.add),
|
||||
)
|
||||
: null,
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = options.elementAt(index);
|
||||
final highlight =
|
||||
AutocompleteHighlightedOption.of(context) == index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration timeStamp) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
return _buildOptionWidget(option, highlight);
|
||||
},
|
||||
),
|
||||
body: Builder(builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = options.elementAt(index);
|
||||
final shouldHighlight = switch (option) {
|
||||
NotAssignedIdQueryParameter() => true,
|
||||
AnyAssignedIdQueryParameter() => true,
|
||||
SetIdQueryParameter(id: var id) =>
|
||||
(widget.options[id]?.documentCount ?? 0) > 0,
|
||||
_ => false,
|
||||
};
|
||||
final highlight =
|
||||
AutocompleteHighlightedOption.of(context) == index;
|
||||
if (highlight && shouldHighlight) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration timeStamp) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
return _buildOptionWidget(
|
||||
option, highlight && shouldHighlight);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,8 +182,12 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
Iterable<IdQueryParameter> _filterOptionsByQuery(String query) sync* {
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
if (normalizedQuery.isEmpty) {
|
||||
if (widget.initialValue == null) {
|
||||
// If nothing is selected yet, show all options first.
|
||||
if ((widget.initialValue.isUnset)) {
|
||||
if (widget.options.isEmpty) {
|
||||
yield const UnsetIdQueryParameter();
|
||||
return;
|
||||
}
|
||||
// If nothing is selected yet (==> UnsetIdQueryParameter), show all options first.
|
||||
for (final option in widget.options.values) {
|
||||
yield SetIdQueryParameter(id: option.id!);
|
||||
}
|
||||
@@ -243,12 +250,17 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
AnyAssignedIdQueryParameter() => S.of(context)!.anyAssigned,
|
||||
SetIdQueryParameter(id: var id) =>
|
||||
widget.options[id]?.name ?? S.of(context)!.startTyping,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildOptionWidget(IdQueryParameter option, bool highlight) {
|
||||
void onTap() => widget.onSubmit(returnValue: option);
|
||||
final hasNoAssignedDocumentsTextStyle = Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium
|
||||
?.apply(color: Theme.of(context).disabledColor);
|
||||
final hasAssignedDocumentsTextStyle =
|
||||
Theme.of(context).textTheme.labelMedium;
|
||||
|
||||
return switch (option) {
|
||||
NotAssignedIdQueryParameter() => ListTile(
|
||||
@@ -266,11 +278,20 @@ class _FullscreenLabelFormState<T extends Label>
|
||||
SetIdQueryParameter(id: var id) => ListTile(
|
||||
selected: highlight,
|
||||
selectedTileColor: Theme.of(context).focusColor,
|
||||
title: Text(widget.options[id]!.name),
|
||||
title: Text(
|
||||
widget.options[id]!.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: onTap,
|
||||
enabled: widget.allowSelectUnassigned
|
||||
? true
|
||||
: widget.options[id]!.documentCount != 0,
|
||||
trailing: Text(
|
||||
S
|
||||
.of(context)!
|
||||
.documentsAssigned(widget.options[id]!.documentCount ?? 0),
|
||||
style: (widget.options[id]!.documentCount ?? 0) > 0
|
||||
? hasAssignedDocumentsTextStyle
|
||||
: hasNoAssignedDocumentsTextStyle,
|
||||
),
|
||||
),
|
||||
UnsetIdQueryParameter() => Center(
|
||||
child: Column(
|
||||
|
||||
@@ -19,11 +19,11 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
final String name;
|
||||
final String labelText;
|
||||
final FormFieldValidator? validator;
|
||||
final Widget Function(String? initialName)? addLabelPageBuilder;
|
||||
final Future<T?> Function(String? initialName)? onAddLabel;
|
||||
final void Function(IdQueryParameter?)? onChanged;
|
||||
final bool showNotAssignedOption;
|
||||
final bool showAnyAssignedOption;
|
||||
final List<T> suggestions;
|
||||
final Iterable<int> suggestions;
|
||||
final String? addLabelText;
|
||||
final bool allowSelectUnassigned;
|
||||
final bool canCreateNewLabel;
|
||||
@@ -36,7 +36,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
required this.prefixIcon,
|
||||
this.initialValue,
|
||||
this.validator,
|
||||
this.addLabelPageBuilder,
|
||||
this.onAddLabel,
|
||||
this.onChanged,
|
||||
this.showNotAssignedOption = true,
|
||||
this.showAnyAssignedOption = true,
|
||||
@@ -58,21 +58,21 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
|
||||
addLabelPageBuilder != null;
|
||||
final enabled = options.values.isNotEmpty || onAddLabel != null;
|
||||
|
||||
return FormBuilderField<IdQueryParameter>(
|
||||
name: name,
|
||||
initialValue: initialValue,
|
||||
onChanged: onChanged,
|
||||
enabled: isEnabled,
|
||||
enabled: enabled,
|
||||
builder: (field) {
|
||||
final controller = TextEditingController(
|
||||
text: _buildText(context, field.value),
|
||||
);
|
||||
final displayedSuggestions = suggestions
|
||||
.whereNot(
|
||||
(e) =>
|
||||
e.id ==
|
||||
(id) =>
|
||||
id ==
|
||||
switch (field.value) {
|
||||
SetIdQueryParameter(id: var id) => id,
|
||||
_ => -1,
|
||||
@@ -89,13 +89,14 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
closedShape: InputBorder.none,
|
||||
openElevation: 0,
|
||||
closedElevation: 0,
|
||||
tappable: enabled,
|
||||
closedBuilder: (context, openForm) => Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onTap: openForm,
|
||||
readOnly: true,
|
||||
enabled: isEnabled,
|
||||
enabled: enabled,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: prefixIcon,
|
||||
labelText: labelText,
|
||||
@@ -114,19 +115,10 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
canCreateNewLabel: canCreateNewLabel,
|
||||
addNewLabelText: addLabelText,
|
||||
leadingIcon: prefixIcon,
|
||||
onCreateNewLabel: addLabelPageBuilder != null
|
||||
? (initialName) {
|
||||
return Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
addLabelPageBuilder!(initialName),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onCreateNewLabel: onAddLabel,
|
||||
options: options,
|
||||
onSubmit: closeForm,
|
||||
initialValue: field.value,
|
||||
initialValue: field.value ?? const UnsetIdQueryParameter(),
|
||||
showAnyAssignedOption: showAnyAssignedOption,
|
||||
showNotAssignedOption: showNotAssignedOption,
|
||||
),
|
||||
@@ -151,7 +143,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
itemCount: displayedSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion =
|
||||
displayedSuggestions.elementAt(index);
|
||||
options[displayedSuggestions.elementAt(index)]!;
|
||||
|
||||
return ColoredChipWrapper(
|
||||
child: ActionChip(
|
||||
label: Text(suggestion.name),
|
||||
|
||||
Reference in New Issue
Block a user