mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 23:15:43 -06:00
feat: Finalize bulk edits and reworked form fields
This commit is contained in:
56
lib/core/widgets/dialog_utils/dialog_confirm_button.dart
Normal file
56
lib/core/widgets/dialog_utils/dialog_confirm_button.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
|
import 'package:flutter/src/widgets/placeholder.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
enum DialogConfirmButtonStyle {
|
||||||
|
normal,
|
||||||
|
danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DialogConfirmButton<T> extends StatelessWidget {
|
||||||
|
final DialogConfirmButtonStyle style;
|
||||||
|
final String? label;
|
||||||
|
final T? returnValue;
|
||||||
|
const DialogConfirmButton({
|
||||||
|
super.key,
|
||||||
|
this.style = DialogConfirmButtonStyle.normal,
|
||||||
|
this.label,
|
||||||
|
this.returnValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final _normalStyle = ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
foregroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final _dangerStyle = ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
foregroundColor: MaterialStatePropertyAll(
|
||||||
|
Theme.of(context).colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
late final ButtonStyle _style;
|
||||||
|
switch (style) {
|
||||||
|
case DialogConfirmButtonStyle.normal:
|
||||||
|
_style = _normalStyle;
|
||||||
|
break;
|
||||||
|
case DialogConfirmButtonStyle.danger:
|
||||||
|
_style = _dangerStyle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ElevatedButton(
|
||||||
|
child: Text(label ?? S.of(context)!.confirm),
|
||||||
|
style: _style,
|
||||||
|
onPressed: () => Navigator.of(context).pop(returnValue ?? true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
|
||||||
class ErrorReportPage extends StatefulWidget {
|
class ErrorReportPage extends StatefulWidget {
|
||||||
@@ -136,10 +137,7 @@ Note: If you have the GitHub Android app installed, the descriptions will not be
|
|||||||
Navigator.pop(context, true);
|
Navigator.pop(context, true);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
child: const Text('Cancel'),
|
|
||||||
onPressed: () => Navigator.pop(context, false),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
extension on Color {
|
extension on Color {
|
||||||
@@ -136,11 +138,12 @@ class FormBuilderColorPickerField extends FormBuilderField<Color> {
|
|||||||
: LayoutBuilder(
|
: LayoutBuilder(
|
||||||
key: ObjectKey(state.value),
|
key: ObjectKey(state.value),
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Icon(
|
return Padding(
|
||||||
Icons.circle,
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: CircleAvatar(
|
||||||
key: ObjectKey(state.value),
|
key: ObjectKey(state.value),
|
||||||
size: constraints.minHeight,
|
backgroundColor: state.value,
|
||||||
color: state.value,
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -218,17 +221,11 @@ class FormBuilderColorPickerFieldState
|
|||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
// title: null, //const Text('Pick a color!'),
|
// title: null, //const Text('Pick a color!'),
|
||||||
content: SingleChildScrollView(
|
content: _buildColorPicker(),
|
||||||
child: _buildColorPicker(),
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(materialLocalizations.cancel),
|
label: S.of(context)!.ok,
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
child: Text(materialLocalizations.ok),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class FullscreenSelectionForm extends StatefulWidget {
|
class FullscreenSelectionForm extends StatefulWidget {
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
@@ -115,7 +117,14 @@ class _FullscreenSelectionFormState extends State<FullscreenSelectionForm> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Builder(builder: (context) {
|
||||||
|
if (widget.selectionCount == 0) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Text(S.of(context)!.noItemsFound).padded(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
@@ -139,7 +148,8 @@ class _FullscreenSelectionFormState extends State<FullscreenSelectionForm> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,3 +36,9 @@ extension DateHelpers on DateTime {
|
|||||||
yesterday.year == year;
|
yesterday.year == year;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension StringNormalizer on String {
|
||||||
|
String normalized() {
|
||||||
|
return trim().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
|
|||||||
final deletedDocuments = state.selection
|
final deletedDocuments = state.selection
|
||||||
.where((element) => deletedDocumentIds.contains(element.id));
|
.where((element) => deletedDocumentIds.contains(element.id));
|
||||||
for (final doc in deletedDocuments) {
|
for (final doc in deletedDocuments) {
|
||||||
_notifier.notifyUpdated(doc);
|
_notifier.notifyDeleted(doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
|
|||||||
final updatedDocuments = state.selection
|
final updatedDocuments = state.selection
|
||||||
.where((element) => modifiedDocumentIds.contains(element.id))
|
.where((element) => modifiedDocumentIds.contains(element.id))
|
||||||
.map((doc) => doc.copyWith(tags: [
|
.map((doc) => doc.copyWith(tags: [
|
||||||
...doc.tags.toSet().difference(addTagIds.toSet()),
|
...doc.tags.toSet().difference(removeTagIds.toSet()),
|
||||||
...addTagIds
|
...addTagIds
|
||||||
]));
|
]));
|
||||||
for (final doc in updatedDocuments) {
|
for (final doc in updatedDocuments) {
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
|
|
||||||
class BulkEditPage<T extends Label> extends StatefulWidget {
|
|
||||||
final bool enableMultipleChoice;
|
|
||||||
final Map<int, T> availableOptions;
|
|
||||||
|
|
||||||
const BulkEditPage({
|
|
||||||
super.key,
|
|
||||||
required this.enableMultipleChoice,
|
|
||||||
required this.availableOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BulkEditPage> createState() => _BulkEditPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BulkEditPageState extends State<BulkEditPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
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/widgets/dialog_utils/dialog_cancel_button.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class BulkEditTagsBottomSheet extends StatefulWidget {
|
|
||||||
const BulkEditTagsBottomSheet({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BulkEditTagsBottomSheet> createState() =>
|
|
||||||
_BulkEditTagsBottomSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BulkEditTagsBottomSheetState extends State<BulkEditTagsBottomSheet> {
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
|
||||||
final _textEditingController = TextEditingController();
|
|
||||||
late Set<int> _sharedTags;
|
|
||||||
late Set<int> _nonSharedTags;
|
|
||||||
final Set<int> _sharedTagsToRemove = {};
|
|
||||||
final Set<int> _nonSharedTagsToRemove = {};
|
|
||||||
final Set<int> _tagsToAdd = {};
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final state = context.read<DocumentBulkActionCubit>().state;
|
|
||||||
_sharedTags = state.selection
|
|
||||||
.map((doc) => doc.tags)
|
|
||||||
.reduce((previousValue, element) =>
|
|
||||||
previousValue.toSet().intersection(element.toSet()))
|
|
||||||
.toSet();
|
|
||||||
print(_sharedTags.map((e) => e).join(", "));
|
|
||||||
_nonSharedTags = state.selection
|
|
||||||
.map((doc) => doc.tags)
|
|
||||||
.flattened
|
|
||||||
.toSet()
|
|
||||||
.difference(_sharedTags)
|
|
||||||
.toSet();
|
|
||||||
print(_nonSharedTags.map((e) => e).join(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Padding(
|
|
||||||
padding:
|
|
||||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
|
||||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
print(state);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Bulk modify tags",
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
).paddedOnly(bottom: 24),
|
|
||||||
TypeAheadFormField<Tag>(
|
|
||||||
textFieldConfiguration: TextFieldConfiguration(
|
|
||||||
controller: _textEditingController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: "Tags",
|
|
||||||
hintText: "Start typing to add tags...",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onSuggestionSelected: (suggestion) {
|
|
||||||
setState(() {
|
|
||||||
_tagsToAdd.add(suggestion.id!);
|
|
||||||
});
|
|
||||||
_textEditingController.clear();
|
|
||||||
},
|
|
||||||
itemBuilder: (context, option) {
|
|
||||||
return ListTile(
|
|
||||||
leading: SizedBox(
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
child: DecoratedBox(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: option.color!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(option.name),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
suggestionsCallback: (pattern) {
|
|
||||||
final searchString = pattern.toLowerCase();
|
|
||||||
return state.tags.entries
|
|
||||||
.where(
|
|
||||||
(tag) => tag.value.name
|
|
||||||
.toLowerCase()
|
|
||||||
.contains(searchString),
|
|
||||||
)
|
|
||||||
.map((e) => e.key)
|
|
||||||
.toSet()
|
|
||||||
.difference(_sharedTags)
|
|
||||||
.difference(_nonSharedTags)
|
|
||||||
.map((e) => state.tags[e]!);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text("Shared tags"),
|
|
||||||
Wrap(
|
|
||||||
children: _sharedTags
|
|
||||||
.map(
|
|
||||||
(tag) => RemovableTagWidget(
|
|
||||||
tag: state.tags[tag]!,
|
|
||||||
onDeleted: (tag) {
|
|
||||||
setState(() {
|
|
||||||
_sharedTagsToRemove.add(tag);
|
|
||||||
_sharedTags.remove(tag);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text("Non-shared tags"),
|
|
||||||
Wrap(
|
|
||||||
children: _nonSharedTags
|
|
||||||
.map(
|
|
||||||
(tag) => RemovableTagWidget(
|
|
||||||
tag: state.tags[tag]!,
|
|
||||||
onDeleted: (tag) {
|
|
||||||
setState(() {
|
|
||||||
_nonSharedTagsToRemove.add(tag);
|
|
||||||
_nonSharedTags.remove(tag);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
Text("Remove"),
|
|
||||||
Wrap(
|
|
||||||
children: _sharedTagsToRemove.map((tag) {
|
|
||||||
return RemovableTagWidget(
|
|
||||||
tag: state.tags[tag]!,
|
|
||||||
onDeleted: (tag) {
|
|
||||||
setState(() {
|
|
||||||
_sharedTagsToRemove.remove(tag);
|
|
||||||
_sharedTags.add(tag);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList() +
|
|
||||||
_nonSharedTagsToRemove.map((tag) {
|
|
||||||
return RemovableTagWidget(
|
|
||||||
tag: state.tags[tag]!,
|
|
||||||
onDeleted: (tag) {
|
|
||||||
setState(() {
|
|
||||||
_nonSharedTagsToRemove.remove(tag);
|
|
||||||
_nonSharedTags.add(tag);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text("Add"),
|
|
||||||
Wrap(
|
|
||||||
children: _tagsToAdd
|
|
||||||
.map(
|
|
||||||
(tag) => RemovableTagWidget(
|
|
||||||
tag: state.tags[tag]!,
|
|
||||||
onDeleted: (tag) {
|
|
||||||
setState(() {
|
|
||||||
_tagsToAdd.remove(tag);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
const DialogCancelButton(),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_formKey.currentState?.saveAndValidate() ??
|
|
||||||
false) {
|
|
||||||
final value = _formKey.currentState
|
|
||||||
?.getRawValue('labelFormField')
|
|
||||||
as IdsTagsQuery;
|
|
||||||
context
|
|
||||||
.read<DocumentBulkActionCubit>()
|
|
||||||
.bulkModifyTags(
|
|
||||||
addTagIds: value.includedIds,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.apply),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padded(8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RemovableTagWidget extends StatelessWidget {
|
|
||||||
final Tag tag;
|
|
||||||
final void Function(int tagId) onDeleted;
|
|
||||||
const RemovableTagWidget(
|
|
||||||
{super.key, required this.tag, required this.onDeleted});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Chip(
|
|
||||||
label: Text(
|
|
||||||
tag.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: tag.textColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDeleted: () => onDeleted(tag.id!),
|
|
||||||
deleteIcon: Icon(Icons.clear),
|
|
||||||
backgroundColor: tag.color,
|
|
||||||
deleteIconColor: tag.textColor,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
side: BorderSide.none,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class ConfirmBulkModifyLabelDialog extends StatelessWidget {
|
||||||
|
final int selectionCount;
|
||||||
|
final String content;
|
||||||
|
const ConfirmBulkModifyLabelDialog({
|
||||||
|
super.key,
|
||||||
|
required this.selectionCount,
|
||||||
|
required this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context)!.confirmAction),
|
||||||
|
content: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: content,
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: "\n\n"),
|
||||||
|
TextSpan(
|
||||||
|
text: S.of(context)!.areYouSureYouWantToContinue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: const [
|
||||||
|
DialogCancelButton(),
|
||||||
|
DialogConfirmButton(
|
||||||
|
style: DialogConfirmButtonStyle.danger,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
|
import 'package:flutter/src/widgets/placeholder.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class ConfirmBulkModifyTagsDialog extends StatelessWidget {
|
||||||
|
final int selectionCount;
|
||||||
|
final List<String> removeTags;
|
||||||
|
final List<String> addTags;
|
||||||
|
const ConfirmBulkModifyTagsDialog({
|
||||||
|
super.key,
|
||||||
|
required this.removeTags,
|
||||||
|
required this.addTags,
|
||||||
|
required this.selectionCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(S.of(context)!.confirmAction),
|
||||||
|
content: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: _buildText(context),
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: "\n\n"),
|
||||||
|
TextSpan(
|
||||||
|
text: S.of(context)!.areYouSureYouWantToContinue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: const [
|
||||||
|
DialogCancelButton(),
|
||||||
|
DialogConfirmButton(
|
||||||
|
style: DialogConfirmButtonStyle.danger,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildText(BuildContext context) {
|
||||||
|
if (removeTags.isNotEmpty && addTags.isNotEmpty) {
|
||||||
|
return S.of(context)!.bulkEditTagsModifyMessage(
|
||||||
|
addTags.join(", "),
|
||||||
|
selectionCount,
|
||||||
|
removeTags.join(", "),
|
||||||
|
);
|
||||||
|
} else if (removeTags.isNotEmpty) {
|
||||||
|
return S.of(context)!.bulkEditTagsRemoveMessage(
|
||||||
|
selectionCount,
|
||||||
|
removeTags.join(", "),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return S.of(context)!.bulkEditTagsAddMessage(
|
||||||
|
selectionCount,
|
||||||
|
addTags.join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class FullscreenBulkEditLabelFormField extends StatefulWidget {
|
class FullscreenBulkEditLabelPage extends StatefulWidget {
|
||||||
final String hintText;
|
final String hintText;
|
||||||
final Map<int, Label> options;
|
final Map<int, Label> options;
|
||||||
final List<DocumentModel> selection;
|
final List<DocumentModel> selection;
|
||||||
final int? Function(DocumentModel document) labelMapper;
|
final int? Function(DocumentModel document) labelMapper;
|
||||||
final Widget leadingIcon;
|
final Widget leadingIcon;
|
||||||
final void Function(int? id) onSubmit;
|
final void Function(int? id) onSubmit;
|
||||||
|
final Function(String name) Function(int count) removeStringFnBuilder;
|
||||||
|
final String Function(String name) Function(int count) assignStringFnBuilder;
|
||||||
|
|
||||||
FullscreenBulkEditLabelFormField({
|
FullscreenBulkEditLabelPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.selection,
|
required this.selection,
|
||||||
@@ -23,20 +25,27 @@ class FullscreenBulkEditLabelFormField extends StatefulWidget {
|
|||||||
required this.leadingIcon,
|
required this.leadingIcon,
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
|
required this.removeStringFnBuilder,
|
||||||
|
required this.assignStringFnBuilder,
|
||||||
}) : assert(selection.isNotEmpty);
|
}) : assert(selection.isNotEmpty);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FullscreenBulkEditLabelFormField> createState() =>
|
State<FullscreenBulkEditLabelPage> createState() =>
|
||||||
_FullscreenBulkEditLabelFormFieldState();
|
_FullscreenBulkEditLabelPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FullscreenBulkEditLabelFormFieldState<T extends Label>
|
class _FullscreenBulkEditLabelPageState<T extends Label>
|
||||||
extends State<FullscreenBulkEditLabelFormField> {
|
extends State<FullscreenBulkEditLabelPage> {
|
||||||
|
final _controller = TextEditingController();
|
||||||
|
|
||||||
LabelSelection? _selection;
|
LabelSelection? _selection;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_controller.addListener(() {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
if (_initialValues.length == 1 && _initialValues.first != null) {
|
if (_initialValues.length == 1 && _initialValues.first != null) {
|
||||||
_selection = LabelSelection(_initialValues.first);
|
_selection = LabelSelection(_initialValues.first);
|
||||||
}
|
}
|
||||||
@@ -46,13 +55,19 @@ class _FullscreenBulkEditLabelFormFieldState<T extends Label>
|
|||||||
widget.selection.map(widget.labelMapper).toSet().toList();
|
widget.selection.map(widget.labelMapper).toSet().toList();
|
||||||
|
|
||||||
Iterable<int> _generateOrderedLabels() sync* {
|
Iterable<int> _generateOrderedLabels() sync* {
|
||||||
for (var label in _initialValues) {
|
final _availableValues = widget.options.values
|
||||||
|
.where(
|
||||||
|
(e) => e.name.normalized().contains(_controller.text.normalized()))
|
||||||
|
.map((e) => e.id!)
|
||||||
|
.toSet();
|
||||||
|
for (var label
|
||||||
|
in _initialValues.toSet().intersection(_availableValues.toSet())) {
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
yield label;
|
yield label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final id
|
for (final id
|
||||||
in widget.options.keys.whereNot((e) => _initialValues.contains(e))) {
|
in _availableValues.whereNot((e) => _initialValues.contains(e))) {
|
||||||
yield id;
|
yield id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +79,7 @@ class _FullscreenBulkEditLabelFormFieldState<T extends Label>
|
|||||||
(_initialValues.length == 1 &&
|
(_initialValues.length == 1 &&
|
||||||
_selection?.label == _initialValues.first);
|
_selection?.label == _initialValues.first);
|
||||||
return FullscreenSelectionForm(
|
return FullscreenSelectionForm(
|
||||||
|
controller: _controller,
|
||||||
hintText: widget.hintText,
|
hintText: widget.hintText,
|
||||||
leadingIcon: widget.leadingIcon,
|
leadingIcon: widget.leadingIcon,
|
||||||
selectionBuilder: (context, index) =>
|
selectionBuilder: (context, index) =>
|
||||||
@@ -101,16 +117,10 @@ class _FullscreenBulkEditLabelFormFieldState<T extends Label>
|
|||||||
content: Text(
|
content: Text(
|
||||||
S.of(context)!.areYouSureYouWantToContinue,
|
S.of(context)!.areYouSureYouWantToContinue,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: const [
|
||||||
const DialogCancelButton(),
|
DialogCancelButton(),
|
||||||
TextButton(
|
DialogConfirmButton(
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
child: Text(
|
|
||||||
S.of(context)!.confirm,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class FullscreenBulkEditTagsWidget extends StatefulWidget {
|
||||||
|
const FullscreenBulkEditTagsWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FullscreenBulkEditTagsWidget> createState() =>
|
||||||
|
_FullscreenBulkEditTagsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FullscreenBulkEditTagsWidgetState
|
||||||
|
extends State<FullscreenBulkEditTagsWidget> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
/// Tags shared by all documents
|
||||||
|
late final List<int> _sharedTags;
|
||||||
|
|
||||||
|
/// Tags not assigned to at least one document in the selection
|
||||||
|
late final List<int> _nonSharedTags;
|
||||||
|
|
||||||
|
List<int> _addTags = [];
|
||||||
|
List<int> _removeTags = [];
|
||||||
|
late List<int> _filteredTags;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final state = context.read<DocumentBulkActionCubit>().state;
|
||||||
|
_sharedTags = state.selection
|
||||||
|
.map((e) => e.tags)
|
||||||
|
.map((e) => e.toSet())
|
||||||
|
.fold(
|
||||||
|
state.tags.values.map((e) => e.id!).toSet(),
|
||||||
|
(previousValue, element) => previousValue.intersection(element),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
_nonSharedTags = state.selection
|
||||||
|
.map((e) => e.tags)
|
||||||
|
.flattened
|
||||||
|
.toSet()
|
||||||
|
.difference(_sharedTags.toSet())
|
||||||
|
.toList();
|
||||||
|
_filteredTags = state.tags.keys.toList();
|
||||||
|
_controller.addListener(() {
|
||||||
|
setState(() {
|
||||||
|
_filteredTags = context
|
||||||
|
.read<DocumentBulkActionCubit>()
|
||||||
|
.state
|
||||||
|
.tags
|
||||||
|
.values
|
||||||
|
.where((e) =>
|
||||||
|
e.name.normalized().contains(_controller.text.normalized()))
|
||||||
|
.map((e) => e.id!)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> get _assignedTags => [..._sharedTags, ..._nonSharedTags];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return FullscreenSelectionForm(
|
||||||
|
controller: _controller,
|
||||||
|
floatingActionButton: _addTags.isNotEmpty || _removeTags.isNotEmpty
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
label: Text(S.of(context)!.apply),
|
||||||
|
icon: const Icon(Icons.done),
|
||||||
|
onPressed: _submit,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
hintText: S.of(context)!.startTyping,
|
||||||
|
leadingIcon: const Icon(Icons.label_outline),
|
||||||
|
selectionBuilder: (context, index) {
|
||||||
|
return _buildTagOption(
|
||||||
|
_filteredTags[index],
|
||||||
|
state.tags,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectionCount: _filteredTags.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTagOption(int id, Map<int, Tag> options) {
|
||||||
|
Widget? icon;
|
||||||
|
if (_sharedTags.contains(id) && !_removeTags.contains(id)) {
|
||||||
|
// Tag is assigned to all documents and not marked for removal
|
||||||
|
// => will remain assigned
|
||||||
|
icon = const Icon(Icons.done);
|
||||||
|
} else if (_addTags.contains(id)) {
|
||||||
|
// tag is marked to be added
|
||||||
|
icon = const Icon(Icons.done);
|
||||||
|
} else if (_nonSharedTags.contains(id) && !_removeTags.contains(id)) {
|
||||||
|
// Tag is neither shared among all documents, nor marked to be removed or
|
||||||
|
// added but assigned to at least one document
|
||||||
|
icon = const Icon(Icons.remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(options[id]!.name),
|
||||||
|
trailing: icon,
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: options[id]!.color,
|
||||||
|
foregroundColor: options[id]!.textColor,
|
||||||
|
child: options[id]!.isInboxTag ? const Icon(Icons.inbox) : null,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (_addTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_addTags.remove(id);
|
||||||
|
});
|
||||||
|
if (_assignedTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_removeTags.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (_removeTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_removeTags.remove(id);
|
||||||
|
});
|
||||||
|
if (!_sharedTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_addTags.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_sharedTags.contains(id)) {
|
||||||
|
setState(() {
|
||||||
|
_removeTags.add(id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_addTags.add(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit() async {
|
||||||
|
if (_addTags.isNotEmpty || _removeTags.isNotEmpty) {
|
||||||
|
final bloc = context.read<DocumentBulkActionCubit>();
|
||||||
|
final addNames = _addTags
|
||||||
|
.map((value) => "\"${bloc.state.tags[value]!.name}\"")
|
||||||
|
.toList();
|
||||||
|
final removeNames = _removeTags
|
||||||
|
.map((value) => "\"${bloc.state.tags[value]!.name}\"")
|
||||||
|
.toList();
|
||||||
|
final shouldPerformAction = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ConfirmBulkModifyTagsDialog(
|
||||||
|
selectionCount: bloc.state.selection.length,
|
||||||
|
addTags: addNames,
|
||||||
|
removeTags: removeNames,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
if (shouldPerformAction) {
|
||||||
|
bloc.bulkModifyTags(
|
||||||
|
removeTagIds: _removeTags,
|
||||||
|
addTagIds: _addTags,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubi
|
|||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.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_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_storage_path_page.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_query_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -207,7 +207,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
],
|
],
|
||||||
).padded(),
|
).padded(),
|
||||||
// Tag form field
|
// Tag form field
|
||||||
TagQueryFormField(
|
TagsFormField(
|
||||||
options: state.tags,
|
options: state.tags,
|
||||||
name: fkTags,
|
name: fkTags,
|
||||||
allowOnlySelection: true,
|
allowOnlySelection: true,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:paperless_mobile/features/document_search/cubit/document_search_
|
|||||||
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
|
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/routes/document_details_route.dart';
|
import 'package:paperless_mobile/routes/document_details_route.dart';
|
||||||
@@ -70,13 +69,14 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
|||||||
controller: _queryController,
|
controller: _queryController,
|
||||||
onChanged: (query) {
|
onChanged: (query) {
|
||||||
_debounceTimer?.cancel();
|
_debounceTimer?.cancel();
|
||||||
_debounceTimer = Timer(const Duration(milliseconds: 700), () {
|
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||||
context.read<DocumentSearchCubit>().suggest(query);
|
context.read<DocumentSearchCubit>().suggest(query);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
onSubmitted: (query) {
|
onSubmitted: (query) {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
_debounceTimer?.cancel();
|
||||||
context.read<DocumentSearchCubit>().search(query);
|
context.read<DocumentSearchCubit>().search(query);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class RemoveHistoryEntryDialog extends StatelessWidget {
|
class RemoveHistoryEntryDialog extends StatelessWidget {
|
||||||
@@ -13,12 +14,10 @@ class RemoveHistoryEntryDialog extends StatelessWidget {
|
|||||||
content: Text(S.of(context)!.removeQueryFromSearchHistory),
|
content: Text(S.of(context)!.removeQueryFromSearchHistory),
|
||||||
actions: [
|
actions: [
|
||||||
const DialogCancelButton(),
|
const DialogCancelButton(),
|
||||||
TextButton(
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.remove),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
onPressed: () {
|
label: S.of(context)!.remove,
|
||||||
Navigator.pop(context, true);
|
)
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,13 +222,12 @@ class _DocumentUploadPreparationPageState
|
|||||||
options: state.documentTypes,
|
options: state.documentTypes,
|
||||||
prefixIcon: const Icon(Icons.description_outlined),
|
prefixIcon: const Icon(Icons.description_outlined),
|
||||||
),
|
),
|
||||||
TagFormField(
|
TagsFormField(
|
||||||
name: DocumentModel.tagsKey,
|
name: DocumentModel.tagsKey,
|
||||||
notAssignedSelectable: false,
|
allowCreation: true,
|
||||||
anyAssignedSelectable: false,
|
allowExclude: false,
|
||||||
excludeAllowed: false,
|
allowOnlySelection: true,
|
||||||
selectableOptions: state.tags,
|
options: state.tags,
|
||||||
//Label: "Tags" + " *",
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"* " + S.of(context)!.uploadInferValuesHint,
|
"* " + S.of(context)!.uploadInferValuesHint,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
||||||
@@ -30,19 +32,10 @@ class DeleteDocumentConfirmationDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.cancel),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
foregroundColor:
|
|
||||||
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.delete),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_query_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -193,19 +193,13 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTagsFormField() {
|
Widget _buildTagsFormField() {
|
||||||
return TagQueryFormField(
|
return TagsFormField(
|
||||||
allowExclude: false,
|
|
||||||
options: widget.tags,
|
|
||||||
name: DocumentModel.tagsKey,
|
name: DocumentModel.tagsKey,
|
||||||
initialValue: widget.initialFilter.tags,
|
initialValue: widget.initialFilter.tags,
|
||||||
|
options: widget.tags,
|
||||||
|
allowExclude: false,
|
||||||
allowOnlySelection: false,
|
allowOnlySelection: false,
|
||||||
allowCreation: false,
|
allowCreation: false,
|
||||||
);
|
);
|
||||||
return TagFormField(
|
|
||||||
name: DocumentModel.tagsKey,
|
|
||||||
initialValue: widget.initialFilter.tags,
|
|
||||||
allowCreation: false,
|
|
||||||
selectableOptions: widget.tags,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -29,19 +31,10 @@ class BulkDeleteConfirmationDialog extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.cancel),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
style: ButtonStyle(
|
|
||||||
foregroundColor:
|
|
||||||
MaterialStateProperty.all(Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
child: Text(S.of(context)!.delete),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
||||||
@@ -19,16 +21,10 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
content: Text(S.of(context)!.doYouReallyWantToDeleteThisView),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
child: Text(S.of(context)!.cancel),
|
DialogConfirmButton(
|
||||||
onPressed: () => Navigator.pop(context, false),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
child: Text(
|
|
||||||
S.of(context)!.delete,
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart';
|
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart';
|
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
|
||||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_form_field.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -82,7 +79,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
child: BlocBuilder<DocumentBulkActionCubit,
|
child: BlocBuilder<DocumentBulkActionCubit,
|
||||||
DocumentBulkActionState>(
|
DocumentBulkActionState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return FullscreenBulkEditLabelFormField(
|
return FullscreenBulkEditLabelPage(
|
||||||
options: state.correspondents,
|
options: state.correspondents,
|
||||||
selection: state.selection,
|
selection: state.selection,
|
||||||
labelMapper: (document) => document.correspondent,
|
labelMapper: (document) => document.correspondent,
|
||||||
@@ -91,6 +88,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
onSubmit: context
|
onSubmit: context
|
||||||
.read<DocumentBulkActionCubit>()
|
.read<DocumentBulkActionCubit>()
|
||||||
.bulkModifyCorrespondent,
|
.bulkModifyCorrespondent,
|
||||||
|
assignStringFnBuilder: (int count) {
|
||||||
|
return (String name) => S
|
||||||
|
.of(context)!
|
||||||
|
.bulkEditCorrespondentAssignMessage(
|
||||||
|
name,
|
||||||
|
count,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeStringFnBuilder: (int count) {
|
||||||
|
return (String name) => S
|
||||||
|
.of(context)!
|
||||||
|
.bulkEditCorrespondentRemoveMessage(
|
||||||
|
name,
|
||||||
|
count,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -115,7 +128,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
child: BlocBuilder<DocumentBulkActionCubit,
|
child: BlocBuilder<DocumentBulkActionCubit,
|
||||||
DocumentBulkActionState>(
|
DocumentBulkActionState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return FullscreenBulkEditLabelFormField(
|
return FullscreenBulkEditLabelPage(
|
||||||
options: state.documentTypes,
|
options: state.documentTypes,
|
||||||
selection: state.selection,
|
selection: state.selection,
|
||||||
labelMapper: (document) => document.documentType,
|
labelMapper: (document) => document.documentType,
|
||||||
@@ -125,6 +138,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
onSubmit: context
|
onSubmit: context
|
||||||
.read<DocumentBulkActionCubit>()
|
.read<DocumentBulkActionCubit>()
|
||||||
.bulkModifyDocumentType,
|
.bulkModifyDocumentType,
|
||||||
|
assignStringFnBuilder: (int count) {
|
||||||
|
return (String name) => S
|
||||||
|
.of(context)!
|
||||||
|
.bulkEditDocumentTypeAssignMessage(
|
||||||
|
count,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeStringFnBuilder: (int count) {
|
||||||
|
return (String name) => S
|
||||||
|
.of(context)!
|
||||||
|
.bulkEditDocumentTypeRemoveMessage(
|
||||||
|
count,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -149,7 +178,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
child: BlocBuilder<DocumentBulkActionCubit,
|
child: BlocBuilder<DocumentBulkActionCubit,
|
||||||
DocumentBulkActionState>(
|
DocumentBulkActionState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return FullscreenBulkEditLabelFormField(
|
return FullscreenBulkEditLabelPage(
|
||||||
options: state.storagePaths,
|
options: state.storagePaths,
|
||||||
selection: state.selection,
|
selection: state.selection,
|
||||||
labelMapper: (document) => document.storagePath,
|
labelMapper: (document) => document.storagePath,
|
||||||
@@ -158,6 +187,22 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
onSubmit: context
|
onSubmit: context
|
||||||
.read<DocumentBulkActionCubit>()
|
.read<DocumentBulkActionCubit>()
|
||||||
.bulkModifyStoragePath,
|
.bulkModifyStoragePath,
|
||||||
|
assignStringFnBuilder: (int count) {
|
||||||
|
return (String name) => S
|
||||||
|
.of(context)!
|
||||||
|
.bulkEditStoragePathAssignMessage(
|
||||||
|
count,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
removeStringFnBuilder: (int count) {
|
||||||
|
return (String name) => S
|
||||||
|
.of(context)!
|
||||||
|
.bulkEditStoragePathRemoveMessage(
|
||||||
|
count,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -179,17 +224,9 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
label: Text(S.of(context)!.tags),
|
label: Text(S.of(context)!.tags),
|
||||||
avatar: const Icon(Icons.edit),
|
avatar: const Icon(Icons.edit),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
Navigator.of(context).push(
|
||||||
shape: const RoundedRectangleBorder(
|
MaterialPageRoute(
|
||||||
borderRadius: BorderRadius.only(
|
builder: (context) => BlocProvider(
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder: (_) {
|
|
||||||
return BlocProvider(
|
|
||||||
create: (context) => DocumentBulkActionCubit(
|
create: (context) => DocumentBulkActionCubit(
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
@@ -197,10 +234,10 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
|
|||||||
selection: state.selection,
|
selection: state.selection,
|
||||||
),
|
),
|
||||||
child: Builder(builder: (context) {
|
child: Builder(builder: (context) {
|
||||||
return const BulkEditTagsBottomSheet();
|
return const FullscreenBulkEditTagsWidget();
|
||||||
}),
|
}),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.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/features/edit_label/view/label_form.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -95,19 +97,10 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
|||||||
S.of(context)!.deleteLabelWarningText,
|
S.of(context)!.deleteLabelWarningText,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.pop(context, false),
|
DialogConfirmButton(
|
||||||
child: Text(S.of(context)!.cancel),
|
label: S.of(context)!.delete,
|
||||||
),
|
style: DialogConfirmButtonStyle.danger,
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
S.of(context)!.delete,
|
|
||||||
style:
|
|
||||||
TextStyle(color: Theme.of(context).colorScheme.error),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class AddTagPage extends StatelessWidget {
|
|||||||
colorPickerType: ColorPickerType.materialPicker,
|
colorPickerType: ColorPickerType.materialPicker,
|
||||||
initialValue: Color((Random().nextDouble() * 0xFFFFFF).toInt())
|
initialValue: Color((Random().nextDouble() * 0xFFFFFF).toInt())
|
||||||
.withOpacity(1.0),
|
.withOpacity(1.0),
|
||||||
|
readOnly: true,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
FormBuilderCheckbox(
|
||||||
name: Tag.isInboxTagKey,
|
name: Tag.isInboxTagKey,
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ class EditTagPage extends StatelessWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
label: Text(S.of(context)!.color),
|
label: Text(S.of(context)!.color),
|
||||||
),
|
),
|
||||||
colorPickerType: ColorPickerType.blockPicker,
|
colorPickerType: ColorPickerType.materialPicker,
|
||||||
|
readOnly: true,
|
||||||
),
|
),
|
||||||
FormBuilderCheckbox(
|
FormBuilderCheckbox(
|
||||||
initialValue: tag.isInboxTag,
|
initialValue: tag.isInboxTag,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
Future<void> loadInbox() async {
|
Future<void> loadInbox() async {
|
||||||
debugPrint("Initializing inbox...");
|
debugPrint("Initializing inbox...");
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (inboxTags.isEmpty) {
|
if (inboxTags.isEmpty) {
|
||||||
@@ -106,7 +106,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
Future<void> reloadInbox() async {
|
Future<void> reloadInbox() async {
|
||||||
emit(state.copyWith(hasLoaded: false, isLoading: true));
|
emit(state.copyWith(hasLoaded: false, isLoading: true));
|
||||||
final inboxTags = await _labelRepository.findAllTags().then(
|
final inboxTags = await _labelRepository.findAllTags().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (inboxTags.isEmpty) {
|
if (inboxTags.isEmpty) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
@@ -197,18 +199,10 @@ class _InboxPageState extends State<InboxPage>
|
|||||||
S.of(context)!.areYouSureYouWantToMarkAllDocumentsAsSeen,
|
S.of(context)!.areYouSureYouWantToMarkAllDocumentsAsSeen,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
const DialogCancelButton(),
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
DialogConfirmButton(
|
||||||
child: Text(
|
label: S.of(context)!.markAsSeen,
|
||||||
S.of(context)!.cancel,
|
style: DialogConfirmButtonStyle.danger,
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(S.of(context)!.ok),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -12,7 +10,7 @@ class FullscreenTagsForm extends StatefulWidget {
|
|||||||
final bool allowOnlySelection;
|
final bool allowOnlySelection;
|
||||||
final bool allowCreation;
|
final bool allowCreation;
|
||||||
final bool allowExclude;
|
final bool allowExclude;
|
||||||
|
final bool autofocus;
|
||||||
const FullscreenTagsForm({
|
const FullscreenTagsForm({
|
||||||
super.key,
|
super.key,
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
@@ -21,6 +19,7 @@ class FullscreenTagsForm extends StatefulWidget {
|
|||||||
required this.allowOnlySelection,
|
required this.allowOnlySelection,
|
||||||
required this.allowCreation,
|
required this.allowCreation,
|
||||||
required this.allowExclude,
|
required this.allowExclude,
|
||||||
|
this.autofocus = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -56,6 +55,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
_textEditingController.addListener(() => setState(() {
|
_textEditingController.addListener(() => setState(() {
|
||||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||||
}));
|
}));
|
||||||
|
if (widget.autofocus) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
//Delay keyboard popup to ensure open animation is finished before.
|
//Delay keyboard popup to ensure open animation is finished before.
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
@@ -64,6 +64,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -72,7 +73,7 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
floatingActionButton: widget.allowCreation
|
floatingActionButton: widget.allowCreation
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
onPressed: _onAddTag,
|
onPressed: _onAddTag,
|
||||||
child: Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -145,12 +146,12 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
|
|||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
enabled: isSegmentedButtonEnabled,
|
enabled: isSegmentedButtonEnabled,
|
||||||
value: false,
|
value: false,
|
||||||
label: const Text("All"), //TODO: INTL
|
label: Text(S.of(context)!.allTags),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
enabled: isSegmentedButtonEnabled,
|
enabled: isSegmentedButtonEnabled,
|
||||||
value: true,
|
value: true,
|
||||||
label: Text(S.of(context)!.anyAssigned),
|
label: Text(S.of(context)!.anyTag),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
multiSelectionEnabled: false,
|
multiSelectionEnabled: false,
|
||||||
@@ -308,7 +309,7 @@ class SelectableTagWidget extends StatelessWidget {
|
|||||||
: (selected ? const Icon(Icons.done) : null),
|
: (selected ? const Icon(Icons.done) : null),
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
child: (tag.isInboxTag ?? false)
|
child: (tag.isInboxTag)
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.inbox,
|
Icons.inbox,
|
||||||
color: tag.textColor,
|
color: tag.textColor,
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
|
||||||
|
|
||||||
class TagQueryFormField extends StatelessWidget {
|
|
||||||
final String name;
|
|
||||||
final Map<int, Tag> options;
|
|
||||||
final TagsQuery? initialValue;
|
|
||||||
final bool allowOnlySelection;
|
|
||||||
final bool allowCreation;
|
|
||||||
final bool allowExclude;
|
|
||||||
|
|
||||||
const TagQueryFormField({
|
|
||||||
super.key,
|
|
||||||
required this.options,
|
|
||||||
this.initialValue,
|
|
||||||
required this.name,
|
|
||||||
required this.allowOnlySelection,
|
|
||||||
required this.allowCreation,
|
|
||||||
required this.allowExclude,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FormBuilderField<TagsQuery?>(
|
|
||||||
initialValue: initialValue,
|
|
||||||
builder: (field) {
|
|
||||||
final values = _generateOptions(context, field.value, field).toList();
|
|
||||||
final isEmpty = (field.value is IdsTagsQuery &&
|
|
||||||
(field.value as IdsTagsQuery).ids.isEmpty) ||
|
|
||||||
field.value == null;
|
|
||||||
bool anyAssigned = field.value is AnyAssignedTagsQuery;
|
|
||||||
return OpenContainer<TagsQuery>(
|
|
||||||
middleColor: Theme.of(context).colorScheme.background,
|
|
||||||
closedColor: Theme.of(context).colorScheme.background,
|
|
||||||
openColor: Theme.of(context).colorScheme.background,
|
|
||||||
closedShape: InputBorder.none,
|
|
||||||
openElevation: 0,
|
|
||||||
closedElevation: 0,
|
|
||||||
closedBuilder: (context, openForm) => Container(
|
|
||||||
margin: const EdgeInsets.only(top: 6),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: openForm,
|
|
||||||
child: InputDecorator(
|
|
||||||
isEmpty: isEmpty,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
contentPadding: const EdgeInsets.all(12),
|
|
||||||
labelText:
|
|
||||||
'${S.of(context)!.tags}${anyAssigned ? ' (${S.of(context)!.anyAssigned})' : ''}',
|
|
||||||
prefixIcon: const Icon(Icons.label_outline),
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 32,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
separatorBuilder: (context, index) => SizedBox(width: 4),
|
|
||||||
itemBuilder: (context, index) => values[index],
|
|
||||||
itemCount: values.length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
openBuilder: (context, closeForm) => FullscreenTagsForm(
|
|
||||||
options: options,
|
|
||||||
onSubmit: closeForm,
|
|
||||||
initialValue: field.value,
|
|
||||||
allowOnlySelection: allowOnlySelection,
|
|
||||||
allowCreation: allowCreation,
|
|
||||||
allowExclude: allowExclude,
|
|
||||||
),
|
|
||||||
onClosed: (data) {
|
|
||||||
if (data != null) {
|
|
||||||
field.didChange(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
name: name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Widget> _generateOptions(
|
|
||||||
BuildContext context,
|
|
||||||
TagsQuery? query,
|
|
||||||
FormFieldState<TagsQuery?> field,
|
|
||||||
) sync* {
|
|
||||||
if (query == null) {
|
|
||||||
yield Container();
|
|
||||||
} else if (query is IdsTagsQuery) {
|
|
||||||
for (final e in query.queries) {
|
|
||||||
yield _buildTagIdQueryWidget(context, e, field);
|
|
||||||
}
|
|
||||||
} else if (query is OnlyNotAssignedTagsQuery) {
|
|
||||||
yield _buildNotAssignedTagWidget(context, field);
|
|
||||||
} else if (query is AnyAssignedTagsQuery) {
|
|
||||||
for (final e in query.tagIds) {
|
|
||||||
yield _buildAnyAssignedTagWidget(context, e, field, query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTagIdQueryWidget(
|
|
||||||
BuildContext context,
|
|
||||||
TagIdQuery e,
|
|
||||||
FormFieldState<TagsQuery?> field,
|
|
||||||
) {
|
|
||||||
assert(field.value is IdsTagsQuery);
|
|
||||||
final formValue = field.value as IdsTagsQuery;
|
|
||||||
final tag = options[e.id]!;
|
|
||||||
return QueryTagChip(
|
|
||||||
onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])),
|
|
||||||
onSelected: allowExclude
|
|
||||||
? () => field.didChange(formValue.withIdQueryToggled(e.id))
|
|
||||||
: null,
|
|
||||||
exclude: e is ExcludeTagIdQuery,
|
|
||||||
backgroundColor: tag.color,
|
|
||||||
foregroundColor: tag.textColor,
|
|
||||||
labelText: tag.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNotAssignedTagWidget(
|
|
||||||
BuildContext context,
|
|
||||||
FormFieldState<TagsQuery?> field,
|
|
||||||
) {
|
|
||||||
return QueryTagChip(
|
|
||||||
onDeleted: () => field.didChange(null),
|
|
||||||
exclude: false,
|
|
||||||
backgroundColor: Colors.grey,
|
|
||||||
foregroundColor: Colors.black,
|
|
||||||
labelText: S.of(context)!.notAssigned,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAnyAssignedTagWidget(
|
|
||||||
BuildContext context,
|
|
||||||
int e,
|
|
||||||
FormFieldState<TagsQuery?> field,
|
|
||||||
AnyAssignedTagsQuery query,
|
|
||||||
) {
|
|
||||||
return QueryTagChip(
|
|
||||||
onDeleted: () {
|
|
||||||
final updatedQuery = query.withRemoved([e]);
|
|
||||||
if (updatedQuery.tagIds.isEmpty) {
|
|
||||||
field.didChange(const IdsTagsQuery());
|
|
||||||
} else {
|
|
||||||
field.didChange(updatedQuery);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
exclude: false,
|
|
||||||
backgroundColor: options[e]!.color,
|
|
||||||
foregroundColor: options[e]!.textColor,
|
|
||||||
labelText: options[e]!.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef TagQueryCallback = void Function(Tag tag);
|
|
||||||
|
|
||||||
class QueryTagChip extends StatelessWidget {
|
|
||||||
final VoidCallback onDeleted;
|
|
||||||
final VoidCallback? onSelected;
|
|
||||||
final bool exclude;
|
|
||||||
final Color? backgroundColor;
|
|
||||||
final Color? foregroundColor;
|
|
||||||
final String labelText;
|
|
||||||
|
|
||||||
const QueryTagChip({
|
|
||||||
super.key,
|
|
||||||
required this.onDeleted,
|
|
||||||
this.onSelected,
|
|
||||||
required this.exclude,
|
|
||||||
this.backgroundColor,
|
|
||||||
this.foregroundColor,
|
|
||||||
required this.labelText,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ColoredChipWrapper(
|
|
||||||
child: InputChip(
|
|
||||||
labelPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
selectedColor: backgroundColor,
|
|
||||||
visualDensity: const VisualDensity(vertical: -2),
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
label: Text(
|
|
||||||
labelText,
|
|
||||||
style: TextStyle(
|
|
||||||
color: foregroundColor,
|
|
||||||
decorationColor: foregroundColor,
|
|
||||||
decoration: exclude ? TextDecoration.lineThrough : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDeleted: onDeleted,
|
|
||||||
onPressed: onSelected,
|
|
||||||
deleteIconColor: foregroundColor,
|
|
||||||
checkmarkColor: foregroundColor,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
side: BorderSide.none,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,335 +1,213 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.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_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|
||||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class TagFormField extends StatefulWidget {
|
class TagsFormField extends StatelessWidget {
|
||||||
final TagsQuery? initialValue;
|
|
||||||
final String name;
|
final String name;
|
||||||
|
final Map<int, Tag> options;
|
||||||
|
final TagsQuery? initialValue;
|
||||||
|
final bool allowOnlySelection;
|
||||||
final bool allowCreation;
|
final bool allowCreation;
|
||||||
final bool notAssignedSelectable;
|
final bool allowExclude;
|
||||||
final bool anyAssignedSelectable;
|
|
||||||
final bool excludeAllowed;
|
|
||||||
final Map<int, Tag> selectableOptions;
|
|
||||||
final Widget? suggestions;
|
|
||||||
final String? labelText;
|
|
||||||
final String? hintText;
|
|
||||||
|
|
||||||
const TagFormField({
|
const TagsFormField({
|
||||||
super.key,
|
super.key,
|
||||||
required this.name,
|
required this.options,
|
||||||
this.initialValue,
|
this.initialValue,
|
||||||
this.allowCreation = true,
|
required this.name,
|
||||||
this.notAssignedSelectable = true,
|
required this.allowOnlySelection,
|
||||||
this.anyAssignedSelectable = true,
|
required this.allowCreation,
|
||||||
this.excludeAllowed = true,
|
required this.allowExclude,
|
||||||
required this.selectableOptions,
|
|
||||||
this.suggestions,
|
|
||||||
this.labelText,
|
|
||||||
this.hintText,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
State<TagFormField> createState() => _TagFormFieldState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TagFormFieldState extends State<TagFormField> {
|
|
||||||
static const _onlyNotAssignedId = -1;
|
|
||||||
static const _anyAssignedId = -2;
|
|
||||||
|
|
||||||
late final TextEditingController _textEditingController;
|
|
||||||
bool _showCreationSuffixIcon = false;
|
|
||||||
bool _showClearSuffixIcon = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_textEditingController = TextEditingController()
|
|
||||||
..addListener(() {
|
|
||||||
setState(() {
|
|
||||||
_showCreationSuffixIcon = widget.selectableOptions.values.where(
|
|
||||||
(item) {
|
|
||||||
log(item.name
|
|
||||||
.toLowerCase()
|
|
||||||
.startsWith(
|
|
||||||
_textEditingController.text.toLowerCase(),
|
|
||||||
)
|
|
||||||
.toString());
|
|
||||||
return item.name.toLowerCase().startsWith(
|
|
||||||
_textEditingController.text.toLowerCase(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).isEmpty;
|
|
||||||
});
|
|
||||||
setState(
|
|
||||||
() => _showClearSuffixIcon = _textEditingController.text.isNotEmpty,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isEnabled = widget.selectableOptions.values.fold<bool>(
|
return FormBuilderField<TagsQuery?>(
|
||||||
false,
|
initialValue: initialValue,
|
||||||
(previousValue, element) =>
|
|
||||||
previousValue || (element.documentCount ?? 0) > 0) ||
|
|
||||||
widget.allowCreation;
|
|
||||||
|
|
||||||
return FormBuilderField<TagsQuery>(
|
|
||||||
enabled: isEnabled,
|
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
return Column(
|
final values = _generateOptions(context, field.value, field).toList();
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final isEmpty = (field.value is IdsTagsQuery &&
|
||||||
children: [
|
(field.value as IdsTagsQuery).ids.isEmpty) ||
|
||||||
TypeAheadField<int>(
|
field.value == null;
|
||||||
textFieldConfiguration: TextFieldConfiguration(
|
bool anyAssigned = field.value is AnyAssignedTagsQuery;
|
||||||
enabled: isEnabled,
|
return OpenContainer<TagsQuery>(
|
||||||
|
middleColor: Theme.of(context).colorScheme.background,
|
||||||
|
closedColor: Theme.of(context).colorScheme.background,
|
||||||
|
openColor: Theme.of(context).colorScheme.background,
|
||||||
|
closedShape: InputBorder.none,
|
||||||
|
openElevation: 0,
|
||||||
|
closedElevation: 0,
|
||||||
|
closedBuilder: (context, openForm) => Container(
|
||||||
|
margin: const EdgeInsets.only(top: 6),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: openForm,
|
||||||
|
child: InputDecorator(
|
||||||
|
isEmpty: isEmpty,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: const Icon(
|
contentPadding: const EdgeInsets.all(12),
|
||||||
Icons.label_outline,
|
labelText:
|
||||||
|
'${S.of(context)!.tags}${anyAssigned ? ' (${S.of(context)!.anyAssigned})' : ''}',
|
||||||
|
prefixIcon: const Icon(Icons.label_outline),
|
||||||
),
|
),
|
||||||
suffixIcon: _buildSuffixIcon(context, field),
|
child: SizedBox(
|
||||||
labelText: widget.labelText ?? S.of(context)!.tags,
|
height: 32,
|
||||||
hintText: widget.hintText ?? S.of(context)!.filterTags,
|
child: ListView.separated(
|
||||||
),
|
scrollDirection: Axis.horizontal,
|
||||||
controller: _textEditingController,
|
separatorBuilder: (context, index) =>
|
||||||
),
|
const SizedBox(width: 4),
|
||||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
itemBuilder: (context, index) => values[index],
|
||||||
elevation: 4.0,
|
itemCount: values.length,
|
||||||
shadowColor: Theme.of(context).colorScheme.primary,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suggestionsCallback: (query) {
|
),
|
||||||
final suggestions = widget.selectableOptions.entries
|
)),
|
||||||
.where(
|
openBuilder: (context, closeForm) => FullscreenTagsForm(
|
||||||
(entry) => entry.value.name
|
options: options,
|
||||||
.toLowerCase()
|
onSubmit: closeForm,
|
||||||
.startsWith(query.toLowerCase()),
|
initialValue: field.value,
|
||||||
)
|
allowOnlySelection: allowOnlySelection,
|
||||||
.where((entry) =>
|
allowCreation: allowCreation,
|
||||||
widget.allowCreation ||
|
allowExclude: allowExclude,
|
||||||
(entry.value.documentCount ?? 0) > 0)
|
),
|
||||||
.map((entry) => entry.key)
|
onClosed: (data) {
|
||||||
.toList();
|
if (data != null) {
|
||||||
if (field.value is IdsTagsQuery) {
|
field.didChange(data);
|
||||||
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) {
|
|
||||||
late String? title;
|
|
||||||
switch (data) {
|
|
||||||
case _onlyNotAssignedId:
|
|
||||||
title = S.of(context)!.notAssigned;
|
|
||||||
break;
|
|
||||||
case _anyAssignedId:
|
|
||||||
title = S.of(context)!.anyAssigned;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
title = widget.selectableOptions[data]?.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
final tag = widget.selectableOptions[data];
|
|
||||||
return ListTile(
|
|
||||||
dense: true,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
style: ListTileStyle.list,
|
|
||||||
leading: data != _onlyNotAssignedId && data != _anyAssignedId
|
|
||||||
? Icon(
|
|
||||||
Icons.circle,
|
|
||||||
color: tag?.color,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
title: Text(
|
|
||||||
title ?? '',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onBackground),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onSuggestionSelected: (id) {
|
name: name,
|
||||||
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).padded()
|
|
||||||
] else if (field.value is AnyAssignedTagsQuery) ...[
|
|
||||||
_buildAnyAssignedTag(field).padded()
|
|
||||||
] else ...[
|
|
||||||
if (widget.suggestions != null) widget.suggestions!,
|
|
||||||
// field.value is IdsTagsQuery
|
|
||||||
Wrap(
|
|
||||||
alignment: WrapAlignment.start,
|
|
||||||
runAlignment: WrapAlignment.start,
|
|
||||||
spacing: 4.0,
|
|
||||||
runSpacing: 4.0,
|
|
||||||
children: ((field.value as IdsTagsQuery).queries)
|
|
||||||
.map(
|
|
||||||
(query) => _buildTag(
|
|
||||||
field,
|
|
||||||
query,
|
|
||||||
widget.selectableOptions[query.id],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
).padded(),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
initialValue: widget.initialValue ?? const IdsTagsQuery(),
|
|
||||||
name: widget.name,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget? _buildSuffixIcon(
|
Iterable<Widget> _generateOptions(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
FormFieldState<TagsQuery> field,
|
TagsQuery? query,
|
||||||
|
FormFieldState<TagsQuery?> field,
|
||||||
|
) sync* {
|
||||||
|
if (query == null) {
|
||||||
|
yield Container();
|
||||||
|
} else if (query is IdsTagsQuery) {
|
||||||
|
for (final e in query.queries) {
|
||||||
|
yield _buildTagIdQueryWidget(context, e, field);
|
||||||
|
}
|
||||||
|
} else if (query is OnlyNotAssignedTagsQuery) {
|
||||||
|
yield _buildNotAssignedTagWidget(context, field);
|
||||||
|
} else if (query is AnyAssignedTagsQuery) {
|
||||||
|
for (final e in query.tagIds) {
|
||||||
|
yield _buildAnyAssignedTagWidget(context, e, field, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTagIdQueryWidget(
|
||||||
|
BuildContext context,
|
||||||
|
TagIdQuery e,
|
||||||
|
FormFieldState<TagsQuery?> field,
|
||||||
) {
|
) {
|
||||||
if (_showCreationSuffixIcon && widget.allowCreation) {
|
assert(field.value is IdsTagsQuery);
|
||||||
return IconButton(
|
final formValue = field.value as IdsTagsQuery;
|
||||||
onPressed: () => _onAddTag(context, field),
|
final tag = options[e.id]!;
|
||||||
icon: const Icon(
|
return QueryTagChip(
|
||||||
Icons.new_label,
|
onDeleted: () => field.didChange(formValue.withIdsRemoved([e.id])),
|
||||||
),
|
onSelected: allowExclude
|
||||||
);
|
? () => field.didChange(formValue.withIdQueryToggled(e.id))
|
||||||
}
|
|
||||||
if (_showClearSuffixIcon) {
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
onPressed: _textEditingController.clear,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAddTag(BuildContext context, FormFieldState<TagsQuery> field) async {
|
|
||||||
final Tag? tag = await Navigator.of(context).push<Tag>(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => RepositoryProvider.value(
|
|
||||||
value: context.read<LabelRepository>(),
|
|
||||||
child: AddTagPage(initialValue: _textEditingController.text),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (tag != null) {
|
|
||||||
final tagsQuery = field.value is IdsTagsQuery
|
|
||||||
? field.value as IdsTagsQuery
|
|
||||||
: const IdsTagsQuery();
|
|
||||||
field.didChange(
|
|
||||||
tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tag.id!)]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_textEditingController.clear();
|
|
||||||
// Call has to be delayed as otherwise the framework will not hide the keyboard directly after closing the add page.
|
|
||||||
Future.delayed(
|
|
||||||
const Duration(milliseconds: 100),
|
|
||||||
FocusScope.of(context).unfocus,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNotAssignedTag(FormFieldState<TagsQuery> field) {
|
|
||||||
return ColoredChipWrapper(
|
|
||||||
child: InputChip(
|
|
||||||
labelPadding: const EdgeInsets.symmetric(horizontal: 2),
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
side: BorderSide.none,
|
|
||||||
label: Text(
|
|
||||||
S.of(context)!.notAssigned,
|
|
||||||
),
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
|
|
||||||
onDeleted: () => field.didChange(const IdsTagsQuery()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTag(
|
|
||||||
FormFieldState<TagsQuery> field,
|
|
||||||
TagIdQuery query,
|
|
||||||
Tag? tag,
|
|
||||||
) {
|
|
||||||
final currentQuery = field.value as IdsTagsQuery;
|
|
||||||
final isIncludedTag = currentQuery.includedIds.contains(query.id);
|
|
||||||
if (tag == null) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
return ColoredChipWrapper(
|
|
||||||
child: InputChip(
|
|
||||||
labelPadding: const EdgeInsets.symmetric(horizontal: 2),
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
side: BorderSide.none,
|
|
||||||
label: Text(
|
|
||||||
tag.name,
|
|
||||||
style: TextStyle(
|
|
||||||
color: tag.textColor,
|
|
||||||
decorationColor: tag.textColor,
|
|
||||||
decoration: !isIncludedTag ? TextDecoration.lineThrough : null,
|
|
||||||
decorationThickness: 2.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: widget.excludeAllowed
|
|
||||||
? () => field.didChange(currentQuery.withIdQueryToggled(tag.id!))
|
|
||||||
: null,
|
: null,
|
||||||
|
exclude: e is ExcludeTagIdQuery,
|
||||||
backgroundColor: tag.color,
|
backgroundColor: tag.color,
|
||||||
deleteIconColor: tag.textColor,
|
foregroundColor: tag.textColor,
|
||||||
onDeleted: () => field.didChange(
|
labelText: tag.name,
|
||||||
(field.value as IdsTagsQuery).withIdsRemoved([tag.id!]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAnyAssignedTag(FormFieldState<TagsQuery> field) {
|
Widget _buildNotAssignedTagWidget(
|
||||||
|
BuildContext context,
|
||||||
|
FormFieldState<TagsQuery?> field,
|
||||||
|
) {
|
||||||
|
return QueryTagChip(
|
||||||
|
onDeleted: () => field.didChange(null),
|
||||||
|
exclude: false,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
labelText: S.of(context)!.notAssigned,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAnyAssignedTagWidget(
|
||||||
|
BuildContext context,
|
||||||
|
int e,
|
||||||
|
FormFieldState<TagsQuery?> field,
|
||||||
|
AnyAssignedTagsQuery query,
|
||||||
|
) {
|
||||||
|
return QueryTagChip(
|
||||||
|
onDeleted: () {
|
||||||
|
final updatedQuery = query.withRemoved([e]);
|
||||||
|
if (updatedQuery.tagIds.isEmpty) {
|
||||||
|
field.didChange(const IdsTagsQuery());
|
||||||
|
} else {
|
||||||
|
field.didChange(updatedQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exclude: false,
|
||||||
|
backgroundColor: options[e]!.color,
|
||||||
|
foregroundColor: options[e]!.textColor,
|
||||||
|
labelText: options[e]!.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef TagQueryCallback = void Function(Tag tag);
|
||||||
|
|
||||||
|
class QueryTagChip extends StatelessWidget {
|
||||||
|
final VoidCallback onDeleted;
|
||||||
|
final VoidCallback? onSelected;
|
||||||
|
final bool exclude;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Color? foregroundColor;
|
||||||
|
final String labelText;
|
||||||
|
|
||||||
|
const QueryTagChip({
|
||||||
|
super.key,
|
||||||
|
required this.onDeleted,
|
||||||
|
this.onSelected,
|
||||||
|
required this.exclude,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.foregroundColor,
|
||||||
|
required this.labelText,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return ColoredChipWrapper(
|
return ColoredChipWrapper(
|
||||||
child: InputChip(
|
child: InputChip(
|
||||||
labelPadding: const EdgeInsets.symmetric(horizontal: 2),
|
labelPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
padding: const EdgeInsets.all(4),
|
padding: const EdgeInsets.all(4),
|
||||||
|
selectedColor: backgroundColor,
|
||||||
|
visualDensity: const VisualDensity(vertical: -2),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
label: Text(
|
||||||
|
labelText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: foregroundColor,
|
||||||
|
decorationColor: foregroundColor,
|
||||||
|
decoration: exclude ? TextDecoration.lineThrough : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDeleted: onDeleted,
|
||||||
|
onPressed: onSelected,
|
||||||
|
deleteIconColor: foregroundColor,
|
||||||
|
checkmarkColor: foregroundColor,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
side: BorderSide.none,
|
side: BorderSide.none,
|
||||||
label: Text(S.of(context)!.anyAssigned),
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12),
|
|
||||||
onDeleted: () => field.didChange(const IdsTagsQuery()),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ class _LabelsPageState extends State<LabelsPage>
|
|||||||
onEdit: _openEditTagPage,
|
onEdit: _openEditTagPage,
|
||||||
leadingBuilder: (t) => CircleAvatar(
|
leadingBuilder: (t) => CircleAvatar(
|
||||||
backgroundColor: t.color,
|
backgroundColor: t.color,
|
||||||
child: t.isInboxTag ?? false
|
child: t.isInboxTag
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.inbox,
|
Icons.inbox,
|
||||||
color: t.textColor,
|
color: t.textColor,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
|||||||
final void Function({IdQueryParameter returnValue}) onSubmit;
|
final void Function({IdQueryParameter returnValue}) onSubmit;
|
||||||
final Widget leadingIcon;
|
final Widget leadingIcon;
|
||||||
final String? addNewLabelText;
|
final String? addNewLabelText;
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
FullscreenLabelForm({
|
FullscreenLabelForm({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -25,6 +26,7 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
|||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.leadingIcon,
|
required this.leadingIcon,
|
||||||
this.addNewLabelText,
|
this.addNewLabelText,
|
||||||
|
this.autofocus = true,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
|
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
|
||||||
),
|
),
|
||||||
@@ -49,6 +51,7 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
_textEditingController.addListener(() => setState(() {
|
_textEditingController.addListener(() => setState(() {
|
||||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||||
}));
|
}));
|
||||||
|
if (widget.autofocus) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
//Delay keyboard popup to ensure open animation is finished before.
|
//Delay keyboard popup to ensure open animation is finished before.
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
@@ -57,6 +60,7 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -215,7 +219,7 @@ class _FullscreenLabelFormState<T extends Label>
|
|||||||
|
|
||||||
String? _buildHintText() {
|
String? _buildHintText() {
|
||||||
if (widget.initialValue?.isSet ?? false) {
|
if (widget.initialValue?.isSet ?? false) {
|
||||||
return widget.options[widget.initialValue!.id]?.name ?? 'undefined';
|
return widget.options[widget.initialValue!.id]!.name;
|
||||||
}
|
}
|
||||||
if (widget.initialValue?.onlyNotAssigned ?? false) {
|
if (widget.initialValue?.onlyNotAssigned ?? false) {
|
||||||
return S.of(context)!.notAssigned;
|
return S.of(context)!.notAssigned;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
|
|||||||
|
|
||||||
String _buildText(BuildContext context, IdQueryParameter? value) {
|
String _buildText(BuildContext context, IdQueryParameter? value) {
|
||||||
if (value?.isSet ?? false) {
|
if (value?.isSet ?? false) {
|
||||||
return options[value!.id]?.name ?? 'undefined';
|
return options[value!.id]!.name;
|
||||||
} else if (value?.onlyNotAssigned ?? false) {
|
} else if (value?.onlyNotAssigned ?? false) {
|
||||||
return S.of(context)!.notAssigned;
|
return S.of(context)!.notAssigned;
|
||||||
} else if (value?.onlyAssigned ?? false) {
|
} else if (value?.onlyAssigned ?? false) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class RadioSettingsDialog<T> extends StatefulWidget {
|
class RadioSettingsDialog<T> extends StatefulWidget {
|
||||||
@@ -38,14 +40,11 @@ class _RadioSettingsDialogState<T> extends State<RadioSettingsDialog<T>> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
actions: [
|
actions: [
|
||||||
|
const DialogCancelButton(),
|
||||||
widget.confirmButton ??
|
widget.confirmButton ??
|
||||||
TextButton(
|
DialogConfirmButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
returnValue: _groupValue,
|
||||||
child: Text(S.of(context)!.cancel)),
|
),
|
||||||
widget.confirmButton ??
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context, _groupValue),
|
|
||||||
child: Text(S.of(context)!.ok)),
|
|
||||||
],
|
],
|
||||||
title: widget.titleText != null ? Text(widget.titleText!) : null,
|
title: widget.titleText != null ? Text(widget.titleText!) : null,
|
||||||
content: Column(
|
content: Column(
|
||||||
|
|||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Are you sure you want to continue?"
|
"areYouSureYouWantToContinue": "Are you sure you want to continue?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}",
|
||||||
|
"anyTag": "Any",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "All",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Bist Du sicher, dass Du fortfahren möchtest?"
|
"areYouSureYouWantToContinue": "Bist Du sicher, dass Du fortfahren möchtest?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{Diese Operation wird die Tags {tags} dem ausgewählten Dokument hinzufügen.} other{Diese Operation wird die Tags {tags} den {count} ausgewählten Dokumenten hinzufügen.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{Diese Operation wird die Tags {tags} vom ausgewählten Dokument entfernen.} other{Diese Operation wird die Tags {tags} von den {count} ausgewählten Dokumenten entfernen.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{Diese Operation wird die Tags {addTags} hinzufügen und die Tags {removeTags} von dem ausgewählten Dokument entfernen.} other{Diese Operation wird die Tags {addTags} hinzufügen und die Tags {removeTags} von {count} ausgewählten Dokumenten entfernen.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{Diese Operation wird den Korrespondent {correspondent} dem ausgewählten Dokument zuweisen.} other{Diese Operation wird den Korrespondent {correspondent} den {count} ausgewählten Dokumenten zuweisen.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{Diese Operation wird den Dokumenttyp {docType} dem ausgewählten Dokument zuweisen.} other{Diese Operation wird den Dokumenttyp {docType} den {count} ausgewählten Dokumenten zuweisen.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{Diese Operation wird den Speicherpfad {path} dem ausgewählten Dokument zuweisen.} other{Diese Operation wird den Speicherpfad {path} den {count} ausgewählten Dokumenten zuweisen.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{Diese Operation wird den Korrespondent {correspondent} vom ausgewählten Dokument entfernen.} other{Diese Operation wird den Korrespondenten {correspondent} von {count} ausgewählten Dokumenten entfernen.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{Diese Operation wird den Dokumenttyp {docType} vom ausgewählten Dokument entfernen.} other{Diese Operation wird den Dokumenttyp {docType} von {count} ausgewählten Dokumenten entfernen.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{Diese Operation wird den Speicherpfad {path} vom ausgewählten Dokument entfernen.} other{Diese Operation wird den Speicherpfad {path} von {count} ausgewählten Dokumenten entfernen.}}",
|
||||||
|
"anyTag": "Irgendeines",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "Alle",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Are you sure you want to continue?"
|
"areYouSureYouWantToContinue": "Are you sure you want to continue?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}",
|
||||||
|
"anyTag": "Any",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "All",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Are you sure you want to continue?"
|
"areYouSureYouWantToContinue": "Are you sure you want to continue?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}",
|
||||||
|
"anyTag": "Any",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "All",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Are you sure you want to continue?"
|
"areYouSureYouWantToContinue": "Are you sure you want to continue?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}",
|
||||||
|
"anyTag": "Any",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "All",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Are you sure you want to continue?"
|
"areYouSureYouWantToContinue": "Are you sure you want to continue?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}",
|
||||||
|
"anyTag": "Any",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "All",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -703,5 +703,31 @@
|
|||||||
"@confirmAction": {
|
"@confirmAction": {
|
||||||
"description": "Typically used as a title to confirm a previously selected action"
|
"description": "Typically used as a title to confirm a previously selected action"
|
||||||
},
|
},
|
||||||
"areYouSureYouWantToContinue": "Are you sure you want to continue?"
|
"areYouSureYouWantToContinue": "Are you sure you want to continue?",
|
||||||
|
"bulkEditTagsAddMessage": "{count, plural, one{This operation will add the tags {tags} to the selected document.} other{This operation will add the tags {tags} to {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsAddMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk adding tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsRemoveMessage": "{count, plural, one{This operation will remove the tags {tags} from the selected document.} other{This operation will remove the tags {tags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsRemoveMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when bulk removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditTagsModifyMessage": "{count, plural, one{This operation will add the tags {addTags} and remove the tags {removeTags} from the selected document.} other{This operation will add the tags {addTags} and remove the tags {removeTags} from {count} selected documents.}}",
|
||||||
|
"@bulkEditTagsModifyMessage": {
|
||||||
|
"description": "Message of the confirmation dialog when both adding and removing tags."
|
||||||
|
},
|
||||||
|
"bulkEditCorrespondentAssignMessage": "{count, plural, one{This operation will assign the correspondent {correspondent} to the selected document.} other{This operation will assign the correspondent {correspondent} to {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeAssignMessage": "{count, plural, one{This operation will assign the document type {docType} to the selected document.} other{This operation will assign the documentType {docType} to {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathAssignMessage": "{count, plural, one{This operation will assign the storage path {path} to the selected document.} other{This operation will assign the storage path {path} to {count} selected documents.}}",
|
||||||
|
"bulkEditCorrespondentRemoveMessage": "{count, plural, one{This operation will remove the correspondent {correspondent} from the selected document.} other{This operation will remove the correspondent {correspondent} from {count} selected documents.}}",
|
||||||
|
"bulkEditDocumentTypeRemoveMessage": "{count, plural, one{This operation will remove the document type {docType} from the selected document.} other{This operation will remove the document type {docType} from {count} selected documents.}}",
|
||||||
|
"bulkEditStoragePathRemoveMessage": "{count, plural, one{This operation will remove the storage path {path} from the selected document.} other{This operation will remove the storage path {path} from {count} selected documents.}}",
|
||||||
|
"anyTag": "Any",
|
||||||
|
"@anyTag": {
|
||||||
|
"description": "Label shown when any tag should be filtered"
|
||||||
|
},
|
||||||
|
"allTags": "All",
|
||||||
|
"@allTags": {
|
||||||
|
"description": "Label shown when a document has to be assigned to all selected tags"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ class Tag extends Label {
|
|||||||
|
|
||||||
final Color? textColor;
|
final Color? textColor;
|
||||||
|
|
||||||
final bool? isInboxTag;
|
final bool isInboxTag;
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
@JsonKey(name: colorKey)
|
@JsonKey(name: colorKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user