mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 12:07:54 -06:00
Initial commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class CorrespondentCubit extends LabelCubit<Correspondent> {
|
||||
CorrespondentCubit(super.metaDataService);
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
return labelRepository.getCorrespondents().then(loadFrom);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Correspondent> save(Correspondent item) => labelRepository.saveCorrespondent(item);
|
||||
|
||||
@override
|
||||
Future<Correspondent> update(Correspondent item) => labelRepository.updateCorrespondent(item);
|
||||
|
||||
@override
|
||||
Future<int> delete(Correspondent item) => labelRepository.deleteCorrespondent(item);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
|
||||
class Correspondent extends Label {
|
||||
static const lastCorrespondenceKey = 'last_correspondence';
|
||||
|
||||
late DateTime? lastCorrespondence;
|
||||
|
||||
Correspondent({
|
||||
required super.id,
|
||||
required super.name,
|
||||
super.slug,
|
||||
super.match,
|
||||
super.matchingAlgorithm,
|
||||
super.isInsensitive,
|
||||
super.documentCount,
|
||||
this.lastCorrespondence,
|
||||
});
|
||||
|
||||
Correspondent.fromJson(JSON json)
|
||||
: lastCorrespondence =
|
||||
DateTime.tryParse(json[lastCorrespondenceKey] ?? ''),
|
||||
super.fromJson(json);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
void addSpecificFieldsToJson(JSON json) {
|
||||
json.tryPutIfAbsent(
|
||||
lastCorrespondenceKey,
|
||||
() => lastCorrespondence?.toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Correspondent copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
String? slug,
|
||||
String? match,
|
||||
MatchingAlgorithm? matchingAlgorithm,
|
||||
bool? isInsensitive,
|
||||
int? documentCount,
|
||||
DateTime? lastCorrespondence,
|
||||
}) {
|
||||
return Correspondent(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
documentCount: documentCount ?? documentCount,
|
||||
isInsensitive: isInsensitive ?? isInsensitive,
|
||||
lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence,
|
||||
match: match ?? this.match,
|
||||
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
|
||||
slug: slug ?? this.slug,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get queryEndpoint => 'correspondents';
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/add_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class AddCorrespondentPage extends StatelessWidget {
|
||||
final String? initalValue;
|
||||
const AddCorrespondentPage({Key? key, this.initalValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AddLabelPage<Correspondent>(
|
||||
addLabelStr: S.of(context).addCorrespondentPageTitle,
|
||||
fromJson: Correspondent.fromJson,
|
||||
cubit: BlocProvider.of<CorrespondentCubit>(context),
|
||||
initialName: initalValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/edit_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
|
||||
class EditCorrespondentPage extends StatelessWidget {
|
||||
final Correspondent correspondent;
|
||||
const EditCorrespondentPage({super.key, required this.correspondent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditLabelPage<Correspondent>(
|
||||
label: correspondent,
|
||||
onSubmit: BlocProvider.of<CorrespondentCubit>(context).replace,
|
||||
onDelete: (correspondent) => _onDelete(correspondent, context),
|
||||
fromJson: Correspondent.fromJson,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDelete(Correspondent correspondent, BuildContext context) async {
|
||||
try {
|
||||
await BlocProvider.of<CorrespondentCubit>(context).remove(correspondent);
|
||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
if (cubit.state.filter.correspondent.id == correspondent.id) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(correspondent: const CorrespondentQuery.unset()),
|
||||
);
|
||||
}
|
||||
} on ErrorMessage catch (e) {
|
||||
showSnackBar(context, translateError(context, e.code));
|
||||
} finally {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/query_parameters/correspondent_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
|
||||
class CorrespondentWidget extends StatelessWidget {
|
||||
final int? correspondentId;
|
||||
final void Function()? afterSelected;
|
||||
final Color? textColor;
|
||||
final bool isClickable;
|
||||
|
||||
const CorrespondentWidget({
|
||||
Key? key,
|
||||
required this.correspondentId,
|
||||
this.afterSelected,
|
||||
this.textColor,
|
||||
this.isClickable = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !isClickable,
|
||||
child: BlocBuilder<CorrespondentCubit, Map<int, Correspondent>>(
|
||||
builder: (context, state) {
|
||||
return GestureDetector(
|
||||
onTap: () => _addCorrespondentToFilter(context),
|
||||
child: Text(
|
||||
(state[correspondentId]?.name) ?? "-",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyText2?.copyWith(
|
||||
color: textColor ?? Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addCorrespondentToFilter(BuildContext context) {
|
||||
final cubit = getIt<DocumentsCubit>();
|
||||
if (cubit.state.filter.correspondent.id == correspondentId) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(correspondent: const CorrespondentQuery.unset()));
|
||||
} else {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(
|
||||
correspondent: CorrespondentQuery.fromId(correspondentId),
|
||||
),
|
||||
);
|
||||
}
|
||||
afterSelected?.call();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class DocumentTypeCubit extends LabelCubit<DocumentType> {
|
||||
DocumentTypeCubit(super.metaDataService);
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
return labelRepository.getDocumentTypes().then(loadFrom);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DocumentType> save(DocumentType item) => labelRepository.saveDocumentType(item);
|
||||
|
||||
@override
|
||||
Future<DocumentType> update(DocumentType item) => labelRepository.updateDocumentType(item);
|
||||
|
||||
@override
|
||||
Future<int> delete(DocumentType item) => labelRepository.deleteDocumentType(item);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
|
||||
class DocumentType extends Label {
|
||||
DocumentType({
|
||||
required super.id,
|
||||
required super.name,
|
||||
super.slug,
|
||||
super.match,
|
||||
super.matchingAlgorithm,
|
||||
super.isInsensitive,
|
||||
super.documentCount,
|
||||
});
|
||||
|
||||
DocumentType.fromJson(JSON json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
void addSpecificFieldsToJson(JSON json) {}
|
||||
|
||||
@override
|
||||
String get queryEndpoint => 'document_types';
|
||||
|
||||
@override
|
||||
DocumentType copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
String? match,
|
||||
MatchingAlgorithm? matchingAlgorithm,
|
||||
bool? isInsensitive,
|
||||
int? documentCount,
|
||||
String? slug,
|
||||
}) {
|
||||
return DocumentType(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
match: match ?? this.match,
|
||||
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
|
||||
isInsensitive: isInsensitive ?? this.isInsensitive,
|
||||
documentCount: documentCount ?? this.documentCount,
|
||||
slug: slug ?? this.slug,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
enum MatchingAlgorithm {
|
||||
anyWord(1, "Any: Match one of the following words"),
|
||||
allWords(2, "All: Match all of the following words"),
|
||||
exactMatch(3, "Exact: Match the following string"),
|
||||
regex(4, "Regex: Match the regular expression"),
|
||||
similarWord(5, "Similar: Match a similar word"),
|
||||
auto(6, "Auto: Learn automatic assignment");
|
||||
|
||||
final int value;
|
||||
final String name;
|
||||
|
||||
const MatchingAlgorithm(this.value, this.name);
|
||||
|
||||
static MatchingAlgorithm fromInt(int? value) {
|
||||
return MatchingAlgorithm.values
|
||||
.where((element) => element.value == value)
|
||||
.firstWhere(
|
||||
(element) => true,
|
||||
orElse: () => MatchingAlgorithm.anyWord,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/add_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class AddDocumentTypePage extends StatelessWidget {
|
||||
final String? initialName;
|
||||
const AddDocumentTypePage({Key? key, this.initialName}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AddLabelPage<DocumentType>(
|
||||
addLabelStr: S.of(context).addDocumentTypePageTitle,
|
||||
fromJson: DocumentType.fromJson,
|
||||
cubit: BlocProvider.of<DocumentTypeCubit>(context),
|
||||
initialName: initialName,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/edit_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
|
||||
class EditDocumentTypePage extends StatelessWidget {
|
||||
final DocumentType documentType;
|
||||
const EditDocumentTypePage({super.key, required this.documentType});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditLabelPage<DocumentType>(
|
||||
label: documentType,
|
||||
onSubmit: BlocProvider.of<DocumentTypeCubit>(context).replace,
|
||||
onDelete: (docType) => _onDelete(docType, context),
|
||||
fromJson: DocumentType.fromJson,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDelete(DocumentType docType, BuildContext context) async {
|
||||
try {
|
||||
await BlocProvider.of<DocumentTypeCubit>(context).remove(docType);
|
||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
if (cubit.state.filter.documentType.id == docType.id) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(documentType: const DocumentTypeQuery.unset()),
|
||||
);
|
||||
}
|
||||
} on ErrorMessage catch (e) {
|
||||
showSnackBar(context, translateError(context, e.code));
|
||||
} finally {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
|
||||
class DocumentTypeWidget extends StatelessWidget {
|
||||
final int? documentTypeId;
|
||||
final void Function()? afterSelected;
|
||||
const DocumentTypeWidget({
|
||||
Key? key,
|
||||
required this.documentTypeId,
|
||||
this.afterSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _addDocumentTypeToFilter,
|
||||
child: BlocBuilder<DocumentTypeCubit, Map<int, DocumentType>>(
|
||||
builder: (context, state) {
|
||||
return Text(
|
||||
state[documentTypeId]?.toString() ?? "-",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyText2!
|
||||
.copyWith(color: Theme.of(context).colorScheme.primary),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addDocumentTypeToFilter() {
|
||||
final cubit = getIt<DocumentsCubit>();
|
||||
if (cubit.state.filter.documentType.id == documentTypeId) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(documentType: const DocumentTypeQuery.unset()));
|
||||
} else {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(
|
||||
documentType: DocumentTypeQuery.fromId(documentTypeId),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (afterSelected != null) {
|
||||
afterSelected?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
82
lib/features/labels/model/label.model.dart
Normal file
82
lib/features/labels/model/label.model.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
|
||||
abstract class Label with EquatableMixin implements Comparable {
|
||||
static const idKey = "id";
|
||||
static const nameKey = "name";
|
||||
static const slugKey = "slug";
|
||||
static const matchKey = "match";
|
||||
static const matchingAlgorithmKey = "matching_algorithm";
|
||||
static const isInsensitiveKey = "is_insensitive";
|
||||
static const documentCountKey = "document_count";
|
||||
|
||||
String get queryEndpoint;
|
||||
|
||||
final int? id;
|
||||
final String name;
|
||||
final String? slug;
|
||||
final String? match;
|
||||
final MatchingAlgorithm? matchingAlgorithm;
|
||||
final bool? isInsensitive;
|
||||
final int? documentCount;
|
||||
|
||||
const Label({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.match,
|
||||
this.matchingAlgorithm,
|
||||
this.isInsensitive,
|
||||
this.documentCount,
|
||||
this.slug,
|
||||
});
|
||||
|
||||
Label.fromJson(JSON json)
|
||||
: id = json[idKey],
|
||||
name = json[nameKey],
|
||||
slug = json[slugKey],
|
||||
match = json[matchKey],
|
||||
matchingAlgorithm =
|
||||
MatchingAlgorithm.fromInt(json[matchingAlgorithmKey]),
|
||||
isInsensitive = json[isInsensitiveKey],
|
||||
documentCount = json[documentCountKey];
|
||||
|
||||
JSON toJson() {
|
||||
JSON json = {};
|
||||
json.tryPutIfAbsent(idKey, () => id);
|
||||
json.tryPutIfAbsent(nameKey, () => name);
|
||||
json.tryPutIfAbsent(slugKey, () => slug);
|
||||
json.tryPutIfAbsent(matchKey, () => match);
|
||||
json.tryPutIfAbsent(matchingAlgorithmKey, () => matchingAlgorithm?.value);
|
||||
json.tryPutIfAbsent(isInsensitiveKey, () => isInsensitive);
|
||||
json.tryPutIfAbsent(documentCountKey, () => documentCount);
|
||||
addSpecificFieldsToJson(json);
|
||||
return json;
|
||||
}
|
||||
|
||||
void addSpecificFieldsToJson(JSON json);
|
||||
|
||||
Label copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
String? match,
|
||||
MatchingAlgorithm? matchingAlgorithm,
|
||||
bool? isInsensitive,
|
||||
int? documentCount,
|
||||
String? slug,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(dynamic other) {
|
||||
return toString().toLowerCase().compareTo(other.toString().toLowerCase());
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
246
lib/features/labels/repository/label.repository.dart
Normal file
246
lib/features/labels/repository/label.repository.dart
Normal file
@@ -0,0 +1,246 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/core/util.dart';
|
||||
import 'package:flutter_paperless_mobile/di_initializer.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/repository/label_repository.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@Singleton(as: LabelRepository)
|
||||
class LabelRepositoryImpl implements LabelRepository {
|
||||
final BaseClient httpClient;
|
||||
|
||||
LabelRepositoryImpl(@Named("timeoutClient") this.httpClient);
|
||||
|
||||
@override
|
||||
Future<Correspondent?> getCorrespondent(int id) async {
|
||||
return getSingleResult(
|
||||
"/api/correspondents/$id/",
|
||||
Correspondent.fromJson,
|
||||
ErrorCode.correspondentLoadFailed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Tag?> getTag(int id) async {
|
||||
return getSingleResult("/api/tags/$id/", Tag.fromJson, ErrorCode.tagLoadFailed);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Tag>> getTags({List<int>? ids}) async {
|
||||
final results = await getCollection(
|
||||
"/api/tags/?page=1&page_size=100000",
|
||||
Tag.fromJson,
|
||||
ErrorCode.tagLoadFailed,
|
||||
minRequiredApiVersion: 2,
|
||||
);
|
||||
return results.where((element) => ids?.contains(element.id) ?? true).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DocumentType?> getDocumentType(int id) async {
|
||||
return getSingleResult(
|
||||
"/api/document_types/$id/",
|
||||
DocumentType.fromJson,
|
||||
ErrorCode.documentTypeLoadFailed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Correspondent>> getCorrespondents() {
|
||||
return getCollection(
|
||||
"/api/correspondents/?page=1&page_size=100000",
|
||||
Correspondent.fromJson,
|
||||
ErrorCode.correspondentLoadFailed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DocumentType>> getDocumentTypes() {
|
||||
return getCollection(
|
||||
"/api/document_types/?page=1&page_size=100000",
|
||||
DocumentType.fromJson,
|
||||
ErrorCode.documentTypeLoadFailed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Correspondent> saveCorrespondent(Correspondent correspondent) async {
|
||||
final response = await httpClient.post(
|
||||
Uri.parse('/api/correspondents/'),
|
||||
body: json.encode(correspondent.toJson()),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
);
|
||||
if (response.statusCode == 201) {
|
||||
return Correspondent.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw ErrorMessage(ErrorCode.correspondentCreateFailed, httpStatusCode: response.statusCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DocumentType> saveDocumentType(DocumentType type) async {
|
||||
final response = await httpClient.post(
|
||||
Uri.parse('/api/document_types/'),
|
||||
body: json.encode(type.toJson()),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
);
|
||||
if (response.statusCode == 201) {
|
||||
return DocumentType.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.documentTypeCreateFailed);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Tag> saveTag(Tag tag) async {
|
||||
final body = json.encode(tag.toJson());
|
||||
final response = await httpClient.post(Uri.parse('/api/tags/'), body: body, headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json; version=2",
|
||||
});
|
||||
if (response.statusCode == 201) {
|
||||
return Tag.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.tagCreateFailed);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> getStatistics() async {
|
||||
final response = await httpClient.get(Uri.parse('/api/statistics/'));
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body)['documents_total'];
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> deleteCorrespondent(Correspondent correspondent) async {
|
||||
assert(correspondent.id != null);
|
||||
final response = await httpClient.delete(Uri.parse('/api/correspondents/${correspondent.id}/'));
|
||||
if (response.statusCode == 204) {
|
||||
return correspondent.id!;
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> deleteDocumentType(DocumentType documentType) async {
|
||||
assert(documentType.id != null);
|
||||
final response = await httpClient.delete(Uri.parse('/api/document_types/${documentType.id}/'));
|
||||
if (response.statusCode == 204) {
|
||||
return documentType.id!;
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> deleteTag(Tag tag) async {
|
||||
assert(tag.id != null);
|
||||
final response = await httpClient.delete(Uri.parse('/api/tags/${tag.id}/'));
|
||||
if (response.statusCode == 204) {
|
||||
return tag.id!;
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
|
||||
assert(correspondent.id != null);
|
||||
final response = await httpClient.put(
|
||||
Uri.parse('/api/correspondents/${correspondent.id}/'),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(correspondent.toJson()),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return Correspondent.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
|
||||
assert(documentType.id != null);
|
||||
final response = await httpClient.put(
|
||||
Uri.parse('/api/document_types/${documentType.id}/'),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(documentType.toJson()),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return DocumentType.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Tag> updateTag(Tag tag) async {
|
||||
assert(tag.id != null);
|
||||
final response = await httpClient.put(
|
||||
Uri.parse('/api/tags/${tag.id}/'),
|
||||
headers: {
|
||||
"Accept": "application/json; version=2",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: json.encode(tag.toJson()),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return Tag.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> deleteStoragePath(StoragePath path) async {
|
||||
assert(path.id != null);
|
||||
final response = await httpClient.delete(Uri.parse('/api/storage_paths/${path.id}/'));
|
||||
if (response.statusCode == 204) {
|
||||
return path.id!;
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StoragePath?> getStoragePath(int id) {
|
||||
return getSingleResult("/api/storage_paths/?page=1&page_size=100000", StoragePath.fromJson,
|
||||
ErrorCode.storagePathLoadFailed);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StoragePath>> getStoragePaths() {
|
||||
return getCollection(
|
||||
"/api/storage_paths/?page=1&page_size=100000",
|
||||
StoragePath.fromJson,
|
||||
ErrorCode.storagePathLoadFailed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StoragePath> saveStoragePath(StoragePath path) async {
|
||||
final response = await httpClient.post(
|
||||
Uri.parse('/api/storage_paths/'),
|
||||
body: json.encode(path.toJson()),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
);
|
||||
if (response.statusCode == 201) {
|
||||
return StoragePath.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw ErrorMessage(ErrorCode.storagePathCreateFailed, httpStatusCode: response.statusCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StoragePath> updateStoragePath(StoragePath path) async {
|
||||
assert(path.id != null);
|
||||
final response = await httpClient.put(
|
||||
Uri.parse('/api/storage_paths/${path.id}/'),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: json.encode(path.toJson()),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
return StoragePath.fromJson(json.decode(response.body));
|
||||
}
|
||||
throw const ErrorMessage(ErrorCode.unknown);
|
||||
}
|
||||
}
|
||||
32
lib/features/labels/repository/label_repository.dart
Normal file
32
lib/features/labels/repository/label_repository.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
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/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
|
||||
abstract class LabelRepository {
|
||||
Future<Correspondent?> getCorrespondent(int id);
|
||||
Future<List<Correspondent>> getCorrespondents();
|
||||
Future<Correspondent> saveCorrespondent(Correspondent correspondent);
|
||||
Future<Correspondent> updateCorrespondent(Correspondent correspondent);
|
||||
Future<int> deleteCorrespondent(Correspondent correspondent);
|
||||
|
||||
Future<Tag?> getTag(int id);
|
||||
Future<List<Tag>> getTags({List<int>? ids});
|
||||
Future<Tag> saveTag(Tag tag);
|
||||
Future<Tag> updateTag(Tag tag);
|
||||
Future<int> deleteTag(Tag tag);
|
||||
|
||||
Future<DocumentType?> getDocumentType(int id);
|
||||
Future<List<DocumentType>> getDocumentTypes();
|
||||
Future<DocumentType> saveDocumentType(DocumentType type);
|
||||
Future<DocumentType> updateDocumentType(DocumentType documentType);
|
||||
Future<int> deleteDocumentType(DocumentType documentType);
|
||||
|
||||
Future<StoragePath?> getStoragePath(int id);
|
||||
Future<List<StoragePath>> getStoragePaths();
|
||||
Future<StoragePath> saveStoragePath(StoragePath path);
|
||||
Future<StoragePath> updateStoragePath(StoragePath path);
|
||||
Future<int> deleteStoragePath(StoragePath path);
|
||||
|
||||
Future<int> getStatistics();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class StoragePathCubit extends LabelCubit<StoragePath> {
|
||||
StoragePathCubit(super.metaDataService);
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
return labelRepository.getStoragePaths().then(loadFrom);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StoragePath> save(StoragePath item) => labelRepository.saveStoragePath(item);
|
||||
|
||||
@override
|
||||
Future<StoragePath> update(StoragePath item) => labelRepository.updateStoragePath(item);
|
||||
|
||||
@override
|
||||
Future<int> delete(StoragePath item) => labelRepository.deleteStoragePath(item);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
|
||||
class StoragePath extends Label {
|
||||
static const pathKey = 'path';
|
||||
|
||||
late String? path;
|
||||
|
||||
StoragePath({
|
||||
required super.id,
|
||||
required super.name,
|
||||
super.slug,
|
||||
super.match,
|
||||
super.matchingAlgorithm,
|
||||
super.isInsensitive,
|
||||
super.documentCount,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
StoragePath.fromJson(JSON json)
|
||||
: path = json[pathKey],
|
||||
super.fromJson(json);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
void addSpecificFieldsToJson(JSON json) {
|
||||
json.tryPutIfAbsent(
|
||||
pathKey,
|
||||
() => path,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
StoragePath copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
String? slug,
|
||||
String? match,
|
||||
MatchingAlgorithm? matchingAlgorithm,
|
||||
bool? isInsensitive,
|
||||
int? documentCount,
|
||||
String? path,
|
||||
}) {
|
||||
return StoragePath(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
documentCount: documentCount ?? documentCount,
|
||||
isInsensitive: isInsensitive ?? isInsensitive,
|
||||
path: path ?? this.path,
|
||||
match: match ?? this.match,
|
||||
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
|
||||
slug: slug ?? this.slug,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get queryEndpoint => 'storage_paths';
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/add_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class AddStoragePathPage extends StatelessWidget {
|
||||
final String? initalValue;
|
||||
const AddStoragePathPage({Key? key, this.initalValue}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AddLabelPage<StoragePath>(
|
||||
addLabelStr: S.of(context).addStoragePathPageTitle,
|
||||
fromJson: StoragePath.fromJson,
|
||||
cubit: BlocProvider.of<StoragePathCubit>(context),
|
||||
initialName: initalValue,
|
||||
additionalFields: const [
|
||||
StoragePathAutofillFormBuilderField(name: StoragePath.pathKey),
|
||||
SizedBox(height: 120.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/bloc/documents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/edit_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
|
||||
class EditStoragePathPage extends StatelessWidget {
|
||||
final StoragePath storagePath;
|
||||
const EditStoragePathPage({super.key, required this.storagePath});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditLabelPage<StoragePath>(
|
||||
label: storagePath,
|
||||
onSubmit: BlocProvider.of<StoragePathCubit>(context).replace,
|
||||
onDelete: (correspondent) => _onDelete(correspondent, context),
|
||||
fromJson: StoragePath.fromJson,
|
||||
additionalFields: [
|
||||
StoragePathAutofillFormBuilderField(
|
||||
name: StoragePath.pathKey,
|
||||
initialValue: storagePath.path,
|
||||
),
|
||||
const SizedBox(height: 120.0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDelete(StoragePath path, BuildContext context) async {
|
||||
try {
|
||||
await BlocProvider.of<StoragePathCubit>(context).remove(path);
|
||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
if (cubit.state.filter.storagePath.id == path.id) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(storagePath: const StoragePathQuery.unset()));
|
||||
}
|
||||
} on ErrorMessage catch (e) {
|
||||
showSnackBar(context, translateError(context, e.code));
|
||||
} finally {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/src/widgets/container.dart';
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
class StoragePathAutofillFormBuilderField extends StatefulWidget {
|
||||
final String name;
|
||||
final String? initialValue;
|
||||
const StoragePathAutofillFormBuilderField({
|
||||
super.key,
|
||||
required this.name,
|
||||
this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StoragePathAutofillFormBuilderField> createState() =>
|
||||
_StoragePathAutofillFormBuilderFieldState();
|
||||
}
|
||||
|
||||
class _StoragePathAutofillFormBuilderFieldState extends State<StoragePathAutofillFormBuilderField> {
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
late String _exampleOutput;
|
||||
late bool _showClearIcon;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController = TextEditingController.fromValue(
|
||||
TextEditingValue(text: widget.initialValue ?? ''),
|
||||
)..addListener(() {
|
||||
setState(() {
|
||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||
});
|
||||
});
|
||||
_exampleOutput = _buildExampleOutput(widget.initialValue ?? '');
|
||||
_showClearIcon = widget.initialValue?.isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FormBuilderField<String>(
|
||||
name: widget.name,
|
||||
initialValue: widget.initialValue ?? '',
|
||||
builder: (field) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _textEditingController,
|
||||
validator: FormBuilderValidators.required(), //TODO: INTL
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context).documentStoragePathPropertyLabel),
|
||||
suffixIcon: _showClearIcon
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () => _resetfield(field),
|
||||
)
|
||||
: null),
|
||||
onChanged: field.didChange,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
"Select to autofill path variable",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentArchiveSerialNumberPropertyLongLabel),
|
||||
onPressed: () => _addParameterToInput("{asn}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCorrespondentPropertyLabel),
|
||||
onPressed: () => _addParameterToInput("{correspondent}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentDocumentTypePropertyLabel),
|
||||
onPressed: () => _addParameterToInput("{document_type}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentTagsPropertyLabel),
|
||||
onPressed: () => _addParameterToInput("{tag_list}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentTitlePropertyLabel),
|
||||
onPressed: () => _addParameterToInput("{title}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||
onPressed: () => _addParameterToInput("{created}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel +
|
||||
" (${S.of(context).storagePathParameterYearLabel})"),
|
||||
onPressed: () => _addParameterToInput("{created_year}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel +
|
||||
" (${S.of(context).storagePathParameterMonthLabel})"),
|
||||
onPressed: () => _addParameterToInput("{created_month}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel +
|
||||
" (${S.of(context).storagePathParameterDayLabel})"),
|
||||
onPressed: () => _addParameterToInput("{created_day}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel),
|
||||
onPressed: () => _addParameterToInput("{added}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel +
|
||||
" (${S.of(context).storagePathParameterYearLabel})"),
|
||||
onPressed: () => _addParameterToInput("{added_year}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel +
|
||||
" (${S.of(context).storagePathParameterMonthLabel})"),
|
||||
onPressed: () => _addParameterToInput("{added_month}", field),
|
||||
),
|
||||
InputChip(
|
||||
label: Text(S.of(context).documentCreatedPropertyLabel +
|
||||
" (${S.of(context).storagePathParameterDayLabel})"),
|
||||
onPressed: () => _addParameterToInput("{added_day}", field),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addParameterToInput(String param, FormFieldState<String> field) {
|
||||
final text = (field.value ?? "") + param;
|
||||
field.didChange(text);
|
||||
_textEditingController.text = text;
|
||||
}
|
||||
|
||||
String _buildExampleOutput(String input) {
|
||||
return input
|
||||
.replaceAll("{asn}", "1234")
|
||||
.replaceAll("{correspondent}", "My Bank")
|
||||
.replaceAll("{document_type}", "Invoice")
|
||||
.replaceAll("{tag_list}", "TODO,University,Work")
|
||||
.replaceAll("{created}", "2020-02-10")
|
||||
.replaceAll("{created_year}", "2020")
|
||||
.replaceAll("{created_month}", "02")
|
||||
.replaceAll("{created_day}", "10")
|
||||
.replaceAll("{added}", "2029-12-24")
|
||||
.replaceAll("{added_year}", "2029")
|
||||
.replaceAll("{added_month}", "12")
|
||||
.replaceAll("{added_day}", "24");
|
||||
}
|
||||
|
||||
void _resetfield(FormFieldState<String> field) {
|
||||
field.didChange("");
|
||||
_textEditingController.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/query_parameters/storage_path_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
|
||||
class StoragePathWidget extends StatelessWidget {
|
||||
final int? pathId;
|
||||
final void Function()? afterSelected;
|
||||
final Color? textColor;
|
||||
final bool isClickable;
|
||||
|
||||
const StoragePathWidget({
|
||||
Key? key,
|
||||
required this.pathId,
|
||||
this.afterSelected,
|
||||
this.textColor,
|
||||
this.isClickable = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !isClickable,
|
||||
child: BlocBuilder<StoragePathCubit, Map<int, StoragePath>>(
|
||||
builder: (context, state) {
|
||||
return GestureDetector(
|
||||
onTap: () => _addStoragePathToFilter(context),
|
||||
child: Text(
|
||||
(state[pathId]?.name) ?? "-",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyText2?.copyWith(
|
||||
color: textColor ?? Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addStoragePathToFilter(BuildContext context) {
|
||||
final cubit = getIt<DocumentsCubit>();
|
||||
if (cubit.state.filter.correspondent.id == pathId) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(storagePath: const StoragePathQuery.unset()));
|
||||
} else {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(
|
||||
storagePath: StoragePathQuery.fromId(pathId),
|
||||
),
|
||||
);
|
||||
}
|
||||
afterSelected?.call();
|
||||
}
|
||||
}
|
||||
22
lib/features/labels/tags/bloc/tags_cubit.dart
Normal file
22
lib/features/labels/tags/bloc/tags_cubit.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
class TagCubit extends LabelCubit<Tag> {
|
||||
TagCubit(super.metaDataService);
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
return labelRepository.getTags().then(loadFrom);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Tag> save(Tag item) => labelRepository.saveTag(item);
|
||||
|
||||
@override
|
||||
Future<Tag> update(Tag item) => labelRepository.updateTag(item);
|
||||
|
||||
@override
|
||||
Future<int> delete(Tag item) => labelRepository.deleteTag(item);
|
||||
}
|
||||
96
lib/features/labels/tags/model/tag.model.dart
Normal file
96
lib/features/labels/tags/model/tag.model.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
|
||||
class Tag extends Label {
|
||||
static const colorKey = 'color';
|
||||
static const isInboxTagKey = 'is_inbox_tag';
|
||||
static const textColorKey = 'text_color';
|
||||
|
||||
final Color? color;
|
||||
final Color? textColor;
|
||||
final bool? isInboxTag;
|
||||
|
||||
Tag({
|
||||
required super.id,
|
||||
required super.name,
|
||||
super.documentCount,
|
||||
super.isInsensitive,
|
||||
super.match,
|
||||
super.matchingAlgorithm,
|
||||
super.slug,
|
||||
this.color,
|
||||
this.textColor,
|
||||
this.isInboxTag,
|
||||
});
|
||||
|
||||
Tag.fromJson(JSON json)
|
||||
: isInboxTag = json[isInboxTagKey],
|
||||
textColor = Color(_colorStringToInt(json[textColorKey]) ?? 0),
|
||||
color = (json[colorKey] is Color)
|
||||
? json[colorKey]
|
||||
: Color(_colorStringToInt(json[colorKey]) ?? 0),
|
||||
super.fromJson(json);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@override
|
||||
void addSpecificFieldsToJson(JSON json) {
|
||||
json.tryPutIfAbsent(colorKey, () => _toHex(color));
|
||||
json.tryPutIfAbsent(isInboxTagKey, () => isInboxTag);
|
||||
}
|
||||
|
||||
@override
|
||||
Tag copyWith({
|
||||
int? id,
|
||||
String? name,
|
||||
String? match,
|
||||
MatchingAlgorithm? matchingAlgorithm,
|
||||
bool? isInsensitive,
|
||||
int? documentCount,
|
||||
String? slug,
|
||||
Color? color,
|
||||
Color? textColor,
|
||||
bool? isInboxTag,
|
||||
}) {
|
||||
return Tag(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
match: match ?? this.match,
|
||||
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
|
||||
isInsensitive: isInsensitive ?? this.isInsensitive,
|
||||
documentCount: documentCount ?? this.documentCount,
|
||||
slug: slug ?? this.slug,
|
||||
color: color ?? this.color,
|
||||
textColor: textColor ?? this.textColor,
|
||||
isInboxTag: isInboxTag ?? this.isInboxTag,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get queryEndpoint => 'tags';
|
||||
}
|
||||
|
||||
///
|
||||
/// Taken from [FormBuilderColorPicker].
|
||||
///
|
||||
String? _toHex(Color? color) {
|
||||
if (color == null) {
|
||||
return null;
|
||||
}
|
||||
String val = '#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
|
||||
log("Color in Tag#_toHex is $val");
|
||||
return val;
|
||||
}
|
||||
|
||||
int? _colorStringToInt(String? color) {
|
||||
if (color == null) return null;
|
||||
return int.tryParse(color.replaceAll("#", "ff"), radix: 16);
|
||||
}
|
||||
35
lib/features/labels/tags/view/pages/add_tag_page.dart
Normal file
35
lib/features/labels/tags/view/pages/add_tag_page.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/add_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
|
||||
|
||||
class AddTagPage extends StatelessWidget {
|
||||
const AddTagPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AddLabelPage<Tag>(
|
||||
addLabelStr: S.of(context).addTagPageTitle,
|
||||
fromJson: Tag.fromJson,
|
||||
cubit: BlocProvider.of<TagCubit>(context),
|
||||
additionalFields: [
|
||||
FormBuilderColorPickerField(
|
||||
name: Tag.colorKey,
|
||||
valueTransformer: (color) => "#${color?.value.toRadixString(16)}",
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context).tagColorPropertyLabel),
|
||||
),
|
||||
colorPickerType: ColorPickerType.materialPicker,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: Tag.isInboxTagKey,
|
||||
title: Text(S.of(context).tagInboxTagPropertyLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/features/labels/tags/view/pages/edit_tag_page.dart
Normal file
61
lib/features/labels/tags/view/pages/edit_tag_page.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.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/model/query_parameters/tags_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/pages/edit_label_page.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
import 'package:form_builder_extra_fields/form_builder_extra_fields.dart';
|
||||
|
||||
class EditTagPage extends StatelessWidget {
|
||||
final Tag tag;
|
||||
|
||||
const EditTagPage({super.key, required this.tag});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditLabelPage<Tag>(
|
||||
label: tag,
|
||||
onSubmit: BlocProvider.of<TagCubit>(context).replace,
|
||||
onDelete: (tag) => _onDelete(tag, context),
|
||||
fromJson: Tag.fromJson,
|
||||
additionalFields: [
|
||||
FormBuilderColorPickerField(
|
||||
initialValue: tag.color,
|
||||
name: Tag.colorKey,
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context).tagColorPropertyLabel),
|
||||
),
|
||||
colorPickerType: ColorPickerType.blockPicker,
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
initialValue: tag.isInboxTag,
|
||||
name: Tag.isInboxTagKey,
|
||||
title: Text(S.of(context).tagInboxTagPropertyLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onDelete(Tag tag, BuildContext context) async {
|
||||
try {
|
||||
await BlocProvider.of<TagCubit>(context).remove(tag);
|
||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
final currentFilter = cubit.state.filter;
|
||||
late DocumentFilter updatedFilter = currentFilter;
|
||||
if (currentFilter.tags.ids.contains(tag.id)) {
|
||||
updatedFilter = currentFilter.copyWith(
|
||||
tags: TagsQuery.fromIds(
|
||||
currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList()));
|
||||
}
|
||||
cubit.updateFilter(filter: updatedFilter);
|
||||
} on ErrorMessage catch (error) {
|
||||
showError(context, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
55
lib/features/labels/tags/view/widgets/tag_widget.dart
Normal file
55
lib/features/labels/tags/view/widgets/tag_widget.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/query_parameters/tags_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
|
||||
class TagWidget extends StatelessWidget {
|
||||
final Tag tag;
|
||||
final void Function()? afterTagTapped;
|
||||
const TagWidget({super.key, required this.tag, required this.afterTagTapped});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
return FilterChip(
|
||||
selected: state.filter.tags.ids.contains(tag.id),
|
||||
selectedColor: tag.color,
|
||||
onSelected: (_) => _addTagToFilter(context),
|
||||
visualDensity: const VisualDensity(vertical: -2),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
label: Text(
|
||||
tag.name,
|
||||
style: TextStyle(color: tag.textColor),
|
||||
),
|
||||
backgroundColor: tag.color,
|
||||
side: BorderSide.none,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addTagToFilter(BuildContext context) {
|
||||
final cubit = BlocProvider.of<DocumentsCubit>(context);
|
||||
if (cubit.state.filter.tags.ids.contains(tag.id)) {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter.copyWith(
|
||||
tags: TagsQuery.fromIds(cubit.state.filter.tags.ids.where((id) => id != tag.id).toList()),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
cubit.updateFilter(
|
||||
filter: cubit.state.filter
|
||||
.copyWith(tags: TagsQuery.fromIds([...cubit.state.filter.tags.ids, tag.id!])),
|
||||
);
|
||||
}
|
||||
if (afterTagTapped != null) {
|
||||
afterTagTapped!();
|
||||
}
|
||||
}
|
||||
}
|
||||
99
lib/features/labels/tags/view/widgets/tags_form_field.dart
Normal file
99
lib/features/labels/tags/view/widgets/tags_form_field.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class TagFormField extends StatefulWidget {
|
||||
final TagsQuery? initialValue;
|
||||
final String name;
|
||||
|
||||
const TagFormField({
|
||||
super.key,
|
||||
required this.name,
|
||||
this.initialValue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TagFormField> createState() => _TagFormFieldState();
|
||||
}
|
||||
|
||||
class _TagFormFieldState extends State<TagFormField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TagCubit, Map<int, Tag>>(
|
||||
builder: (context, tagState) {
|
||||
return FormBuilderField<TagsQuery>(
|
||||
builder: (field) {
|
||||
final sortedTags = tagState.values.toList()
|
||||
..sort(
|
||||
(a, b) => a.name.compareTo(b.name),
|
||||
);
|
||||
//TODO: this is either not correctly resetting on filter reset or (when adding UniqueKey to FormField or ChipsInput) unmounts widget.
|
||||
// return ChipsInput<int>(
|
||||
// chipBuilder: (context, state, data) => Chip(
|
||||
// onDeleted: () => state.deleteChip(data),
|
||||
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
// backgroundColor: Color(tagState[data]!.color ?? Colors.white.value),
|
||||
// label: Text(
|
||||
// tagState[data]!.name,
|
||||
// style: TextStyle(color: Color(tagState[data]!.textColor ?? Colors.black.value)),
|
||||
// ),
|
||||
// ),
|
||||
// suggestionBuilder: (context, state, data) => ListTile(
|
||||
// title: Text(tagState[data]!.name),
|
||||
// textColor: Color(tagState[data]!.textColor!),
|
||||
// tileColor: Color(tagState[data]!.color!),
|
||||
// onTap: () => state.selectSuggestion(data),
|
||||
// ),
|
||||
// findSuggestions: (query) => tagState.values
|
||||
// .where((element) => element.name.toLowerCase().startsWith(query.toLowerCase()))
|
||||
// .map((e) => e.id!)
|
||||
// .toList(),
|
||||
// onChanged: (tags) => field.didChange(tags),
|
||||
// initialValue: field.value!,
|
||||
// );
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).documentTagsPropertyLabel,
|
||||
),
|
||||
Wrap(
|
||||
children: sortedTags
|
||||
.map((tag) => FilterChip(
|
||||
label: Text(
|
||||
tag.name,
|
||||
style: TextStyle(
|
||||
color: tag.textColor,
|
||||
),
|
||||
),
|
||||
selectedColor: tag.color,
|
||||
selected: field.value?.ids.contains(tag.id) ?? false,
|
||||
onSelected: (isSelected) {
|
||||
List<int> ids = [...field.value?.ids ?? []];
|
||||
if (isSelected) {
|
||||
ids.add(tag.id!);
|
||||
} else {
|
||||
ids.remove(tag.id);
|
||||
}
|
||||
field.didChange(TagsQuery.fromIds(ids));
|
||||
},
|
||||
backgroundColor: tag.color,
|
||||
))
|
||||
.toList()
|
||||
.padded(const EdgeInsets.only(right: 4.0)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
initialValue: widget.initialValue ?? const TagsQuery.unset(),
|
||||
name: widget.name,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/features/labels/tags/view/widgets/tags_widget.dart
Normal file
55
lib/features/labels/tags/view/widgets/tags_widget.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
|
||||
|
||||
class TagsWidget extends StatefulWidget {
|
||||
final List<int> tagIds;
|
||||
final bool isMultiLine;
|
||||
final void Function()? afterTagTapped;
|
||||
|
||||
const TagsWidget({
|
||||
Key? key,
|
||||
required this.tagIds,
|
||||
this.afterTagTapped,
|
||||
this.isMultiLine = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TagsWidget> createState() => _TagsWidgetState();
|
||||
}
|
||||
|
||||
class _TagsWidgetState extends State<TagsWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TagCubit, Map<int, Tag>>(
|
||||
builder: (context, state) {
|
||||
final children = widget.tagIds
|
||||
.where((id) => state.containsKey(id))
|
||||
.map(
|
||||
(id) => TagWidget(
|
||||
tag: state[id]!,
|
||||
afterTagTapped: widget.afterTagTapped,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
if (widget.isMultiLine) {
|
||||
return Wrap(
|
||||
runAlignment: WrapAlignment.start,
|
||||
children: children,
|
||||
runSpacing: 8,
|
||||
spacing: 4,
|
||||
);
|
||||
} else {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
114
lib/features/labels/view/pages/add_label_page.dart
Normal file
114
lib/features/labels/view/pages/add_label_page.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/core/bloc/label_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
class AddLabelPage<T extends Label> extends StatefulWidget {
|
||||
final String? initialName;
|
||||
final String addLabelStr;
|
||||
final T Function(Map<String, dynamic> json) fromJson;
|
||||
final LabelCubit<T> cubit;
|
||||
final List<Widget> additionalFields;
|
||||
|
||||
const AddLabelPage({
|
||||
Key? key,
|
||||
this.initialName,
|
||||
required this.addLabelStr,
|
||||
required this.fromJson,
|
||||
required this.cubit,
|
||||
this.additionalFields = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AddLabelPage> createState() => _AddLabelPageState<T>();
|
||||
}
|
||||
|
||||
class _AddLabelPageState<T extends Label> extends State<AddLabelPage<T>> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
Map<String, String> _errors = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: Text(widget.addLabelStr),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(S.of(context).genericActionCreateLabel),
|
||||
onPressed: _onSubmit,
|
||||
),
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
name: Label.nameKey,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).labelNamePropertyLabel,
|
||||
errorText: _errors[Label.nameKey],
|
||||
),
|
||||
initialValue: widget.initialName,
|
||||
validator: FormBuilderValidators.required(),
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
name: Label.matchKey,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).labelMatchPropertyLabel,
|
||||
),
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
),
|
||||
FormBuilderDropdown<int?>(
|
||||
name: Label.matchingAlgorithmKey,
|
||||
initialValue: MatchingAlgorithm.anyWord.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
|
||||
errorText: _errors[Label.matchingAlgorithmKey],
|
||||
),
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
items: MatchingAlgorithm.values
|
||||
.map((algo) => DropdownMenuItem<int?>(
|
||||
child: Text(algo.name), //TODO: INTL
|
||||
value: algo.value))
|
||||
.toList(),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: Label.isInsensitiveKey,
|
||||
initialValue: true,
|
||||
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
|
||||
),
|
||||
...widget.additionalFields,
|
||||
].padded(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubmit() async {
|
||||
log("IsValid? ${_formKey.currentState?.isValid}");
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
try {
|
||||
final label = await widget.cubit.add(widget.fromJson(_formKey.currentState!.value));
|
||||
Navigator.pop(context, label);
|
||||
} on ErrorMessage catch (e) {
|
||||
showSnackBar(context, translateError(context, e.code));
|
||||
} on Map<String, String> catch (json) {
|
||||
setState(() => _errors = json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
lib/features/labels/view/pages/edit_label_page.dart
Normal file
124
lib/features/labels/view/pages/edit_label_page.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_paperless_mobile/core/logic/error_code_localization_mapper.dart';
|
||||
import 'package:flutter_paperless_mobile/core/model/error_message.dart';
|
||||
import 'package:flutter_paperless_mobile/core/type/json.dart';
|
||||
import 'package:flutter_paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/matching_algorithm.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
import 'package:flutter_paperless_mobile/util.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
class EditLabelPage<T extends Label> extends StatefulWidget {
|
||||
final T label;
|
||||
final Future<void> Function(T) onSubmit;
|
||||
final Future<void> Function(T) onDelete;
|
||||
final T Function(JSON) fromJson;
|
||||
final List<Widget> additionalFields;
|
||||
|
||||
const EditLabelPage({
|
||||
Key? key,
|
||||
required this.label,
|
||||
required this.fromJson,
|
||||
required this.onSubmit,
|
||||
required this.onDelete,
|
||||
this.additionalFields = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<EditLabelPage> createState() => _EditLabelPageState<T>();
|
||||
}
|
||||
|
||||
class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
Map<String, String> _errors = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context).genericActionEditLabel),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => widget.onDelete(widget.label),
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(S.of(context).genericActionUpdateLabel),
|
||||
onPressed: _onSubmit,
|
||||
),
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: Label.nameKey,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).labelNamePropertyLabel,
|
||||
errorText: _errors[Label.nameKey],
|
||||
),
|
||||
validator: FormBuilderValidators.required(),
|
||||
initialValue: widget.label.name,
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: Label.matchKey,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).labelMatchPropertyLabel,
|
||||
errorText: _errors[Label.matchKey],
|
||||
),
|
||||
initialValue: widget.label.match,
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
),
|
||||
FormBuilderDropdown<int?>(
|
||||
name: Label.matchingAlgorithmKey,
|
||||
initialValue:
|
||||
widget.label.matchingAlgorithm?.value ?? MatchingAlgorithm.allWords.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: S.of(context).labelMatchingAlgorithmPropertyLabel,
|
||||
errorText: _errors[Label.matchingAlgorithmKey],
|
||||
),
|
||||
onChanged: (val) => setState(() => _errors = {}),
|
||||
items: MatchingAlgorithm.values
|
||||
.map(
|
||||
(algo) => DropdownMenuItem<int?>(
|
||||
child: Text(algo.name), //TODO: INTL
|
||||
value: algo.value,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
FormBuilderCheckbox(
|
||||
name: Label.isInsensitiveKey,
|
||||
initialValue: widget.label.isInsensitive,
|
||||
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
|
||||
),
|
||||
...widget.additionalFields,
|
||||
].padded(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubmit() async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
try {
|
||||
final mergedJson = {...widget.label.toJson(), ..._formKey.currentState!.value};
|
||||
await widget.onSubmit(widget.fromJson(mergedJson));
|
||||
Navigator.pop(context);
|
||||
} on ErrorMessage catch (e) {
|
||||
showSnackBar(context, translateError(context, e.code));
|
||||
} on Map<String, String> catch (errorMessages) {
|
||||
setState(() => _errors = errorMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
lib/features/labels/view/pages/labels_page.dart
Normal file
237
lib/features/labels/view/pages/labels_page.dart
Normal file
@@ -0,0 +1,237 @@
|
||||
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/query_parameters/correspondent_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/view/pages/edit_correspondent_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/documents/model/document_filter.dart';
|
||||
import 'package:flutter_paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/model/document_type.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/document_type/view/pages/edit_document_type_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/model/label.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/storage_path/view/pages/edit_storage_path_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/model/tag.model.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/view/pages/edit_tag_page.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/label_item.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
|
||||
import 'package:flutter_paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
|
||||
import 'package:flutter_paperless_mobile/generated/l10n.dart';
|
||||
|
||||
class LabelsPage extends StatefulWidget {
|
||||
const LabelsPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LabelsPage> createState() => _LabelsPageState();
|
||||
}
|
||||
|
||||
class _LabelsPageState extends State<LabelsPage> with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
BlocProvider.of<CorrespondentCubit>(context).initialize();
|
||||
BlocProvider.of<DocumentTypeCubit>(context).initialize();
|
||||
BlocProvider.of<TagCubit>(context).initialize();
|
||||
|
||||
_tabController = TabController(length: 4, vsync: this)
|
||||
..addListener(() => setState(() => _currentIndex = _tabController.index));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
drawer: const InfoDrawer(),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
[
|
||||
S.of(context).labelsPageCorrespondentsTitleText,
|
||||
S.of(context).labelsPageDocumentTypesTitleText,
|
||||
S.of(context).labelsPageTagsTitleText,
|
||||
S.of(context).labelsPageStoragePathTitleText
|
||||
][_currentIndex],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _onAddPressed,
|
||||
icon: const Icon(Icons.add),
|
||||
)
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).bottomAppBarColor,
|
||||
child: TabBar(
|
||||
indicatorColor: Theme.of(context).colorScheme.primary,
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.person_outline,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.description_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.label_outline,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(
|
||||
Icons.folder_open,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
LabelTabView<Correspondent>(
|
||||
cubit: BlocProvider.of<CorrespondentCubit>(context),
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
correspondent: CorrespondentQuery.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onOpenEditPage: _openEditCorrespondentPage,
|
||||
),
|
||||
LabelTabView<DocumentType>(
|
||||
cubit: BlocProvider.of<DocumentTypeCubit>(context),
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
documentType: DocumentTypeQuery.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onOpenEditPage: _openEditDocumentTypePage,
|
||||
),
|
||||
LabelTabView<Tag>(
|
||||
cubit: BlocProvider.of<TagCubit>(context),
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
tags: TagsQuery.fromIds([label.id!]),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
onOpenEditPage: _openEditTagPage,
|
||||
leadingBuilder: (t) => CircleAvatar(backgroundColor: t.color),
|
||||
),
|
||||
LabelTabView<StoragePath>(
|
||||
cubit: BlocProvider.of<StoragePathCubit>(context),
|
||||
onOpenEditPage: _openEditStoragePathPage,
|
||||
filterBuilder: (label) => DocumentFilter(
|
||||
storagePath: StoragePathQuery.fromId(label.id),
|
||||
pageSize: label.documentCount ?? 0,
|
||||
),
|
||||
contentBuilder: (path) => Text(path.path ?? ""),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openEditCorrespondentPage(Correspondent correspondent) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||
BlocProvider.value(value: BlocProvider.of<CorrespondentCubit>(context)),
|
||||
],
|
||||
child: EditCorrespondentPage(correspondent: correspondent),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openEditDocumentTypePage(DocumentType docType) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||
BlocProvider.value(value: BlocProvider.of<DocumentTypeCubit>(context)),
|
||||
],
|
||||
child: EditDocumentTypePage(documentType: docType),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openEditTagPage(Tag tag) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||
BlocProvider.value(value: BlocProvider.of<TagCubit>(context)),
|
||||
],
|
||||
child: EditTagPage(tag: tag),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openEditStoragePathPage(StoragePath path) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<DocumentsCubit>()),
|
||||
BlocProvider.value(value: BlocProvider.of<StoragePathCubit>(context)),
|
||||
],
|
||||
child: EditStoragePathPage(storagePath: path),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAddPressed() {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
late final Widget page;
|
||||
switch (_currentIndex) {
|
||||
case 0:
|
||||
page = const AddCorrespondentPage();
|
||||
break;
|
||||
case 1:
|
||||
page = const AddDocumentTypePage();
|
||||
break;
|
||||
case 2:
|
||||
page = const AddTagPage();
|
||||
break;
|
||||
case 3:
|
||||
page = const AddStoragePathPage();
|
||||
}
|
||||
return LabelBlocProvider(child: page);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
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