mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 16:07:58 -06:00
Initial commit
This commit is contained in:
161
lib/features/labels/view/widgets/label_form_field.dart
Normal file
161
lib/features/labels/view/widgets/label_form_field.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/id_query_parameter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
|
||||
|
||||
///
|
||||
/// Form field allowing to select labels (i.e. correspondent, documentType)
|
||||
/// [T] is the label (model) type, [R] is the return type.
|
||||
///
|
||||
class LabelFormField<T extends Label, R extends IdQueryParameter> extends StatefulWidget {
|
||||
final Widget prefixIcon;
|
||||
final Map<int, T> state;
|
||||
final FormBuilderState? formBuilderState;
|
||||
final IdQueryParameter? initialValue;
|
||||
final String name;
|
||||
final String label;
|
||||
final FormFieldValidator? validator;
|
||||
final Widget Function(String)? labelCreationWidgetBuilder;
|
||||
final R Function() queryParameterNotAssignedBuilder;
|
||||
final R Function(int? id) queryParameterIdBuilder;
|
||||
final bool notAssignedSelectable;
|
||||
|
||||
const LabelFormField({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.state,
|
||||
this.validator,
|
||||
this.initialValue,
|
||||
required this.label,
|
||||
this.labelCreationWidgetBuilder,
|
||||
required this.queryParameterNotAssignedBuilder,
|
||||
required this.queryParameterIdBuilder,
|
||||
required this.formBuilderState,
|
||||
required this.prefixIcon,
|
||||
this.notAssignedSelectable = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LabelFormField<T, R>> createState() => _LabelFormFieldState<T, R>();
|
||||
}
|
||||
|
||||
class _LabelFormFieldState<T extends Label, R extends IdQueryParameter>
|
||||
extends State<LabelFormField<T, R>> {
|
||||
bool _showCreationSuffixIcon = false;
|
||||
late bool _showClearSuffixIcon;
|
||||
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showClearSuffixIcon = widget.state.containsKey(widget.initialValue?.id);
|
||||
_textEditingController =
|
||||
TextEditingController(text: widget.state[widget.initialValue?.id]?.name ?? '')
|
||||
..addListener(() {
|
||||
setState(() {
|
||||
_showCreationSuffixIcon = widget.state.values
|
||||
.where((item) => item.name.toLowerCase().startsWith(
|
||||
_textEditingController.text.toLowerCase(),
|
||||
))
|
||||
.isEmpty;
|
||||
});
|
||||
setState(() => _showClearSuffixIcon = _textEditingController.text.isNotEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderTypeAhead<IdQueryParameter>(
|
||||
initialValue: widget.initialValue ?? widget.queryParameterIdBuilder(null),
|
||||
name: widget.name,
|
||||
itemBuilder: (context, suggestion) => ListTile(
|
||||
title: Text(widget.state[suggestion.id]?.name ?? S.of(context).labelNotAssignedText),
|
||||
),
|
||||
suggestionsCallback: (pattern) {
|
||||
final List<IdQueryParameter> suggestions = widget.state.keys
|
||||
.where((item) =>
|
||||
widget.state[item]!.name.toLowerCase().startsWith(pattern.toLowerCase()) ||
|
||||
pattern.isEmpty)
|
||||
.map((id) => widget.queryParameterIdBuilder(id))
|
||||
.toList();
|
||||
if (widget.notAssignedSelectable) {
|
||||
suggestions.insert(0, widget.queryParameterNotAssignedBuilder());
|
||||
}
|
||||
return suggestions;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() => _showClearSuffixIcon = value?.isSet ?? false);
|
||||
},
|
||||
controller: _textEditingController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: widget.prefixIcon,
|
||||
label: Text(widget.label),
|
||||
hintText: _getLocalizedHint(context),
|
||||
suffixIcon: _buildSuffixIcon(context),
|
||||
),
|
||||
selectionToTextTransformer: (suggestion) {
|
||||
if (suggestion == widget.queryParameterNotAssignedBuilder()) {
|
||||
return S.of(context).labelNotAssignedText;
|
||||
}
|
||||
return widget.state[suggestion.id]?.name ?? "";
|
||||
},
|
||||
direction: AxisDirection.up,
|
||||
onSuggestionSelected: (suggestion) =>
|
||||
widget.formBuilderState?.fields[widget.name]?.didChange(suggestion as R),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildSuffixIcon(BuildContext context) {
|
||||
if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) {
|
||||
return IconButton(
|
||||
onPressed: () => Navigator.of(context)
|
||||
.push<T>(MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
widget.labelCreationWidgetBuilder!(_textEditingController.text)))
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done).
|
||||
widget.formBuilderState?.fields[widget.name]
|
||||
?.didChange(widget.queryParameterIdBuilder(value.id));
|
||||
_textEditingController.text = value.name;
|
||||
FocusScope.of(context).unfocus();
|
||||
} 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(widget.queryParameterIdBuilder(null));
|
||||
_textEditingController.clear();
|
||||
}
|
||||
|
||||
String _getLocalizedHint(BuildContext context) {
|
||||
if (T == Correspondent) {
|
||||
return S.of(context).correspondentFormFieldSearchHintText;
|
||||
} else if (T == DocumentType) {
|
||||
return S.of(context).documentTypeFormFieldSearchHintText;
|
||||
} else {
|
||||
return S
|
||||
.of(context)
|
||||
.tagFormFieldSearchHintText; //TODO: Update tag form field once there is multi selection support.
|
||||
}
|
||||
}
|
||||
}
|
||||
70
lib/features/labels/view/widgets/label_item.dart
Normal file
70
lib/features/labels/view/widgets/label_item.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_bloc_provider.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/repository/document_repository.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/linked_documents_preview.dart';
|
||||
|
||||
class LabelItem<T extends Label> extends StatelessWidget {
|
||||
final T label;
|
||||
final String name;
|
||||
final Widget content;
|
||||
final void Function(T) onOpenEditPage;
|
||||
final DocumentFilter Function(T) filterBuilder;
|
||||
final Widget? leading;
|
||||
|
||||
const LabelItem({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.content,
|
||||
required this.onOpenEditPage,
|
||||
required this.filterBuilder,
|
||||
this.leading,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(name),
|
||||
subtitle: content,
|
||||
leading: leading,
|
||||
onTap: () => onOpenEditPage(label),
|
||||
trailing: _buildDocumentCountWidget(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentCountWidget(BuildContext context) {
|
||||
return TextButton.icon(
|
||||
label: const Icon(Icons.link),
|
||||
icon: Text(_formatDocumentCount(label.documentCount)),
|
||||
onPressed: (label.documentCount ?? 0) == 0
|
||||
? null
|
||||
: () {
|
||||
final filter = filterBuilder(label);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LabelBlocProvider(
|
||||
child: BlocProvider(
|
||||
create: (context) =>
|
||||
DocumentsCubit(getIt<DocumentRepository>())..updateFilter(filter: filter),
|
||||
child: LinkedDocumentsPreview(filter: filter),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDocumentCount(int? count) {
|
||||
if ((count ?? 0) > 99) {
|
||||
return "99+";
|
||||
}
|
||||
return (count ?? 0).toString().padLeft(3);
|
||||
}
|
||||
}
|
||||
73
lib/features/labels/view/widgets/label_list_tile.dart
Normal file
73
lib/features/labels/view/widgets/label_list_tile.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_bloc_provider.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/repository/document_repository.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/linked_documents_preview.dart';
|
||||
|
||||
class LabelListTile<T extends Label> extends StatelessWidget {
|
||||
final T label;
|
||||
final DocumentFilter Function(Label) filterBuilder;
|
||||
final void Function() onOpenEditPage;
|
||||
|
||||
const LabelListTile(
|
||||
this.label, {
|
||||
super.key,
|
||||
required this.filterBuilder,
|
||||
required this.onOpenEditPage,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: (label is Tag)
|
||||
? CircleAvatar(
|
||||
backgroundColor: (label as Tag).color,
|
||||
)
|
||||
: null,
|
||||
title: Text(label.name),
|
||||
onTap: onOpenEditPage,
|
||||
trailing: _buildDocumentCountWidget(context),
|
||||
subtitle: Text(
|
||||
(label.match?.isEmpty ?? true) ? "-" : label.match!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentCountWidget(BuildContext context) {
|
||||
return TextButton.icon(
|
||||
label: const Icon(Icons.link),
|
||||
icon: Text(_formatDocumentCount(label.documentCount)),
|
||||
onPressed: (label.documentCount ?? 0) == 0
|
||||
? null
|
||||
: () {
|
||||
final filter = filterBuilder(label);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LabelBlocProvider(
|
||||
child: BlocProvider(
|
||||
create: (context) =>
|
||||
DocumentsCubit(getIt<DocumentRepository>())..updateFilter(filter: filter),
|
||||
child: LinkedDocumentsPreview(filter: filter),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDocumentCount(int? count) {
|
||||
if ((count ?? 0) > 99) {
|
||||
return "99+";
|
||||
}
|
||||
return (count ?? 0).toString().padLeft(3);
|
||||
}
|
||||
}
|
||||
52
lib/features/labels/view/widgets/label_tab_view.dart
Normal file
52
lib/features/labels/view/widgets/label_tab_view.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/label_item.dart';
|
||||
|
||||
class LabelTabView<T extends Label> extends StatelessWidget {
|
||||
final LabelCubit<T> cubit;
|
||||
final DocumentFilter Function(Label) filterBuilder;
|
||||
final void Function(T) onOpenEditPage;
|
||||
|
||||
/// Displayed as the subtitle of the [ListTile]
|
||||
final Widget Function(T)? contentBuilder;
|
||||
|
||||
/// Displayed as the leading widget of the [ListTile]
|
||||
final Widget Function(T)? leadingBuilder;
|
||||
|
||||
const LabelTabView({
|
||||
super.key,
|
||||
required this.cubit,
|
||||
required this.filterBuilder,
|
||||
this.contentBuilder,
|
||||
this.leadingBuilder,
|
||||
required this.onOpenEditPage,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<Cubit<Map<int, T>>, Map<int, T>>(
|
||||
bloc: cubit,
|
||||
builder: (context, state) {
|
||||
final labels = state.values.toList()..sort();
|
||||
return RefreshIndicator(
|
||||
onRefresh: cubit.initialize,
|
||||
child: ListView(
|
||||
children: labels
|
||||
.map((l) => LabelItem<T>(
|
||||
name: l.name,
|
||||
content: contentBuilder?.call(l) ?? Text(l.match ?? '-'),
|
||||
onOpenEditPage: onOpenEditPage,
|
||||
filterBuilder: filterBuilder,
|
||||
leading: leadingBuilder?.call(l),
|
||||
label: l,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_bloc_provider.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_state.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/pages/document_details_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/view/widgets/list/document_list.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
class LinkedDocumentsPreview extends StatefulWidget {
|
||||
final DocumentFilter filter;
|
||||
|
||||
const LinkedDocumentsPreview({super.key, required this.filter});
|
||||
|
||||
@override
|
||||
State<LinkedDocumentsPreview> createState() => _LinkedDocumentsPreviewState();
|
||||
}
|
||||
|
||||
class _LinkedDocumentsPreviewState extends State<LinkedDocumentsPreview> {
|
||||
final PagingController<int, DocumentModel> _pagingController = PagingController(firstPageKey: 1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.nextPageKey = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).linkedDocumentsPageTitle),
|
||||
),
|
||||
body: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
_pagingController.itemList = state.documents;
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
DocumentListView(
|
||||
onTap: (doc) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (ctxt) => LabelBlocProvider(
|
||||
child: BlocProvider.value(
|
||||
value: BlocProvider.of<DocumentsCubit>(context),
|
||||
child: DocumentDetailsPage(documentId: doc.id)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
pagingController: _pagingController,
|
||||
state: state,
|
||||
onSelected: BlocProvider.of<DocumentsCubit>(context).toggleDocumentSelection,
|
||||
hasInternetConnection: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user