mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 20:07:51 -06:00
feat: Replaced old label form fields with full page search, removed badge from edit button in document details
This commit is contained in:
@@ -61,9 +61,8 @@ class _BulkEditLabelBottomSheetState<T extends Label>
|
||||
initialValue:
|
||||
IdQueryParameter.fromId(widget.initialValue),
|
||||
name: "labelFormField",
|
||||
labelOptions: widget.availableOptionsSelector(state),
|
||||
textFieldLabel: widget.formFieldLabel,
|
||||
formBuilderState: _formKey.currentState,
|
||||
options: widget.availableOptionsSelector(state),
|
||||
labelText: widget.formFieldLabel,
|
||||
prefixIcon: widget.formFieldPrefixIcon,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
final DocumentChangedNotifier _notifier;
|
||||
final LocalNotificationService _notificationService;
|
||||
final LabelRepository _labelRepository;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
DocumentDetailsCubit(
|
||||
this._api,
|
||||
this._labelRepository,
|
||||
@@ -207,9 +207,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
for (final element in _subscriptions) {
|
||||
await element.cancel();
|
||||
}
|
||||
_labelRepository.removeListener(this);
|
||||
_notifier.removeListener(this);
|
||||
await super.close();
|
||||
}
|
||||
|
||||
@@ -218,32 +218,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
Widget _buildEditButton() {
|
||||
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
final _filteredSuggestions =
|
||||
state.suggestions?.documentDifference(state.document);
|
||||
// final _filteredSuggestions =
|
||||
// state.suggestions?.documentDifference(state.document);
|
||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivityState) {
|
||||
if (!connectivityState.isConnected) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return b.Badge(
|
||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||
showBadge: _filteredSuggestions?.hasSuggestions ?? false,
|
||||
child: Tooltip(
|
||||
message: S.of(context)!.editDocumentTooltip,
|
||||
preferBelow: false,
|
||||
verticalOffset: 40,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.edit),
|
||||
onPressed: () => _onEdit(state.document),
|
||||
),
|
||||
return Tooltip(
|
||||
message: S.of(context)!.editDocumentTooltip,
|
||||
preferBelow: false,
|
||||
verticalOffset: 40,
|
||||
child: FloatingActionButton(
|
||||
child: const Icon(Icons.edit),
|
||||
onPressed: () => _onEdit(state.document),
|
||||
),
|
||||
badgeContent: Text(
|
||||
'${_filteredSuggestions?.suggestionsCount ?? 0}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
badgeColor: Colors.red,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -12,10 +12,8 @@ part 'document_edit_cubit.freezed.dart';
|
||||
class DocumentEditCubit extends Cubit<DocumentEditState> {
|
||||
final DocumentModel _initialDocument;
|
||||
final PaperlessDocumentsApi _docsApi;
|
||||
|
||||
final DocumentChangedNotifier _notifier;
|
||||
final LabelRepository _labelRepository;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final DocumentChangedNotifier _notifier;
|
||||
|
||||
DocumentEditCubit(
|
||||
this._labelRepository,
|
||||
@@ -23,19 +21,16 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
||||
this._notifier, {
|
||||
required DocumentModel document,
|
||||
}) : _initialDocument = document,
|
||||
super(
|
||||
DocumentEditState(
|
||||
document: document,
|
||||
correspondents: _labelRepository.state.correspondents,
|
||||
documentTypes: _labelRepository.state.documentTypes,
|
||||
storagePaths: _labelRepository.state.storagePaths,
|
||||
tags: _labelRepository.state.tags,
|
||||
),
|
||||
) {
|
||||
super(DocumentEditState(document: document)) {
|
||||
_notifier.addListener(this, onUpdated: replace);
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) => emit(state.copyWith()),
|
||||
onChanged: (labels) => emit(state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
storagePaths: labels.storagePaths,
|
||||
tags: labels.tags,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,10 +63,8 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
for (final sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
_notifier.removeListener(this);
|
||||
_labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +150,10 @@ class __$$_DocumentEditStateCopyWithImpl<$Res>
|
||||
class _$_DocumentEditState implements _DocumentEditState {
|
||||
const _$_DocumentEditState(
|
||||
{required this.document,
|
||||
required final Map<int, Correspondent> correspondents,
|
||||
required final Map<int, DocumentType> documentTypes,
|
||||
required final Map<int, StoragePath> storagePaths,
|
||||
required final Map<int, Tag> tags})
|
||||
final Map<int, Correspondent> correspondents = const {},
|
||||
final Map<int, DocumentType> documentTypes = const {},
|
||||
final Map<int, StoragePath> storagePaths = const {},
|
||||
final Map<int, Tag> tags = const {}})
|
||||
: _correspondents = correspondents,
|
||||
_documentTypes = documentTypes,
|
||||
_storagePaths = storagePaths,
|
||||
@@ -163,6 +163,7 @@ class _$_DocumentEditState implements _DocumentEditState {
|
||||
final DocumentModel document;
|
||||
final Map<int, Correspondent> _correspondents;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<int, Correspondent> get correspondents {
|
||||
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
|
||||
// ignore: implicit_dynamic_type
|
||||
@@ -171,6 +172,7 @@ class _$_DocumentEditState implements _DocumentEditState {
|
||||
|
||||
final Map<int, DocumentType> _documentTypes;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<int, DocumentType> get documentTypes {
|
||||
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
|
||||
// ignore: implicit_dynamic_type
|
||||
@@ -179,6 +181,7 @@ class _$_DocumentEditState implements _DocumentEditState {
|
||||
|
||||
final Map<int, StoragePath> _storagePaths;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<int, StoragePath> get storagePaths {
|
||||
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
|
||||
// ignore: implicit_dynamic_type
|
||||
@@ -187,6 +190,7 @@ class _$_DocumentEditState implements _DocumentEditState {
|
||||
|
||||
final Map<int, Tag> _tags;
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<int, Tag> get tags {
|
||||
if (_tags is EqualUnmodifiableMapView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
@@ -234,10 +238,10 @@ class _$_DocumentEditState implements _DocumentEditState {
|
||||
abstract class _DocumentEditState implements DocumentEditState {
|
||||
const factory _DocumentEditState(
|
||||
{required final DocumentModel document,
|
||||
required final Map<int, Correspondent> correspondents,
|
||||
required final Map<int, DocumentType> documentTypes,
|
||||
required final Map<int, StoragePath> storagePaths,
|
||||
required final Map<int, Tag> tags}) = _$_DocumentEditState;
|
||||
final Map<int, Correspondent> correspondents,
|
||||
final Map<int, DocumentType> documentTypes,
|
||||
final Map<int, StoragePath> storagePaths,
|
||||
final Map<int, Tag> tags}) = _$_DocumentEditState;
|
||||
|
||||
@override
|
||||
DocumentModel get document;
|
||||
|
||||
@@ -4,9 +4,9 @@ part of 'document_edit_cubit.dart';
|
||||
class DocumentEditState with _$DocumentEditState {
|
||||
const factory DocumentEditState({
|
||||
required DocumentModel document,
|
||||
required Map<int, Correspondent> correspondents,
|
||||
required Map<int, DocumentType> documentTypes,
|
||||
required Map<int, StoragePath> storagePaths,
|
||||
required Map<int, Tag> tags,
|
||||
@Default({}) Map<int, Correspondent> correspondents,
|
||||
@Default({}) Map<int, DocumentType> documentTypes,
|
||||
@Default({}) Map<int, StoragePath> storagePaths,
|
||||
@Default({}) Map<int, Tag> tags,
|
||||
}) = _DocumentEditState;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
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:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
@@ -56,6 +57,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
||||
builder: (context, state) {
|
||||
log("Updated state. correspondents have ${state.correspondents.length} items.");
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
@@ -95,18 +97,116 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(state.document.created)
|
||||
.padded(),
|
||||
_buildCorrespondentFormField(
|
||||
state.document.correspondent,
|
||||
state.correspondents,
|
||||
// Correspondent form field
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialValue) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(
|
||||
initialName: initialValue,
|
||||
),
|
||||
),
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent,
|
||||
options: context
|
||||
.watch<DocumentEditCubit>()
|
||||
.state
|
||||
.correspondents,
|
||||
initialValue: IdQueryParameter.fromId(
|
||||
state.document.correspondent,
|
||||
),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
),
|
||||
if (_filteredSuggestions
|
||||
?.hasSuggestedCorrespondents ??
|
||||
false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions:
|
||||
_filteredSuggestions!.correspondents,
|
||||
itemBuilder: (context, itemData) =>
|
||||
ActionChip(
|
||||
label: Text(
|
||||
state.correspondents[itemData]!.name),
|
||||
onPressed: () {
|
||||
_formKey
|
||||
.currentState?.fields[fkCorrespondent]
|
||||
?.didChange(
|
||||
IdQueryParameter.fromId(itemData),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
_buildDocumentTypeFormField(
|
||||
state.document.documentType,
|
||||
state.documentTypes,
|
||||
// DocumentType form field
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (currentInput) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(
|
||||
initialName: currentInput,
|
||||
),
|
||||
),
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue: IdQueryParameter.fromId(
|
||||
state.document.documentType),
|
||||
options: state.documentTypes,
|
||||
name: _DocumentEditPageState.fkDocumentType,
|
||||
prefixIcon:
|
||||
const Icon(Icons.description_outlined),
|
||||
),
|
||||
if (_filteredSuggestions
|
||||
?.hasSuggestedDocumentTypes ??
|
||||
false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions:
|
||||
_filteredSuggestions!.documentTypes,
|
||||
itemBuilder: (context, itemData) =>
|
||||
ActionChip(
|
||||
label: Text(
|
||||
state.documentTypes[itemData]!.name),
|
||||
onPressed: () => _formKey
|
||||
.currentState?.fields[fkDocumentType]
|
||||
?.didChange(
|
||||
IdQueryParameter.fromId(itemData),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
_buildStoragePathFormField(
|
||||
state.document.storagePath,
|
||||
state.storagePaths,
|
||||
// StoragePath form field
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<StoragePath>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialValue) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddStoragePathPage(
|
||||
initalName: initialValue),
|
||||
),
|
||||
addLabelText: S.of(context)!.addStoragePath,
|
||||
labelText: S.of(context)!.storagePath,
|
||||
options: state.storagePaths,
|
||||
initialValue: IdQueryParameter.fromId(
|
||||
state.document.storagePath),
|
||||
name: fkStoragePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// Tag form field
|
||||
TagFormField(
|
||||
initialValue: IdsTagsQuery.included(
|
||||
state.document.tags.toList()),
|
||||
@@ -187,96 +287,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStoragePathFormField(
|
||||
int? initialId,
|
||||
Map<int, StoragePath> options,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
LabelFormField<StoragePath>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddStoragePathPage(initalName: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context)!.storagePath,
|
||||
labelOptions: options,
|
||||
initialValue: IdQueryParameter.fromId(initialId),
|
||||
name: fkStoragePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCorrespondentFormField(
|
||||
int? initialId, Map<int, Correspondent> options) {
|
||||
return Column(
|
||||
children: [
|
||||
LabelFormField<Correspondent>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialValue) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(initialName: initialValue),
|
||||
),
|
||||
textFieldLabel: S.of(context)!.correspondent,
|
||||
labelOptions: options,
|
||||
initialValue: IdQueryParameter.fromId(initialId),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
),
|
||||
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions: _filteredSuggestions!.correspondents,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(options[itemData]!.name),
|
||||
onPressed: () => _formKey.currentState?.fields[fkCorrespondent]
|
||||
?.didChange((IdQueryParameter.fromId(itemData))),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentTypeFormField(
|
||||
int? initialId,
|
||||
Map<int, DocumentType> options,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
LabelFormField<DocumentType>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (currentInput) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(
|
||||
initialName: currentInput,
|
||||
),
|
||||
),
|
||||
textFieldLabel: S.of(context)!.documentType,
|
||||
initialValue: IdQueryParameter.fromId(initialId),
|
||||
labelOptions: options,
|
||||
name: fkDocumentType,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
),
|
||||
if (_filteredSuggestions?.hasSuggestedDocumentTypes ?? false)
|
||||
_buildSuggestionsSkeleton<int>(
|
||||
suggestions: _filteredSuggestions!.documentTypes,
|
||||
itemBuilder: (context, itemData) => ActionChip(
|
||||
label: Text(options[itemData]!.name),
|
||||
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
|
||||
?.didChange(IdQueryParameter.fromId(itemData)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSubmit(DocumentModel document) async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final values = _formKey.currentState!.value;
|
||||
@@ -308,7 +318,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
Widget _buildTitleFormField(String? initialTitle) {
|
||||
return FormBuilderTextField(
|
||||
name: fkTitle,
|
||||
validator: FormBuilderValidators.required(),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.title),
|
||||
),
|
||||
@@ -374,3 +389,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
).padded();
|
||||
}
|
||||
}
|
||||
|
||||
// class SampleWidget extends StatefulWidget {
|
||||
// const SampleWidget({super.key});
|
||||
|
||||
// @override
|
||||
// State<SampleWidget> createState() => _SampleWidgetState();
|
||||
// }
|
||||
|
||||
// class _SampleWidgetState extends State<SampleWidget> {
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return BlocBuilder<OptionsBloc, OptionsState>(
|
||||
// builder: (context, state) {
|
||||
// return OptionsFormField(
|
||||
// options: state.options,
|
||||
// onAddOption: (option) {
|
||||
// // This will call the repository and will cause a new state containing the new option to be emitted.
|
||||
// context.read<OptionsBloc>().addOption(option);
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// class OptionsFormField extends StatefulWidget {
|
||||
// final List<Option> options;
|
||||
// final void Function(Option option) onAddOption;
|
||||
|
||||
|
||||
// const OptionsFormField({
|
||||
// super.key,
|
||||
// required this.options,
|
||||
// required this.onAddOption,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// State<OptionsFormField> createState() => _OptionsFormFieldState();
|
||||
// }
|
||||
|
||||
// class _OptionsFormFieldState extends State<OptionsFormField> {
|
||||
// final TextEditingController _controller;
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return TextFormField(
|
||||
// onTap: () async {
|
||||
// // User creates new option...
|
||||
// final Option option = await showOptionCreationForm();
|
||||
// widget.onAddOption(option);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -23,14 +23,19 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
|
||||
DocumentSearchCubit(this.api, this.notifier, this._labelRepository)
|
||||
: super(const DocumentSearchState()) {
|
||||
_labelRepository.addListener(this, onChanged: (labels) {
|
||||
emit(state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
tags: labels.tags,
|
||||
storagePaths: labels.storagePaths,
|
||||
));
|
||||
});
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
tags: labels.tags,
|
||||
storagePaths: labels.storagePaths,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
notifier.addListener(
|
||||
this,
|
||||
onDeleted: remove,
|
||||
@@ -101,6 +106,7 @@ class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
|
||||
@override
|
||||
Future<void> close() {
|
||||
notifier.removeListener(this);
|
||||
_labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
|
||||
as s;
|
||||
import 'package:paperless_mobile/features/document_search/view/document_search_page.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
@@ -27,7 +28,7 @@ class SliverSearchBar extends StatelessWidget {
|
||||
maxExtent: kToolbarHeight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: SearchBar(
|
||||
child: s.SearchBar(
|
||||
height: kToolbarHeight,
|
||||
supportingText: S.of(context)!.searchDocuments,
|
||||
onTap: () => showDocumentSearchPage(context),
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -97,7 +97,12 @@ class _DocumentUploadPreparationPageState
|
||||
name: DocumentModel.titleKey,
|
||||
initialValue:
|
||||
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
|
||||
validator: FormBuilderValidators.required(),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context)!.title,
|
||||
suffixIcon: IconButton(
|
||||
@@ -189,30 +194,32 @@ class _DocumentUploadPreparationPageState
|
||||
),
|
||||
// Correspondent
|
||||
LabelFormField<Correspondent>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialName) =>
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddCorrespondentPage(initialName: initialName),
|
||||
),
|
||||
textFieldLabel: S.of(context)!.correspondent + " *",
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent + " *",
|
||||
name: DocumentModel.correspondentKey,
|
||||
labelOptions: state.correspondents,
|
||||
options: state.correspondents,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
),
|
||||
// Document type
|
||||
LabelFormField<DocumentType>(
|
||||
notAssignedSelectable: false,
|
||||
formBuilderState: _formKey.currentState,
|
||||
labelCreationWidgetBuilder: (initialName) =>
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
addLabelPageBuilder: (initialName) =>
|
||||
RepositoryProvider.value(
|
||||
value: context.read<LabelRepository>(),
|
||||
child: AddDocumentTypePage(initialName: initialName),
|
||||
),
|
||||
textFieldLabel: S.of(context)!.documentType + " *",
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType + " *",
|
||||
name: DocumentModel.documentTypeKey,
|
||||
labelOptions: state.documentTypes,
|
||||
options: state.documentTypes,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
),
|
||||
TagFormField(
|
||||
|
||||
@@ -24,12 +24,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
DocumentsCubit(this.api, this.notifier, this._labelRepository)
|
||||
: super(DocumentsState(
|
||||
correspondents: _labelRepository.state.correspondents,
|
||||
documentTypes: _labelRepository.state.documentTypes,
|
||||
storagePaths: _labelRepository.state.storagePaths,
|
||||
tags: _labelRepository.state.tags,
|
||||
)) {
|
||||
: super(const DocumentsState()) {
|
||||
notifier.addListener(
|
||||
this,
|
||||
onUpdated: (document) {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
part of 'documents_cubit.dart';
|
||||
|
||||
@JsonSerializable(ignoreUnannotated: true)
|
||||
@JsonSerializable()
|
||||
class DocumentsState extends DocumentPagingState {
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
final List<DocumentModel> selection;
|
||||
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
final Map<int, Correspondent> correspondents;
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
final Map<int, DocumentType> documentTypes;
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
final Map<int, Tag> tags;
|
||||
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||
final Map<int, StoragePath> storagePaths;
|
||||
|
||||
@JsonKey()
|
||||
final ViewType viewType;
|
||||
|
||||
const DocumentsState({
|
||||
@@ -53,15 +57,14 @@ class DocumentsState extends DocumentPagingState {
|
||||
);
|
||||
}
|
||||
|
||||
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
|
||||
_$DocumentsStateFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
selection,
|
||||
viewType,
|
||||
correspondents,
|
||||
documentTypes,
|
||||
tags,
|
||||
storagePaths,
|
||||
...super.props,
|
||||
];
|
||||
|
||||
@@ -79,4 +82,9 @@ class DocumentsState extends DocumentPagingState {
|
||||
value: value,
|
||||
);
|
||||
}
|
||||
|
||||
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
|
||||
_$DocumentsStateFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
Widget _buildFullView(BuildContext context) {
|
||||
if (showLoadingPlaceholder) {
|
||||
//TODO: Build detailed loading animation
|
||||
return DocumentsListLoadingWidget.sliver();
|
||||
return const DocumentsListLoadingWidget.sliver();
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d
|
||||
class DocumentListItem extends DocumentItem {
|
||||
static const _a4AspectRatio = 1 / 1.4142;
|
||||
|
||||
const DocumentListItem({
|
||||
DocumentListItem({
|
||||
super.key,
|
||||
required super.document,
|
||||
required super.isSelected,
|
||||
@@ -28,7 +28,9 @@ class DocumentListItem extends DocumentItem {
|
||||
required super.correspondents,
|
||||
required super.documentTypes,
|
||||
required super.storagePaths,
|
||||
});
|
||||
}) {
|
||||
print(tags.keys.join(", "));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -64,11 +66,14 @@ class DocumentListItem extends DocumentItem {
|
||||
absorbing: isSelectionActive,
|
||||
child: TagsWidget(
|
||||
isClickable: isLabelClickable,
|
||||
tags: document.tags.map((e) => tags[e]!).toList(),
|
||||
tags: document.tags
|
||||
.where((e) => tags.containsKey(e))
|
||||
.map((e) => tags[e]!)
|
||||
.toList(),
|
||||
isMultiLine: false,
|
||||
onTagSelected: (id) => onTagSelected?.call(id),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Padding(
|
||||
|
||||
@@ -155,10 +155,9 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
|
||||
Widget _buildDocumentTypeFormField() {
|
||||
return LabelFormField<DocumentType>(
|
||||
formBuilderState: widget.formKey.currentState,
|
||||
name: DocumentFilterForm.fkDocumentType,
|
||||
labelOptions: widget.documentTypes,
|
||||
textFieldLabel: S.of(context)!.documentType,
|
||||
options: widget.documentTypes,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue: widget.initialFilter.documentType,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
);
|
||||
@@ -166,10 +165,9 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
|
||||
Widget _buildCorrespondentFormField() {
|
||||
return LabelFormField<Correspondent>(
|
||||
formBuilderState: widget.formKey.currentState,
|
||||
name: DocumentFilterForm.fkCorrespondent,
|
||||
labelOptions: widget.correspondents,
|
||||
textFieldLabel: S.of(context)!.correspondent,
|
||||
options: widget.correspondents,
|
||||
labelText: S.of(context)!.correspondent,
|
||||
initialValue: widget.initialFilter.correspondent,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
);
|
||||
@@ -177,10 +175,9 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
|
||||
|
||||
Widget _buildStoragePathFormField() {
|
||||
return LabelFormField<StoragePath>(
|
||||
formBuilderState: widget.formKey.currentState,
|
||||
name: DocumentFilterForm.fkStoragePath,
|
||||
labelOptions: widget.storagePaths,
|
||||
textFieldLabel: S.of(context)!.storagePath,
|
||||
options: widget.storagePaths,
|
||||
labelText: S.of(context)!.storagePath,
|
||||
initialValue: widget.initialFilter.storagePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
);
|
||||
|
||||
@@ -8,7 +8,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'edit_label_state.dart';
|
||||
part 'edit_label_cubit.freezed.dart';
|
||||
|
||||
class EditLabelCubit extends Cubit<EditLabelState> with LabelCubitMixin {
|
||||
class EditLabelCubit extends Cubit<EditLabelState>
|
||||
with LabelCubitMixin<EditLabelState> {
|
||||
@override
|
||||
final LabelRepository labelRepository;
|
||||
|
||||
@@ -29,16 +30,4 @@ class EditLabelCubit extends Cubit<EditLabelState> with LabelCubitMixin {
|
||||
labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<int, Correspondent> get correspondents => state.correspondents;
|
||||
|
||||
@override
|
||||
Map<int, DocumentType> get documentTypes => state.documentTypes;
|
||||
|
||||
@override
|
||||
Map<int, StoragePath> get storagePaths => state.storagePaths;
|
||||
|
||||
@override
|
||||
Map<int, Tag> get tags => state.tags;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class AddLabelFormWidget<T extends Label> extends StatelessWidget {
|
||||
title: pageTitle,
|
||||
),
|
||||
body: LabelForm<T>(
|
||||
autofocusNameField: true,
|
||||
initialValue: label,
|
||||
fromJsonT: fromJsonT,
|
||||
submitButtonConfig: SubmitButtonConfig<T>(
|
||||
|
||||
@@ -72,6 +72,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
body: LabelForm<T>(
|
||||
autofocusNameField: false,
|
||||
initialValue: label,
|
||||
fromJsonT: fromJsonT,
|
||||
submitButtonConfig: SubmitButtonConfig<T>(
|
||||
|
||||
@@ -11,17 +11,20 @@ class EditCorrespondentPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
lazy: false,
|
||||
create: (context) => EditLabelCubit(
|
||||
context.read(),
|
||||
),
|
||||
child: EditLabelPage<Correspondent>(
|
||||
label: correspondent,
|
||||
fromJsonT: Correspondent.fromJson,
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().addCorrespondent(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeCorrespondent(label),
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
return EditLabelPage<Correspondent>(
|
||||
label: correspondent,
|
||||
fromJsonT: Correspondent.fromJson,
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().replaceCorrespondent(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeCorrespondent(label),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class EditDocumentTypePage extends StatelessWidget {
|
||||
label: documentType,
|
||||
fromJsonT: DocumentType.fromJson,
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().addDocumentType(label),
|
||||
context.read<EditLabelCubit>().replaceDocumentType(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeDocumentType(label),
|
||||
),
|
||||
|
||||
@@ -19,7 +19,7 @@ class EditStoragePathPage extends StatelessWidget {
|
||||
label: storagePath,
|
||||
fromJsonT: StoragePath.fromJson,
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().addStoragePath(label),
|
||||
context.read<EditLabelCubit>().replaceStoragePath(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeStoragePath(label),
|
||||
additionalFields: [
|
||||
|
||||
@@ -22,7 +22,7 @@ class EditTagPage extends StatelessWidget {
|
||||
label: tag,
|
||||
fromJsonT: Tag.fromJson,
|
||||
onSubmit: (context, label) =>
|
||||
context.read<EditLabelCubit>().addTag(label),
|
||||
context.read<EditLabelCubit>().replaceTag(label),
|
||||
onDelete: (context, label) =>
|
||||
context.read<EditLabelCubit>().removeTag(label),
|
||||
additionalFields: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/type/types.dart';
|
||||
@@ -25,7 +25,7 @@ class SubmitButtonConfig<T extends Label> {
|
||||
class LabelForm<T extends Label> extends StatefulWidget {
|
||||
final T? initialValue;
|
||||
|
||||
final SubmitButtonConfig submitButtonConfig;
|
||||
final SubmitButtonConfig<T> submitButtonConfig;
|
||||
|
||||
/// FromJson method to parse the form field values into a label instance.
|
||||
final T Function(Map<String, dynamic> json) fromJsonT;
|
||||
@@ -33,12 +33,15 @@ class LabelForm<T extends Label> extends StatefulWidget {
|
||||
/// List of additionally rendered form fields.
|
||||
final List<Widget> additionalFields;
|
||||
|
||||
final bool autofocusNameField;
|
||||
|
||||
const LabelForm({
|
||||
Key? key,
|
||||
required this.initialValue,
|
||||
required this.fromJsonT,
|
||||
this.additionalFields = const [],
|
||||
required this.submitButtonConfig,
|
||||
required this.autofocusNameField,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -74,12 +77,18 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
child: ListView(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
autofocus: widget.autofocusNameField,
|
||||
name: Label.nameKey,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context)!.name,
|
||||
errorText: _errors[Label.nameKey],
|
||||
),
|
||||
validator: FormBuilderValidators.required(),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialValue: widget.initialValue?.name,
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
),
|
||||
@@ -148,6 +157,8 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
|
||||
Navigator.pop(context, createdLabel);
|
||||
} on PaperlessServerException catch (error, stackTrace) {
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on PaperlessValidationErrors catch (errors) {
|
||||
setState(() => _errors = errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -75,6 +76,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
/// Fetches inbox tag ids and loads the inbox items (documents).
|
||||
///
|
||||
Future<void> loadInbox() async {
|
||||
debugPrint("Initializing inbox...");
|
||||
final inboxTags = await _labelRepository.findAllTags().then(
|
||||
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
|
||||
);
|
||||
|
||||
@@ -199,14 +199,16 @@ class _InboxPageState extends State<InboxPage>
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(S.of(context)!.cancel),
|
||||
child: Text(
|
||||
S.of(context)!.cancel,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
S.of(context)!.ok,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
child: Text(S.of(context)!.ok),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -30,16 +30,4 @@ class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
|
||||
labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<int, Correspondent> get correspondents => state.correspondents;
|
||||
|
||||
@override
|
||||
Map<int, DocumentType> get documentTypes => state.documentTypes;
|
||||
|
||||
@override
|
||||
Map<int, StoragePath> get storagePaths => state.storagePaths;
|
||||
|
||||
@override
|
||||
Map<int, Tag> get tags => state.tags;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
|
||||
///
|
||||
/// Mixin which adds functionality to manage labels to [Bloc]s.
|
||||
///
|
||||
mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
LabelRepository get labelRepository;
|
||||
|
||||
Map<int, Correspondent> get correspondents;
|
||||
Map<int, DocumentType> get documentTypes;
|
||||
Map<int, Tag> get tags;
|
||||
Map<int, StoragePath> get storagePaths;
|
||||
|
||||
Future<Correspondent> addCorrespondent(Correspondent item) async {
|
||||
assert(item.id == null);
|
||||
final addedItem = await labelRepository.createCorrespondent(item);
|
||||
@@ -28,7 +27,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeCorrespondent(Correspondent item) async {
|
||||
assert(item.id != null);
|
||||
if (correspondents.containsKey(item.id)) {
|
||||
if (labelRepository.state.correspondents.containsKey(item.id)) {
|
||||
await labelRepository.deleteCorrespondent(item);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +50,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeDocumentType(DocumentType item) async {
|
||||
assert(item.id != null);
|
||||
if (documentTypes.containsKey(item.id)) {
|
||||
if (labelRepository.state.documentTypes.containsKey(item.id)) {
|
||||
await labelRepository.deleteDocumentType(item);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +73,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeStoragePath(StoragePath item) async {
|
||||
assert(item.id != null);
|
||||
if (storagePaths.containsKey(item.id)) {
|
||||
if (labelRepository.state.storagePaths.containsKey(item.id)) {
|
||||
await labelRepository.deleteStoragePath(item);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +96,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeTag(Tag item) async {
|
||||
assert(item.id != null);
|
||||
if (tags.containsKey(item.id)) {
|
||||
if (labelRepository.state.tags.containsKey(item.id)) {
|
||||
await labelRepository.deleteTag(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
@@ -46,7 +46,12 @@ class _StoragePathAutofillFormBuilderFieldState
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _textEditingController,
|
||||
validator: FormBuilderValidators.required(), //TODO: INTL
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.storagePath),
|
||||
suffixIcon: _showClearIcon
|
||||
|
||||
264
lib/features/labels/view/widgets/fullscreen_label_form.dart
Normal file
264
lib/features/labels/view/widgets/fullscreen_label_form.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
final IdQueryParameter? initialValue;
|
||||
|
||||
final Map<int, T> options;
|
||||
final Future<T?> Function(String? initialName)? onCreateNewLabel;
|
||||
final bool showNotAssignedOption;
|
||||
final bool showAnyAssignedOption;
|
||||
final void Function({IdQueryParameter returnValue}) onSubmit;
|
||||
final Widget leadingIcon;
|
||||
final String? addNewLabelText;
|
||||
|
||||
FullscreenLabelForm({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
required this.options,
|
||||
required this.onCreateNewLabel,
|
||||
this.showNotAssignedOption = true,
|
||||
this.showAnyAssignedOption = true,
|
||||
required this.onSubmit,
|
||||
required this.leadingIcon,
|
||||
this.addNewLabelText,
|
||||
}) : assert(
|
||||
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
|
||||
),
|
||||
assert(
|
||||
!(initialValue?.onlyNotAssigned ?? false) || showNotAssignedOption,
|
||||
),
|
||||
assert((addNewLabelText != null) == (onCreateNewLabel != null));
|
||||
|
||||
@override
|
||||
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState();
|
||||
}
|
||||
|
||||
class _FullscreenLabelFormState<T extends Label>
|
||||
extends State<FullscreenLabelForm<T>> {
|
||||
late bool _showClearIcon = false;
|
||||
final _textEditingController = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController.addListener(() => setState(() {
|
||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||
}));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
//Delay keyboard popup to ensure open animation is finished before.
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
() => _focusNode.requestFocus(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final options = _filterOptionsByQuery(_textEditingController.text);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
toolbarHeight: 72,
|
||||
leading: BackButton(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
title: TextFormField(
|
||||
focusNode: _focusNode,
|
||||
controller: _textEditingController,
|
||||
onFieldSubmitted: (value) {
|
||||
FocusScope.of(context).unfocus();
|
||||
final index = AutocompleteHighlightedOption.of(context);
|
||||
final value = index.isNegative ? null : options.elementAt(index);
|
||||
widget.onSubmit(returnValue: IdQueryParameter.fromId(value?.id));
|
||||
},
|
||||
autofocus: true,
|
||||
style: theme.textTheme.bodyLarge?.apply(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
icon: widget.leadingIcon,
|
||||
hintText: _buildHintText(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
actions: [
|
||||
if (_showClearIcon)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Divider(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = options.elementAt(index);
|
||||
final highlight =
|
||||
AutocompleteHighlightedOption.of(context) == index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance
|
||||
.addPostFrameCallback((Duration timeStamp) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
return _buildOptionWidget(option, highlight);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCreateNewLabel() async {
|
||||
final label = await widget.onCreateNewLabel!(_textEditingController.text);
|
||||
if (label?.id != null) {
|
||||
widget.onSubmit(
|
||||
returnValue: IdQueryParameter.fromId(label!.id!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Filters the options passed to this widget by the current [query] and
|
||||
/// adds not-/any assigned options
|
||||
///
|
||||
Iterable<IdQueryParameter> _filterOptionsByQuery(String query) sync* {
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
if (normalizedQuery.isEmpty) {
|
||||
if (widget.initialValue == null) {
|
||||
// If nothing is selected yet, show all options first.
|
||||
for (final option in widget.options.values) {
|
||||
yield IdQueryParameter.fromId(option.id);
|
||||
}
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
} else {
|
||||
// If an initial value is given, show not assigned first, which will be selected by default when pressing "done" on keyboard.
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
for (final option in widget.options.values) {
|
||||
// Don't include the initial value in the selection
|
||||
if (option.id == widget.initialValue?.id) {
|
||||
continue;
|
||||
}
|
||||
yield IdQueryParameter.fromId(option.id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed.
|
||||
final matches = widget.options.values
|
||||
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||
if (matches.isNotEmpty) {
|
||||
for (final match in matches) {
|
||||
yield IdQueryParameter.fromId(match.id);
|
||||
}
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
} else {
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
if (!(widget.showAnyAssignedOption || widget.showNotAssignedOption)) {
|
||||
yield const IdQueryParameter.unset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _buildHintText() {
|
||||
if (widget.initialValue?.isSet ?? false) {
|
||||
return widget.options[widget.initialValue!.id]?.name ?? 'undefined';
|
||||
}
|
||||
if (widget.initialValue?.onlyNotAssigned ?? false) {
|
||||
return S.of(context)!.notAssigned;
|
||||
}
|
||||
if (widget.initialValue?.onlyAssigned ?? false) {
|
||||
return S.of(context)!.anyAssigned;
|
||||
}
|
||||
|
||||
return S.of(context)!.startTyping;
|
||||
}
|
||||
|
||||
Widget _buildOptionWidget(IdQueryParameter option, bool highlight) {
|
||||
void onTap() => widget.onSubmit(returnValue: option);
|
||||
late final String title;
|
||||
|
||||
if (option.isSet) {
|
||||
title = widget.options[option.id]!.name;
|
||||
}
|
||||
if (option.onlyNotAssigned) {
|
||||
title = S.of(context)!.notAssigned;
|
||||
}
|
||||
if (option.onlyAssigned) {
|
||||
title = S.of(context)!.anyAssigned;
|
||||
}
|
||||
if (option.isUnset) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(S.of(context)!.noItemsFound).padded(),
|
||||
if (widget.onCreateNewLabel != null)
|
||||
TextButton(
|
||||
child: Text(widget.addNewLabelText!),
|
||||
onPressed: _onCreateNewLabel,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
selected: highlight,
|
||||
selectedTileColor: Theme.of(context).focusColor,
|
||||
title: Text(title),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +1,164 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.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/form_builder_fields/form_builder_type_ahead.dart';
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/fullscreen_label_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
///
|
||||
/// Form field allowing to select labels (i.e. correspondent, documentType)
|
||||
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...).
|
||||
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...)
|
||||
///
|
||||
class LabelFormField<T extends Label> extends StatefulWidget {
|
||||
class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
final Widget prefixIcon;
|
||||
final Map<int, T> labelOptions;
|
||||
final FormBuilderState? formBuilderState;
|
||||
final Map<int, T> options;
|
||||
final IdQueryParameter? initialValue;
|
||||
final String name;
|
||||
final String textFieldLabel;
|
||||
final String labelText;
|
||||
final FormFieldValidator? validator;
|
||||
final Widget Function(String initialName)? labelCreationWidgetBuilder;
|
||||
final bool notAssignedSelectable;
|
||||
final Widget Function(String? initialName)? addLabelPageBuilder;
|
||||
final void Function(IdQueryParameter?)? onChanged;
|
||||
final bool showNotAssignedOption;
|
||||
final bool showAnyAssignedOption;
|
||||
final List<T> suggestions;
|
||||
final String? addLabelText;
|
||||
|
||||
const LabelFormField({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.labelOptions,
|
||||
this.validator,
|
||||
this.initialValue,
|
||||
required this.textFieldLabel,
|
||||
this.labelCreationWidgetBuilder,
|
||||
required this.formBuilderState,
|
||||
required this.options,
|
||||
required this.labelText,
|
||||
required this.prefixIcon,
|
||||
this.notAssignedSelectable = true,
|
||||
this.initialValue,
|
||||
this.validator,
|
||||
this.addLabelPageBuilder,
|
||||
this.onChanged,
|
||||
this.showNotAssignedOption = true,
|
||||
this.showAnyAssignedOption = true,
|
||||
this.suggestions = const [],
|
||||
this.addLabelText,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LabelFormField<T>> createState() => _LabelFormFieldState<T>();
|
||||
}
|
||||
|
||||
class _LabelFormFieldState<T extends Label> extends State<LabelFormField<T>> {
|
||||
bool _showCreationSuffixIcon = false;
|
||||
late bool _showClearSuffixIcon;
|
||||
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showClearSuffixIcon =
|
||||
widget.labelOptions.containsKey(widget.initialValue?.id);
|
||||
_textEditingController = TextEditingController(
|
||||
text: widget.labelOptions[widget.initialValue?.id]?.name ?? '',
|
||||
)..addListener(() {
|
||||
setState(() {
|
||||
_showCreationSuffixIcon = widget.labelOptions.values
|
||||
.where(
|
||||
(item) => item.name.toLowerCase().startsWith(
|
||||
_textEditingController.text.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.isEmpty;
|
||||
});
|
||||
setState(() =>
|
||||
_showClearSuffixIcon = _textEditingController.text.isNotEmpty);
|
||||
});
|
||||
String _buildText(BuildContext context, IdQueryParameter? value) {
|
||||
if (value?.isSet ?? false) {
|
||||
return options[value!.id]?.name ?? 'undefined';
|
||||
} else if (value?.onlyNotAssigned ?? false) {
|
||||
return S.of(context)!.notAssigned;
|
||||
} else if (value?.onlyAssigned ?? false) {
|
||||
return S.of(context)!.anyAssigned;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEnabled = widget.labelOptions.values.fold<bool>(
|
||||
false,
|
||||
(previousValue, element) =>
|
||||
previousValue || (element.documentCount ?? 0) > 0) ||
|
||||
widget.labelCreationWidgetBuilder != null;
|
||||
return FormBuilderTypeAhead<IdQueryParameter>(
|
||||
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
|
||||
addLabelPageBuilder != null;
|
||||
return FormBuilderField<IdQueryParameter>(
|
||||
name: name,
|
||||
initialValue: initialValue,
|
||||
onChanged: onChanged,
|
||||
enabled: isEnabled,
|
||||
noItemsFoundBuilder: (context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
S.of(context)!.noItemsFound,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0),
|
||||
),
|
||||
),
|
||||
loadingBuilder: (context) => Container(),
|
||||
initialValue: widget.initialValue ?? const IdQueryParameter.unset(),
|
||||
name: widget.name,
|
||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
elevation: 4.0,
|
||||
shadowColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, suggestion) => ListTile(
|
||||
title: Text(
|
||||
widget.labelOptions[suggestion.id]?.name ??
|
||||
S.of(context)!.notAssigned,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// tileColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
dense: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
suggestionsCallback: (pattern) {
|
||||
final List<IdQueryParameter> suggestions = widget.labelOptions.entries
|
||||
.where(
|
||||
(entry) =>
|
||||
widget.labelOptions[entry.key]!.name
|
||||
.toLowerCase()
|
||||
.contains(pattern.toLowerCase()) ||
|
||||
pattern.isEmpty,
|
||||
)
|
||||
.where(
|
||||
(entry) =>
|
||||
widget.labelCreationWidgetBuilder != null ||
|
||||
(entry.value.documentCount ?? 0) > 0,
|
||||
)
|
||||
.map((entry) => IdQueryParameter.fromId(entry.key))
|
||||
.toList();
|
||||
if (widget.notAssignedSelectable) {
|
||||
suggestions.insert(0, const IdQueryParameter.notAssigned());
|
||||
}
|
||||
return suggestions;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() => _showClearSuffixIcon = value?.isSet ?? false);
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
controller: _textEditingController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: widget.prefixIcon,
|
||||
label: Text(widget.textFieldLabel),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
suffixIcon: _buildSuffixIcon(context),
|
||||
),
|
||||
selectionToTextTransformer: (suggestion) {
|
||||
if (suggestion == const IdQueryParameter.notAssigned()) {
|
||||
return S.of(context)!.notAssigned;
|
||||
}
|
||||
return widget.labelOptions[suggestion.id]?.name ?? "";
|
||||
},
|
||||
direction: AxisDirection.up,
|
||||
onSuggestionSelected: (suggestion) =>
|
||||
widget.formBuilderState?.fields[widget.name]?.didChange(suggestion),
|
||||
);
|
||||
}
|
||||
builder: (field) {
|
||||
final controller = TextEditingController(
|
||||
text: _buildText(context, field.value),
|
||||
);
|
||||
final displayedSuggestions =
|
||||
suggestions.whereNot((e) => e.id == field.value?.id).toList();
|
||||
|
||||
Widget? _buildSuffixIcon(BuildContext context) {
|
||||
if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final createdLabel = await showDialog<T>(
|
||||
context: context,
|
||||
builder: (context) => widget.labelCreationWidgetBuilder!(
|
||||
_textEditingController.text,
|
||||
return Column(
|
||||
children: [
|
||||
OpenContainer<IdQueryParameter>(
|
||||
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: 4),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onTap: openForm,
|
||||
readOnly: true,
|
||||
enabled: isEnabled,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: prefixIcon,
|
||||
labelText: labelText,
|
||||
suffixIcon: controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () =>
|
||||
field.didChange(const IdQueryParameter.unset()),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
openBuilder: (context, closeForm) => FullscreenLabelForm<T>(
|
||||
addNewLabelText: addLabelText,
|
||||
leadingIcon: prefixIcon,
|
||||
onCreateNewLabel: addLabelPageBuilder != null
|
||||
? (initialName) {
|
||||
return Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
addLabelPageBuilder!(initialName),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
options: options,
|
||||
onSubmit: closeForm,
|
||||
initialValue: field.value,
|
||||
showAnyAssignedOption: showAnyAssignedOption,
|
||||
showNotAssignedOption: showNotAssignedOption,
|
||||
),
|
||||
onClosed: (data) {
|
||||
if (data != null) {
|
||||
field.didChange(data);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
if (createdLabel != null) {
|
||||
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done).
|
||||
widget.formBuilderState?.fields[widget.name]
|
||||
?.didChange(IdQueryParameter.fromId(createdLabel.id));
|
||||
_textEditingController.text = createdLabel.name;
|
||||
} else {
|
||||
_reset();
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.new_label,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_showClearSuffixIcon) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _reset,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
widget.formBuilderState?.fields[widget.name]?.didChange(
|
||||
const IdQueryParameter.unset(),
|
||||
if (displayedSuggestions.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context)!.suggestions,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: displayedSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion =
|
||||
displayedSuggestions.elementAt(index);
|
||||
return ColoredChipWrapper(
|
||||
child: ActionChip(
|
||||
label: Text(suggestion.name),
|
||||
onPressed: () => field.didChange(
|
||||
IdQueryParameter.fromId(suggestion.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(width: 4.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
_textEditingController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ class LinkedDocumentsState extends DocumentPagingState {
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
viewType,
|
||||
correspondents,
|
||||
documentTypes,
|
||||
tags,
|
||||
storagePaths,
|
||||
...super.props,
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class ServerAddressFormField extends StatefulWidget {
|
||||
@@ -42,15 +42,14 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||
controller: _textEditingController,
|
||||
name: ServerAddressFormField.fkServerAddress,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(
|
||||
errorText: S.of(context)!.serverAddressMustNotBeEmpty,
|
||||
),
|
||||
FormBuilderValidators.match(
|
||||
r"https?://.*",
|
||||
errorText: S.of(context)!.serverAddressMustIncludeAScheme,
|
||||
)
|
||||
]),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.serverAddressMustNotBeEmpty;
|
||||
}
|
||||
if (!RegExp(r"https?://.*").hasMatch(value!)) {
|
||||
return S.of(context)!.serverAddressMustIncludeAScheme;
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: "http://192.168.1.50:8000",
|
||||
labelText: S.of(context)!.serverAddress,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
||||
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
|
||||
@@ -36,9 +36,11 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||
field.value?.copyWith(username: username) ??
|
||||
UserCredentials(username: username),
|
||||
),
|
||||
validator: FormBuilderValidators.required(
|
||||
errorText: S.of(context)!.usernameMustNotBeEmpty,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.usernameMustNotBeEmpty;
|
||||
}
|
||||
},
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.username),
|
||||
@@ -51,9 +53,11 @@ class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {
|
||||
field.value?.copyWith(password: password) ??
|
||||
UserCredentials(password: password),
|
||||
),
|
||||
validator: FormBuilderValidators.required(
|
||||
errorText: S.of(context)!.passwordMustNotBeEmpty,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.passwordMustNotBeEmpty;
|
||||
}
|
||||
},
|
||||
),
|
||||
].map((child) => child.padded()).toList(),
|
||||
),
|
||||
|
||||
@@ -15,12 +15,14 @@ class SavedViewCubit extends Cubit<SavedViewState> {
|
||||
final LabelRepository _labelRepository;
|
||||
|
||||
SavedViewCubit(this._savedViewRepository, this._labelRepository)
|
||||
: super(SavedViewState.initial(
|
||||
correspondents: _labelRepository.state.correspondents,
|
||||
documentTypes: _labelRepository.state.documentTypes,
|
||||
storagePaths: _labelRepository.state.storagePaths,
|
||||
tags: _labelRepository.state.tags,
|
||||
)) {
|
||||
: super(
|
||||
SavedViewState.initial(
|
||||
correspondents: _labelRepository.state.correspondents,
|
||||
documentTypes: _labelRepository.state.documentTypes,
|
||||
storagePaths: _labelRepository.state.storagePaths,
|
||||
tags: _labelRepository.state.tags,
|
||||
),
|
||||
) {
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_form.dart';
|
||||
@@ -55,7 +55,12 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: _AddSavedViewPageState.fkName,
|
||||
validator: FormBuilderValidators.required(),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.name),
|
||||
),
|
||||
|
||||
@@ -23,8 +23,13 @@ class SavedViewList extends StatelessWidget {
|
||||
Center(
|
||||
child: Text("Saved views loading..."),
|
||||
),
|
||||
loaded: (savedViews, correspondents, documentTypes, tags,
|
||||
storagePaths) {
|
||||
loaded: (
|
||||
savedViews,
|
||||
correspondents,
|
||||
documentTypes,
|
||||
tags,
|
||||
storagePaths,
|
||||
) {
|
||||
if (savedViews.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: HintCard(
|
||||
@@ -47,11 +52,12 @@ class SavedViewList extends StatelessWidget {
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
builder: (ctxt) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SavedViewDetailsCubit(
|
||||
context.read(),
|
||||
ctxt.read(),
|
||||
ctxt.read(),
|
||||
context.read(),
|
||||
savedView: view,
|
||||
),
|
||||
@@ -71,7 +77,12 @@ class SavedViewList extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (correspondents, documentTypes, tags, storagePaths) =>
|
||||
error: (
|
||||
correspondents,
|
||||
documentTypes,
|
||||
tags,
|
||||
storagePaths,
|
||||
) =>
|
||||
Center(
|
||||
child: Text(
|
||||
"An error occurred while trying to load the saved views.",
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
@@ -14,20 +15,42 @@ class SavedViewDetailsCubit extends HydratedCubit<SavedViewDetailsState>
|
||||
@override
|
||||
final PaperlessDocumentsApi api;
|
||||
|
||||
final LabelRepository _labelRepository;
|
||||
|
||||
@override
|
||||
final DocumentChangedNotifier notifier;
|
||||
|
||||
final SavedView savedView;
|
||||
|
||||
SavedViewDetailsCubit(
|
||||
this.api,
|
||||
this.notifier, {
|
||||
this.notifier,
|
||||
this._labelRepository, {
|
||||
required this.savedView,
|
||||
}) : super(const SavedViewDetailsState()) {
|
||||
}) : super(
|
||||
SavedViewDetailsState(
|
||||
correspondents: _labelRepository.state.correspondents,
|
||||
documentTypes: _labelRepository.state.documentTypes,
|
||||
tags: _labelRepository.state.tags,
|
||||
storagePaths: _labelRepository.state.storagePaths,
|
||||
),
|
||||
) {
|
||||
notifier.addListener(
|
||||
this,
|
||||
onDeleted: remove,
|
||||
onUpdated: replace,
|
||||
);
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) {
|
||||
emit(state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
tags: labels.tags,
|
||||
storagePaths: labels.storagePaths,
|
||||
));
|
||||
},
|
||||
);
|
||||
updateFilter(filter: savedView.toDocumentFilter());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/search/m3_search_bar.dart'
|
||||
as s;
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/dialogs/account_settings_dialog.dart';
|
||||
|
||||
typedef OpenSearchCallback = void Function(BuildContext context);
|
||||
|
||||
class SearchAppBar extends StatefulWidget with PreferredSizeWidget {
|
||||
class SearchAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
final PreferredSizeWidget? bottom;
|
||||
final OpenSearchCallback onOpenSearch;
|
||||
final Color? backgroundColor;
|
||||
@@ -37,7 +38,7 @@ class _SearchAppBarState extends State<SearchAppBar> {
|
||||
snap: true,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
title: SearchBar(
|
||||
title: s.SearchBar(
|
||||
height: kToolbarHeight - 12,
|
||||
supportingText: widget.hintText,
|
||||
onTap: () => widget.onOpenSearch(context),
|
||||
|
||||
Reference in New Issue
Block a user