Merge branch 'development'

This commit is contained in:
Anton Stubenbord
2022-11-18 00:59:29 +01:00
34 changed files with 814 additions and 518 deletions

View File

@@ -19,7 +19,7 @@ class DocumentProcessingStatus {
final String taskId; final String taskId;
final bool isApproximated; final bool isApproximated;
static const String UNKNOWN_TASK_ID = "NO_TASK_ID"; static const String unknownTaskId = "NO_TASK_ID";
DocumentProcessingStatus({ DocumentProcessingStatus({
required this.currentProgress, required this.currentProgress,

View File

@@ -79,7 +79,7 @@ class LongPollingStatusService implements StatusService {
maxProgress: 100, maxProgress: 100,
message: ProcessingMessage.new_file, message: ProcessingMessage.new_file,
status: ProcessingStatus.working, status: ProcessingStatus.working,
taskId: DocumentProcessingStatus.UNKNOWN_TASK_ID, taskId: DocumentProcessingStatus.unknownTaskId,
documentId: null, documentId: null,
isApproximated: true, isApproximated: true,
), ),
@@ -105,7 +105,7 @@ class LongPollingStatusService implements StatusService {
maxProgress: 100, maxProgress: 100,
message: ProcessingMessage.finished, message: ProcessingMessage.finished,
status: ProcessingStatus.success, status: ProcessingStatus.success,
taskId: DocumentProcessingStatus.UNKNOWN_TASK_ID, taskId: DocumentProcessingStatus.unknownTaskId,
documentId: docId, documentId: docId,
isApproximated: true, isApproximated: true,
), ),

View File

@@ -7,3 +7,18 @@ extension NullableMapKey<K, V> on Map<K, V> {
return putIfAbsent(key, () => value); return putIfAbsent(key, () => value);
} }
} }
extension Unique<E, Id> on List<E> {
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
final ids = <Id>{};
var list = inplace ? this : List<E>.from(this);
list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
return list;
}
}
extension DuplicateCheckable<T> on Iterable<T> {
bool hasDuplicates() {
return toSet().length != length;
}
}

View File

@@ -22,7 +22,7 @@ class DocumentsCubit extends Cubit<DocumentsState> {
required void Function(DocumentModel document) onConsumptionFinished, required void Function(DocumentModel document) onConsumptionFinished,
int? documentType, int? documentType,
int? correspondent, int? correspondent,
List<int>? tags, Iterable<int> tags = const [],
DateTime? createdAt, DateTime? createdAt,
}) async { }) async {
await documentRepository.create( await documentRepository.create(

View File

@@ -2,18 +2,23 @@ import 'package:paperless_mobile/core/type/types.dart';
class BulkEditAction { class BulkEditAction {
final List<int> documents; final List<int> documents;
final String _method; final BulkEditActionMethod _method;
final Map<String, dynamic> parameters; final Map<String, dynamic> parameters;
BulkEditAction.delete(this.documents) BulkEditAction.delete(this.documents)
: _method = 'delete', : _method = BulkEditActionMethod.delete,
parameters = {}; parameters = {};
JSON toJson() { JSON toJson() {
return { return {
'documents': documents, 'documents': documents,
'method': _method, 'method': _method.name,
'parameters': parameters, 'parameters': parameters,
}; };
} }
} }
enum BulkEditActionMethod {
delete,
edit;
}

View File

@@ -23,7 +23,7 @@ class DocumentModel extends Equatable {
final int id; final int id;
final String title; final String title;
final String? content; final String? content;
final List<int> tags; final Iterable<int> tags;
final int? documentType; final int? documentType;
final int? correspondent; final int? correspondent;
final int? storagePath; final int? storagePath;
@@ -78,7 +78,7 @@ class DocumentModel extends Equatable {
modifiedKey: modified.toUtc().toIso8601String(), modifiedKey: modified.toUtc().toIso8601String(),
addedKey: added.toUtc().toIso8601String(), addedKey: added.toUtc().toIso8601String(),
originalFileNameKey: originalFileName, originalFileNameKey: originalFileName,
tagsKey: tags, tagsKey: tags.toList(),
storagePathKey: storagePath, storagePathKey: storagePath,
}; };
} }
@@ -86,10 +86,14 @@ class DocumentModel extends Equatable {
DocumentModel copyWith({ DocumentModel copyWith({
String? title, String? title,
String? content, String? content,
TagsQuery? tags, bool overwriteTags = false,
IdQueryParameter? documentType, Iterable<int>? tags,
IdQueryParameter? correspondent, bool overwriteDocumentType = false,
IdQueryParameter? storagePath, int? documentType,
bool overwriteCorrespondent = false,
int? correspondent,
bool overwriteStoragePath = false,
int? storagePath,
DateTime? created, DateTime? created,
DateTime? modified, DateTime? modified,
DateTime? added, DateTime? added,
@@ -101,10 +105,11 @@ class DocumentModel extends Equatable {
id: id, id: id,
title: title ?? this.title, title: title ?? this.title,
content: content ?? this.content, content: content ?? this.content,
documentType: fromQuery(documentType, this.documentType), documentType: overwriteDocumentType ? documentType : this.documentType,
correspondent: fromQuery(correspondent, this.correspondent), correspondent:
storagePath: fromQuery(storagePath, this.storagePath), overwriteCorrespondent ? correspondent : this.correspondent,
tags: fromListQuery(tags, this.tags), storagePath: overwriteDocumentType ? storagePath : this.storagePath,
tags: overwriteTags ? tags ?? [] : this.tags,
created: created ?? this.created, created: created ?? this.created,
modified: modified ?? this.modified, modified: modified ?? this.modified,
added: added ?? this.added, added: added ?? this.added,
@@ -114,20 +119,6 @@ class DocumentModel extends Equatable {
); );
} }
int? fromQuery(IdQueryParameter? query, int? previous) {
if (query == null) {
return previous;
}
return query.id;
}
List<int> fromListQuery(TagsQuery? query, List<int> previous) {
if (query == null) {
return previous;
}
return query.ids;
}
@override @override
List<Object?> get props => [ List<Object?> get props => [
id, id,

View File

@@ -43,7 +43,7 @@ class DocumentFilter with EquatableMixin {
this.correspondent = const CorrespondentQuery.unset(), this.correspondent = const CorrespondentQuery.unset(),
this.storagePath = const StoragePathQuery.unset(), this.storagePath = const StoragePathQuery.unset(),
this.asn = const AsnQuery.unset(), this.asn = const AsnQuery.unset(),
this.tags = const TagsQuery.unset(), this.tags = const IdsTagsQuery.unset(),
this.sortField = SortField.created, this.sortField = SortField.created,
this.sortOrder = SortOrder.descending, this.sortOrder = SortOrder.descending,
this.page = 1, this.page = 1,

View File

@@ -13,24 +13,24 @@ class FilterRule with EquatableMixin {
static const int asnRule = 2; static const int asnRule = 2;
static const int correspondentRule = 3; static const int correspondentRule = 3;
static const int documentTypeRule = 4; static const int documentTypeRule = 4;
static const int tagRule = 6; static const int includeTagsRule = 6;
static const int hasAnyTag = 7; // Corresponds to Not assigned
static const int createdBeforeRule = 8; static const int createdBeforeRule = 8;
static const int createdAfterRule = 9; static const int createdAfterRule = 9;
static const int addedBeforeRule = 13; static const int addedBeforeRule = 13;
static const int addedAfterRule = 14; static const int addedAfterRule = 14;
static const int excludeTagsRule = 17;
static const int titleAndContentRule = 19; static const int titleAndContentRule = 19;
static const int extendedRule = 20; static const int extendedRule = 20;
static const int storagePathRule = 25; static const int storagePathRule = 25;
// Currently unsupported view optiosn: // Currently unsupported view options:
static const int _content = 1; static const int _content = 1;
static const int _isInInbox = 5; static const int _isInInbox = 5;
static const int _hasAnyTag = 7;
static const int _createdYearIs = 10; static const int _createdYearIs = 10;
static const int _createdMonthIs = 11; static const int _createdMonthIs = 11;
static const int _createdDayIs = 12; static const int _createdDayIs = 12;
static const int _modifiedBefore = 15; static const int _modifiedBefore = 15;
static const int _modifiedAfter = 16; static const int _modifiedAfter = 16;
static const int _doesNotHaveTag = 17;
static const int _doesNotHaveAsn = 18; static const int _doesNotHaveAsn = 18;
static const int _moreLikeThis = 21; static const int _moreLikeThis = 21;
static const int _hasTagsIn = 22; static const int _hasTagsIn = 22;
@@ -76,11 +76,25 @@ class FilterRule with EquatableMixin {
? const StoragePathQuery.notAssigned() ? const StoragePathQuery.notAssigned()
: StoragePathQuery.fromId(int.parse(value!)), : StoragePathQuery.fromId(int.parse(value!)),
); );
case tagRule: case hasAnyTag:
return filter.copyWith( return filter.copyWith(
tags: value == null tags: value == "true"
? const TagsQuery.notAssigned() ? const AnyAssignedTagsQuery()
: TagsQuery.fromIds([...filter.tags.ids, int.parse(value!)]), : const OnlyNotAssignedTagsQuery(),
);
case includeTagsRule:
assert(filter.tags is IdsTagsQuery);
return filter.copyWith(
tags: (filter.tags as IdsTagsQuery).withIdQueriesAdded([
IncludeTagIdQuery(int.parse(value!)),
]),
);
case excludeTagsRule:
assert(filter.tags is IdsTagsQuery);
return filter.copyWith(
tags: (filter.tags as IdsTagsQuery).withIdQueriesAdded([
ExcludeTagIdQuery(int.parse(value!)),
]),
); );
case createdBeforeRule: case createdBeforeRule:
return filter.copyWith( return filter.copyWith(
@@ -131,12 +145,19 @@ class FilterRule with EquatableMixin {
filterRules filterRules
.add(FilterRule(storagePathRule, filter.storagePath.id!.toString())); .add(FilterRule(storagePathRule, filter.storagePath.id!.toString()));
} }
if (filter.tags.onlyNotAssigned) { if (filter.tags is OnlyNotAssignedTagsQuery) {
filterRules.add(FilterRule(tagRule, null)); filterRules.add(FilterRule(hasAnyTag, "false"));
} }
if (filter.tags.isSet) { if (filter.tags is AnyAssignedTagsQuery) {
filterRules.addAll( filterRules.add(FilterRule(hasAnyTag, "true"));
filter.tags.ids.map((id) => FilterRule(tagRule, id.toString()))); }
if (filter.tags is IdsTagsQuery) {
filterRules.addAll((filter.tags as IdsTagsQuery)
.includedIds
.map((id) => FilterRule(includeTagsRule, id.toString())));
filterRules.addAll((filter.tags as IdsTagsQuery)
.excludedIds
.map((id) => FilterRule(excludeTagsRule, id.toString())));
} }
if (filter.queryText != null) { if (filter.queryText != null) {

View File

@@ -2,7 +2,7 @@ enum SortField {
archiveSerialNumber("archive_serial_number"), archiveSerialNumber("archive_serial_number"),
correspondentName("correspondent__name"), correspondentName("correspondent__name"),
title("title"), title("title"),
documentType("documentType"), documentType("document_type__name"),
created("created"), created("created"),
added("added"), added("added"),
modified("modified"); modified("modified");

View File

@@ -1,38 +1,136 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:paperless_mobile/extensions/dart_extensions.dart';
class TagsQuery with EquatableMixin { abstract class TagsQuery with EquatableMixin {
final List<int> _ids; const TagsQuery();
final bool? _isTagged; String toQueryParameter();
}
const TagsQuery.fromIds(List<int> ids) class OnlyNotAssignedTagsQuery extends TagsQuery {
: _isTagged = null, const OnlyNotAssignedTagsQuery();
_ids = ids; @override
List<Object?> get props => [];
const TagsQuery.anyAssigned()
: _isTagged = true,
_ids = const [];
const TagsQuery.notAssigned()
: _isTagged = false,
_ids = const [];
const TagsQuery.unset() : this.fromIds(const []);
bool get onlyNotAssigned => _isTagged == false;
bool get onlyAssigned => _isTagged == true;
bool get isUnset => _ids.isEmpty && _isTagged == null;
bool get isSet => _ids.isNotEmpty && _isTagged == null;
List<int> get ids => _ids;
@override
String toQueryParameter() { String toQueryParameter() {
if (onlyNotAssigned || onlyAssigned) { return '&is_tagged=0';
return '&is_tagged=$_isTagged';
} }
return isUnset ? "" : '&tags__id__all=${ids.join(',')}'; }
class AnyAssignedTagsQuery extends TagsQuery {
const AnyAssignedTagsQuery();
@override
List<Object?> get props => [];
@override
String toQueryParameter() {
return '&is_tagged=1';
}
}
class IdsTagsQuery extends TagsQuery {
final Iterable<TagIdQuery> _idQueries;
const IdsTagsQuery([this._idQueries = const []]);
const IdsTagsQuery.unset() : _idQueries = const [];
IdsTagsQuery.included(Iterable<int> ids)
: _idQueries = ids.map((id) => IncludeTagIdQuery(id));
IdsTagsQuery.fromIds(Iterable<int> ids) : this.included(ids);
IdsTagsQuery.excluded(Iterable<int> ids)
: _idQueries = ids.map((id) => ExcludeTagIdQuery(id));
IdsTagsQuery withIdQueriesAdded(Iterable<TagIdQuery> idQueries) {
final intersection = _idQueries
.map((idQ) => idQ.id)
.toSet()
.intersection(_idQueries.map((idQ) => idQ.id).toSet());
return IdsTagsQuery(
[...withIdsRemoved(intersection).queries, ...idQueries],
);
}
IdsTagsQuery withIdsRemoved(Iterable<int> ids) {
return IdsTagsQuery(
_idQueries.where((idQuery) => !ids.contains(idQuery.id)),
);
}
Iterable<TagIdQuery> get queries => _idQueries;
Iterable<int> get includedIds {
return _idQueries.whereType<IncludeTagIdQuery>().map((e) => e.id);
}
Iterable<int> get excludedIds {
return _idQueries.whereType<ExcludeTagIdQuery>().map((e) => e.id);
}
///
/// Returns a new instance with the type of the given [id] toggled.
/// E.g. if the provided [id] is currently registered as a [IncludeTagIdQuery],
/// then the new isntance will contain a [ExcludeTagIdQuery] with given id.
///
IdsTagsQuery withIdQueryToggled(int id) {
return IdsTagsQuery(
_idQueries.map((idQ) => idQ.id == id ? idQ.toggle() : idQ),
);
}
Iterable<int> get ids => [...includedIds, ...excludedIds];
@override
String toQueryParameter() {
final StringBuffer sb = StringBuffer("");
if (includedIds.isNotEmpty) {
sb.write('&tags__id__all=${includedIds.join(',')}');
}
if (excludedIds.isNotEmpty) {
sb.write('&tags__id__none=${excludedIds.join(',')}');
}
return sb.toString();
} }
@override @override
List<Object?> get props => [_isTagged, _ids]; List<Object?> get props => [_idQueries];
}
abstract class TagIdQuery with EquatableMixin {
final int id;
TagIdQuery(this.id);
String get methodName;
@override
List<Object?> get props => [id, methodName];
TagIdQuery toggle();
}
class IncludeTagIdQuery extends TagIdQuery {
IncludeTagIdQuery(super.id);
@override
String get methodName => 'include';
@override
TagIdQuery toggle() {
return ExcludeTagIdQuery(id);
}
}
class ExcludeTagIdQuery extends TagIdQuery {
ExcludeTagIdQuery(super.id);
@override
String get methodName => 'exclude';
@override
TagIdQuery toggle() {
return IncludeTagIdQuery(id);
}
} }

View File

@@ -13,7 +13,7 @@ abstract class DocumentRepository {
required String title, required String title,
int? documentType, int? documentType,
int? correspondent, int? correspondent,
List<int>? tags, Iterable<int> tags = const [],
DateTime? createdAt, DateTime? createdAt,
}); });
Future<DocumentModel> update(DocumentModel doc); Future<DocumentModel> update(DocumentModel doc);

View File

@@ -5,6 +5,9 @@ import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:http/src/boundary_characters.dart'; //TODO: remove once there is either a paperless API update or there is a better solution...
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/store/local_vault.dart'; import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/core/type/types.dart'; import 'package:paperless_mobile/core/type/types.dart';
@@ -22,9 +25,6 @@ import 'package:paperless_mobile/features/documents/model/query_parameters/sort_
import 'package:paperless_mobile/features/documents/model/similar_document.model.dart'; import 'package:paperless_mobile/features/documents/model/similar_document.model.dart';
import 'package:paperless_mobile/features/documents/repository/document_repository.dart'; import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:http/http.dart';
import 'package:http/src/boundary_characters.dart'; //TODO: remove once there is either a paperless API update or there is a better solution...
import 'package:injectable/injectable.dart';
@Injectable(as: DocumentRepository) @Injectable(as: DocumentRepository)
class DocumentRepositoryImpl implements DocumentRepository { class DocumentRepositoryImpl implements DocumentRepository {
@@ -45,7 +45,7 @@ class DocumentRepositoryImpl implements DocumentRepository {
required String title, required String title,
int? documentType, int? documentType,
int? correspondent, int? correspondent,
List<int>? tags, Iterable<int> tags = const [],
DateTime? createdAt, DateTime? createdAt,
}) async { }) async {
final auth = await localStorage.loadAuthenticationInformation(); final auth = await localStorage.loadAuthenticationInformation();
@@ -78,7 +78,7 @@ class DocumentRepositoryImpl implements DocumentRepository {
bodyBuffer.write(_buildMultipartField(key, fields[key]!, boundary)); bodyBuffer.write(_buildMultipartField(key, fields[key]!, boundary));
} }
for (final tag in tags ?? <int>[]) { for (final tag in tags) {
bodyBuffer.write(_buildMultipartField('tags', tag.toString(), boundary)); bodyBuffer.write(_buildMultipartField('tags', tag.toString(), boundary));
} }
@@ -125,9 +125,9 @@ class DocumentRepositoryImpl implements DocumentRepository {
var prefix = 'dart-http-boundary-'; var prefix = 'dart-http-boundary-';
var list = List<int>.generate( var list = List<int>.generate(
70 - prefix.length, 70 - prefix.length,
(index) => (index) => boundaryCharacters[_random.nextInt(boundaryCharacters.length)],
boundaryCharacters[_random.nextInt(boundaryCharacters.length)], growable: false,
growable: false); );
return '$prefix${String.fromCharCodes(list)}'; return '$prefix${String.fromCharCodes(list)}';
} }

View File

@@ -420,7 +420,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Future<void> _onOpen(DocumentModel document) async { Future<void> _onOpen(DocumentModel document) async {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => DocumentView(document: document), builder: (context) => DocumentView(
documentBytes: getIt<DocumentRepository>().getPreview(document.id),
),
), ),
); );
} }

View File

@@ -53,9 +53,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override @override
void initState() { void initState() {
documentBytes = getIt<DocumentRepository>().getPreview(widget.document.id);
super.initState(); super.initState();
documentBytes = getIt<DocumentRepository>().getPreview(widget.document.id);
} }
@override @override
@@ -69,10 +68,14 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
final updatedDocument = widget.document.copyWith( final updatedDocument = widget.document.copyWith(
title: values[fkTitle], title: values[fkTitle],
created: values[fkCreatedDate], created: values[fkCreatedDate],
documentType: values[fkDocumentType] as IdQueryParameter, overwriteDocumentType: true,
correspondent: values[fkCorrespondent] as IdQueryParameter, documentType: (values[fkDocumentType] as IdQueryParameter).id,
storagePath: values[fkStoragePath] as IdQueryParameter, overwriteCorrespondent: true,
tags: values[fkTags] as TagsQuery, correspondent: (values[fkCorrespondent] as IdQueryParameter).id,
overwriteStoragePath: true,
storagePath: (values[fkStoragePath] as IdQueryParameter).id,
overwriteTags: true,
tags: (values[fkTags] as IdsTagsQuery).includedIds,
); );
setState(() { setState(() {
_isSubmitLoading = true; _isSubmitLoading = true;
@@ -181,7 +184,10 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
}, },
).padded(), ).padded(),
TagFormField( TagFormField(
initialValue: TagsQuery.fromIds(widget.document.tags), initialValue: IdsTagsQuery.included(widget.document.tags),
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
name: fkTags, name: fkTags,
).padded(), ).padded(),
]), ]),

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart';
@@ -6,11 +7,11 @@ import 'package:paperless_mobile/generated/l10n.dart';
import 'package:pdfx/pdfx.dart'; import 'package:pdfx/pdfx.dart';
class DocumentView extends StatefulWidget { class DocumentView extends StatefulWidget {
final DocumentModel document; final Future<Uint8List> documentBytes;
const DocumentView({ const DocumentView({
Key? key, Key? key,
required this.document, required this.documentBytes,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -25,7 +26,7 @@ class _DocumentViewState extends State<DocumentView> {
super.initState(); super.initState();
_pdfController = PdfController( _pdfController = PdfController(
document: PdfDocument.openData( document: PdfDocument.openData(
getIt<DocumentRepository>().getPreview(widget.document.id), widget.documentBytes,
), ),
); );
} }

View File

@@ -1,13 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart'; import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/service/github_issue_service.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/document.model.dart'; import 'package:paperless_mobile/features/documents/model/document.model.dart';
@@ -19,14 +14,15 @@ import 'package:paperless_mobile/features/documents/view/widgets/search/document
import 'package:paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart'; import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart'; import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart'; import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart';
class DocumentsPage extends StatefulWidget { class DocumentsPage extends StatefulWidget {
@@ -42,7 +38,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
firstPageKey: 1, firstPageKey: 1,
); );
final PanelController _panelController = PanelController(); final PanelController _filterPanelController = PanelController();
@override @override
void initState() { void initState() {
@@ -99,9 +95,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
if (_panelController.isPanelOpen) { if (_filterPanelController.isPanelOpen) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
_panelController.close(); _filterPanelController.close();
return false; return false;
} }
final documentsCubit = BlocProvider.of<DocumentsCubit>(context); final documentsCubit = BlocProvider.of<DocumentsCubit>(context);
@@ -129,7 +125,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
backdropEnabled: true, backdropEnabled: true,
parallaxEnabled: true, parallaxEnabled: true,
parallaxOffset: .5, parallaxOffset: .5,
controller: _panelController, controller: _filterPanelController,
defaultPanelState: PanelState.CLOSED, defaultPanelState: PanelState.CLOSED,
minHeight: 48, minHeight: 48,
maxHeight: (MediaQuery.of(context).size.height * 3) / 4, maxHeight: (MediaQuery.of(context).size.height * 3) / 4,
@@ -140,7 +136,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
body: _buildBody(connectivityState), body: _buildBody(connectivityState),
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
panelBuilder: (scrollController) => DocumentFilterPanel( panelBuilder: (scrollController) => DocumentFilterPanel(
panelController: _panelController, panelController: _filterPanelController,
scrollController: scrollController, scrollController: scrollController,
), ),
), ),
@@ -194,7 +190,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
return RefreshIndicator( return RefreshIndicator(
onRefresh: _onRefresh, onRefresh: _onRefresh,
child: Container(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
DocumentsPageAppBar( DocumentsPageAppBar(
@@ -221,7 +216,6 @@ class _DocumentsPageState extends State<DocumentsPage> {
) )
], ],
), ),
),
); );
}, },
); );

View File

@@ -66,8 +66,12 @@ class DocumentGridItem extends StatelessWidget {
tagIds: document.tags, tagIds: document.tags,
isMultiLine: false, isMultiLine: false,
), ),
Text(DateFormat.yMMMd(Intl.getCurrentLocale()) const Spacer(),
.format(document.created)), Text(
DateFormat.yMMMd(Intl.getCurrentLocale())
.format(document.created),
style: Theme.of(context).textTheme.caption,
),
], ],
), ),
), ),

View File

@@ -11,7 +11,6 @@ import 'package:paperless_mobile/features/documents/model/document_filter.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/query_type.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/query_type.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart'; import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart';
@@ -52,16 +51,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
static const fkCreatedAt = DocumentModel.createdKey; static const fkCreatedAt = DocumentModel.createdKey;
static const fkAddedAt = DocumentModel.addedKey; static const fkAddedAt = DocumentModel.addedKey;
static const _sortFields = [
SortField.created,
SortField.added,
SortField.modified,
SortField.title,
SortField.correspondentName,
SortField.documentType,
SortField.archiveSerialNumber
];
final _formKey = GlobalKey<FormBuilderState>(); final _formKey = GlobalKey<FormBuilderState>();
late final DocumentsCubit _documentsCubit; late final DocumentsCubit _documentsCubit;
@@ -99,8 +88,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
builder: (context, state) { builder: (context, state) {
return FormBuilder( return FormBuilder(
key: _formKey, key: _formKey,
child: ListView( child: Column(
controller: widget.scrollController,
children: [ children: [
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
@@ -137,15 +125,25 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
const SizedBox( const SizedBox(
height: 16.0, height: 16.0,
), ),
_buildSortByChipsList(context, state), Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
child: ListView(
controller: widget.scrollController,
children: [
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(S.of(context).documentsFilterPageSearchLabel), child: Text(
S.of(context).documentsFilterPageSearchLabel),
).padded(const EdgeInsets.only(left: 8.0)), ).padded(const EdgeInsets.only(left: 8.0)),
_buildQueryFormField(state), _buildQueryFormField(state),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(S.of(context).documentsFilterPageAdvancedLabel), child: Text(
S.of(context).documentsFilterPageAdvancedLabel),
).padded(const EdgeInsets.only(left: 8.0, top: 8.0)), ).padded(const EdgeInsets.only(left: 8.0, top: 8.0)),
_buildCreatedDateRangePickerFormField(state).padded(), _buildCreatedDateRangePickerFormField(state).padded(),
_buildAddedDateRangePickerFormField(state).padded(), _buildAddedDateRangePickerFormField(state).padded(),
@@ -159,7 +157,11 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
).padded(), ).padded(),
// Required in order for the storage path field to be visible when typing // Required in order for the storage path field to be visible when typing
const SizedBox( const SizedBox(
height: 200, height: 150,
),
],
).padded(),
),
), ),
], ],
), ),
@@ -385,6 +387,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
), ),
), ),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkCreatedAt), _buildDateRangePickerHelper(state, fkCreatedAt),
], ],
); );
@@ -432,6 +435,7 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
), ),
), ),
), ),
const SizedBox(height: 4.0),
_buildDateRangePickerHelper(state, fkAddedAt), _buildDateRangePickerHelper(state, fkAddedAt),
], ],
); );
@@ -448,71 +452,6 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
); );
} }
Widget _buildSortByChipsList(BuildContext context, DocumentsState state) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).documentsPageOrderByLabel,
),
SizedBox(
height: kToolbarHeight,
child: ListView.separated(
itemCount: _sortFields.length,
scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) => const SizedBox(
width: 8.0,
),
itemBuilder: (context, index) => _buildActionChip(
_sortFields[index], state.filter.sortField, context),
),
),
],
).padded();
}
Widget _buildActionChip(SortField sortField,
SortField? currentlySelectedOrder, BuildContext context) {
String text;
switch (sortField) {
case SortField.archiveSerialNumber:
text = S.of(context).documentArchiveSerialNumberPropertyShortLabel;
break;
case SortField.correspondentName:
text = S.of(context).documentCorrespondentPropertyLabel;
break;
case SortField.title:
text = S.of(context).documentTitlePropertyLabel;
break;
case SortField.documentType:
text = S.of(context).documentDocumentTypePropertyLabel;
break;
case SortField.created:
text = S.of(context).documentCreatedPropertyLabel;
break;
case SortField.added:
text = S.of(context).documentAddedPropertyLabel;
break;
case SortField.modified:
text = S.of(context).documentModifiedPropertyLabel;
break;
}
final docBloc = BlocProvider.of<DocumentsCubit>(context);
return ActionChip(
label: Text(text),
avatar: currentlySelectedOrder == sortField
? const Icon(
Icons.done,
color: Colors.green,
)
: null,
onPressed: () => docBloc.updateFilter(
filter: docBloc.state.filter.copyWith(sortField: sortField)),
);
}
void _onApplyFilter() async { void _onApplyFilter() async {
if (_formKey.currentState?.saveAndValidate() ?? false) { if (_formKey.currentState?.saveAndValidate() ?? false) {
final v = _formKey.currentState!.value; final v = _formKey.currentState!.value;

View File

@@ -0,0 +1,137 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_order.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class SortFieldSelectionBottomSheet extends StatefulWidget {
const SortFieldSelectionBottomSheet({super.key});
@override
State<SortFieldSelectionBottomSheet> createState() =>
_SortFieldSelectionBottomSheetState();
}
class _SortFieldSelectionBottomSheetState
extends State<SortFieldSelectionBottomSheet> {
static const _sortFields = [
SortField.created,
SortField.added,
SortField.modified,
SortField.title,
SortField.correspondentName,
SortField.documentType,
SortField.archiveSerialNumber
];
SortField? _selectedFieldLoading;
SortOrder? _selectedOrderLoading;
@override
Widget build(BuildContext context) {
return ClipRRect(
child: BlocBuilder<DocumentsCubit, DocumentsState>(
bloc: getIt<DocumentsCubit>(),
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).documentsPageOrderByLabel,
style: Theme.of(context).textTheme.caption,
textAlign: TextAlign.start,
).padded(
const EdgeInsets.symmetric(horizontal: 16, vertical: 16)),
Column(
children: _sortFields
.map(
(e) => _buildSortOption(
e,
state.filter.sortOrder,
state.filter.sortField == e,
_selectedFieldLoading == e,
),
)
.toList(),
),
],
);
},
),
);
}
Widget _buildSortOption(
SortField field,
SortOrder order,
bool isCurrentlySelected,
bool isNextSelected,
) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text(
_localizedSortField(field),
style: Theme.of(context).textTheme.bodyText2,
),
trailing: isNextSelected
? (_buildOrderIcon(_selectedOrderLoading!))
: (_selectedOrderLoading == null && isCurrentlySelected
? _buildOrderIcon(order)
: null),
onTap: () async {
setState(() {
_selectedFieldLoading = field;
_selectedOrderLoading =
isCurrentlySelected ? order.toggle() : SortOrder.descending;
});
BlocProvider.of<DocumentsCubit>(context)
.updateCurrentFilter((filter) => filter.copyWith(
sortOrder: isCurrentlySelected
? order.toggle()
: SortOrder.descending,
sortField: field,
))
.whenComplete(() {
if (mounted) {
setState(() {
_selectedFieldLoading = null;
_selectedOrderLoading = null;
});
}
});
},
);
}
Widget _buildOrderIcon(SortOrder order) {
if (order == SortOrder.ascending) {
return const Icon(Icons.arrow_upward);
}
return const Icon(Icons.arrow_downward);
}
String _localizedSortField(SortField sortField) {
switch (sortField) {
case SortField.archiveSerialNumber:
return S.of(context).documentArchiveSerialNumberPropertyShortLabel;
case SortField.correspondentName:
return S.of(context).documentCorrespondentPropertyLabel;
case SortField.title:
return S.of(context).documentTitlePropertyLabel;
case SortField.documentType:
return S.of(context).documentDocumentTypePropertyLabel;
case SortField.created:
return S.of(context).documentCreatedPropertyLabel;
case SortField.added:
return S.of(context).documentAddedPropertyLabel;
case SortField.modified:
return S.of(context).documentModifiedPropertyLabel;
}
}
}

View File

@@ -18,10 +18,10 @@ class ConfirmDeleteSavedViewDialog extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text( title: Text(
"Delete view " + view.name + "?", S.of(context).deleteViewDialogTitleText + view.name + "?",
softWrap: true, softWrap: true,
), ),
content: Text("Do you really want to delete this view?"), content: Text(S.of(context).deleteViewDialogContentText),
actions: [ actions: [
TextButton( TextButton(
child: Text(S.of(context).genericActionCancelLabel), child: Text(S.of(context).genericActionCancelLabel),

View File

@@ -77,7 +77,7 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
Widget _buildFlexibleArea(bool enabled) { Widget _buildFlexibleArea(bool enabled) {
return FlexibleSpaceBar( return FlexibleSpaceBar(
background: Padding( background: Padding(
padding: EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [

View File

@@ -1,11 +1,17 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart'; import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/error_message.dart'; import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_field.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/sort_order.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/sort_order.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/sort_field_selection_bottom_sheet.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
class SortDocumentsButton extends StatefulWidget { class SortDocumentsButton extends StatefulWidget {
@@ -18,52 +24,32 @@ class SortDocumentsButton extends StatefulWidget {
} }
class _SortDocumentsButtonState extends State<SortDocumentsButton> { class _SortDocumentsButtonState extends State<SortDocumentsButton> {
bool _isLoading = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<DocumentsCubit, DocumentsState>( return IconButton(
builder: (context, state) { icon: const Icon(Icons.sort),
Widget child; onPressed: _onOpenSortBottomSheet,
if (_isLoading) {
child = const FittedBox(
fit: BoxFit.scaleDown,
child: RefreshProgressIndicator(
strokeWidth: 4.0,
backgroundColor: Colors.transparent,
),
);
} else {
final bool isAscending =
state.filter.sortOrder == SortOrder.ascending;
child = IconButton(
icon: FaIcon(
isAscending
? FontAwesomeIcons.arrowDownAZ
: FontAwesomeIcons.arrowUpZA,
),
onPressed: () async {
setState(() => _isLoading = true);
try {
await BlocProvider.of<DocumentsCubit>(context)
.updateCurrentFilter(
(filter) => filter.copyWith(
sortOrder: state.filter.sortOrder.toggle(),
),
);
} on ErrorMessage catch (error, stackTrace) {
showError(context, error, stackTrace);
} finally {
setState(() => _isLoading = false);
}
},
); );
} }
return SizedBox(
height: Theme.of(context).iconTheme.size, void _onOpenSortBottomSheet() {
width: Theme.of(context).iconTheme.size, showModalBottomSheet(
child: child, elevation: 2,
); context: context,
}, isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
builder: (context) => BlocProvider.value(
value: getIt<DocumentsCubit>(),
child: const FractionallySizedBox(
heightFactor: .6,
child: SortFieldSelectionBottomSheet(),
),
),
); );
} }
} }

View File

@@ -22,11 +22,16 @@ class InfoDrawer extends StatelessWidget {
const InfoDrawer({Key? key}) : super(key: key); const InfoDrawer({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Drawer( return ClipRRect(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16.0),
bottomRight: Radius.circular(16.0),
),
child: Drawer(
child: ListView( child: ListView(
children: [ children: [
DrawerHeader( DrawerHeader(
padding: EdgeInsets.only( padding: const EdgeInsets.only(
top: 8, top: 8,
left: 8, left: 8,
bottom: 0, bottom: 0,
@@ -78,7 +83,8 @@ class InfoDrawer extends StatelessWidget {
children: [ children: [
Text( Text(
state.host ?? '', state.host ?? '',
style: Theme.of(context).textTheme.bodyText2, style:
Theme.of(context).textTheme.bodyText2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end, textAlign: TextAlign.end,
maxLines: 1, maxLines: 1,
@@ -192,6 +198,7 @@ class InfoDrawer extends StatelessWidget {
const Divider(), const Divider(),
], ],
), ),
),
); );
} }

View File

@@ -48,13 +48,15 @@ class EditTagPage extends StatelessWidget {
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
final currentFilter = cubit.state.filter; final currentFilter = cubit.state.filter;
late DocumentFilter updatedFilter = currentFilter; late DocumentFilter updatedFilter = currentFilter;
if (currentFilter.tags.ids.contains(tag.id)) { if (currentFilter.tags is IdsTagsQuery) {
if ((currentFilter.tags as IdsTagsQuery).includedIds.contains(tag.id)) {
updatedFilter = currentFilter.copyWith( updatedFilter = currentFilter.copyWith(
tags: TagsQuery.fromIds( tags: (currentFilter.tags as IdsTagsQuery).withIdsRemoved(
currentFilter.tags.ids.where((tagId) => tagId != tag.id).toList(), [tag.id!],
), ),
); );
} }
}
cubit.updateFilter(filter: updatedFilter); cubit.updateFilter(filter: updatedFilter);
Navigator.pop(context); Navigator.pop(context);
} on ErrorMessage catch (error, stackTrace) { } on ErrorMessage catch (error, stackTrace) {

View File

@@ -18,8 +18,13 @@ class TagWidget extends StatelessWidget {
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: BlocBuilder<DocumentsCubit, DocumentsState>( child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) { builder: (context, state) {
final isIdsQuery = state.filter.tags is IdsTagsQuery;
return FilterChip( return FilterChip(
selected: state.filter.tags.ids.contains(tag.id), selected: isIdsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tag.id)
: false,
selectedColor: tag.color, selectedColor: tag.color,
onSelected: (_) => _addTagToFilter(context), onSelected: (_) => _addTagToFilter(context),
visualDensity: const VisualDensity(vertical: -2), visualDensity: const VisualDensity(vertical: -2),
@@ -40,18 +45,19 @@ class TagWidget extends StatelessWidget {
void _addTagToFilter(BuildContext context) { void _addTagToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context); final cubit = BlocProvider.of<DocumentsCubit>(context);
try { try {
if (cubit.state.filter.tags.ids.contains(tag.id)) { final tagsQuery = cubit.state.filter.tags is IdsTagsQuery
? cubit.state.filter.tags as IdsTagsQuery
: const IdsTagsQuery();
if (tagsQuery.includedIds.contains(tag.id)) {
cubit.updateCurrentFilter( cubit.updateCurrentFilter(
(filter) => filter.copyWith( (filter) => filter.copyWith(
tags: TagsQuery.fromIds(cubit.state.filter.tags.ids tags: tagsQuery.withIdsRemoved([tag.id!]),
.where((id) => id != tag.id)
.toList()),
), ),
); );
} else { } else {
cubit.updateCurrentFilter( cubit.updateCurrentFilter(
(filter) => filter.copyWith( (filter) => filter.copyWith(
tags: TagsQuery.fromIds([...cubit.state.filter.tags.ids, tag.id!]), tags: tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tag.id!)]),
), ),
); );
} }

View File

@@ -13,6 +13,8 @@ class TagFormField extends StatefulWidget {
final String name; final String name;
final bool allowCreation; final bool allowCreation;
final bool notAssignedSelectable; final bool notAssignedSelectable;
final bool anyAssignedSelectable;
final bool excludeAllowed;
const TagFormField({ const TagFormField({
super.key, super.key,
@@ -20,6 +22,8 @@ class TagFormField extends StatefulWidget {
this.initialValue, this.initialValue,
this.allowCreation = true, this.allowCreation = true,
this.notAssignedSelectable = true, this.notAssignedSelectable = true,
this.anyAssignedSelectable = true,
this.excludeAllowed = true,
}); });
@override @override
@@ -27,8 +31,11 @@ class TagFormField extends StatefulWidget {
} }
class _TagFormFieldState extends State<TagFormField> { class _TagFormFieldState extends State<TagFormField> {
static const _onlyNotAssignedId = -1;
static const _anyAssignedId = -2;
late final TextEditingController _textEditingController; late final TextEditingController _textEditingController;
bool _showCreationSuffixIcon = false; bool _showCreationSuffixIcon = true;
bool _showClearSuffixIcon = false; bool _showClearSuffixIcon = false;
@override @override
@@ -44,7 +51,8 @@ class _TagFormFieldState extends State<TagFormField> {
_textEditingController.text.toLowerCase(), _textEditingController.text.toLowerCase(),
), ),
) )
.isEmpty; .isEmpty ||
_textEditingController.text.isEmpty;
}); });
setState(() => setState(() =>
_showClearSuffixIcon = _textEditingController.text.isNotEmpty); _showClearSuffixIcon = _textEditingController.text.isNotEmpty);
@@ -78,21 +86,32 @@ class _TagFormFieldState extends State<TagFormField> {
.toLowerCase() .toLowerCase()
.startsWith(query.toLowerCase())) .startsWith(query.toLowerCase()))
.map((e) => e.id!) .map((e) => e.id!)
.toList() .toList();
..removeWhere((element) => if (field.value is IdsTagsQuery) {
field.value?.ids.contains(element) ?? false); suggestions.removeWhere((element) =>
if (widget.notAssignedSelectable) { (field.value as IdsTagsQuery).ids.contains(element));
suggestions.insert(0, -1); }
if (widget.notAssignedSelectable &&
field.value is! OnlyNotAssignedTagsQuery) {
suggestions.insert(0, _onlyNotAssignedId);
}
if (widget.anyAssignedSelectable &&
field.value is! AnyAssignedTagsQuery) {
suggestions.insert(0, _anyAssignedId);
} }
return suggestions; return suggestions;
}, },
getImmediateSuggestions: true, getImmediateSuggestions: true,
animationStart: 1, animationStart: 1,
itemBuilder: (context, data) { itemBuilder: (context, data) {
if (data == -1) { if (data == _onlyNotAssignedId) {
return ListTile( return ListTile(
title: Text(S.of(context).labelNotAssignedText), title: Text(S.of(context).labelNotAssignedText),
); );
} else if (data == _anyAssignedId) {
return ListTile(
title: Text(S.of(context).labelAnyAssignedText),
);
} }
final tag = tagState[data]!; final tag = tagState[data]!;
return ListTile( return ListTile(
@@ -108,33 +127,48 @@ class _TagFormFieldState extends State<TagFormField> {
); );
}, },
onSuggestionSelected: (id) { onSuggestionSelected: (id) {
if (id == -1) { if (id == _onlyNotAssignedId) {
field.didChange(const TagsQuery.notAssigned()); //Not assigned tag
field.didChange(const OnlyNotAssignedTagsQuery());
return; return;
} else if (id == _anyAssignedId) {
field.didChange(const AnyAssignedTagsQuery());
} else { } else {
field.didChange( final tagsQuery = field.value is IdsTagsQuery
TagsQuery.fromIds([...field.value?.ids ?? [], id])); ? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange(tagsQuery
.withIdQueriesAdded([IncludeTagIdQuery(id)]));
} }
_textEditingController.clear(); _textEditingController.clear();
}, },
direction: AxisDirection.up, direction: AxisDirection.up,
), ),
if (field.value?.onlyNotAssigned ?? false) ...[ if (field.value is OnlyNotAssignedTagsQuery) ...[
_buildNotAssignedTag(field) _buildNotAssignedTag(field)
] else if (field.value is AnyAssignedTagsQuery) ...[
_buildAnyAssignedTag(field)
] else ...[ ] else ...[
// field.value is IdsTagsQuery
Wrap( Wrap(
alignment: WrapAlignment.start, alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start, runAlignment: WrapAlignment.start,
spacing: 8.0, spacing: 8.0,
children: (field.value?.ids ?? []) children: ((field.value as IdsTagsQuery).queries)
.map((id) => _buildTag(field, tagState[id]!)) .map(
(query) => _buildTag(
field,
query,
tagState[query.id]!,
),
)
.toList(), .toList(),
), ),
] ]
], ],
); );
}, },
initialValue: widget.initialValue ?? const TagsQuery.unset(), initialValue: widget.initialValue ?? const IdsTagsQuery(),
name: widget.name, name: widget.name,
); );
}, },
@@ -172,8 +206,11 @@ class _TagFormFieldState extends State<TagFormField> {
), ),
); );
if (tag != null) { if (tag != null) {
final tagsQuery = field.value is IdsTagsQuery
? field.value as IdsTagsQuery
: const IdsTagsQuery();
field.didChange( field.didChange(
TagsQuery.fromIds([...field.value?.ids ?? [], tag.id!]), tagsQuery.withIdQueriesAdded([IncludeTagIdQuery(tag.id!)]),
); );
} }
_textEditingController.clear(); _textEditingController.clear();
@@ -191,24 +228,43 @@ class _TagFormFieldState extends State<TagFormField> {
), ),
backgroundColor: backgroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.12), Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
onDeleted: () => field.didChange(const IdsTagsQuery()),
);
}
Widget _buildTag(
FormFieldState<TagsQuery> field,
TagIdQuery query,
Tag tag,
) {
final currentQuery = field.value as IdsTagsQuery;
final isIncludedTag = currentQuery.includedIds.contains(query.id);
return InputChip(
label: Text(
tag.name,
style: TextStyle(
color: tag.textColor,
decoration: !isIncludedTag ? TextDecoration.lineThrough : null,
decorationThickness: 2.0,
),
),
onPressed: widget.excludeAllowed
? () => field.didChange(currentQuery.withIdQueryToggled(tag.id!))
: null,
backgroundColor: tag.color,
onDeleted: () => field.didChange( onDeleted: () => field.didChange(
const TagsQuery.unset(), (field.value as IdsTagsQuery).withIdsRemoved([tag.id!]),
), ),
); );
} }
Widget _buildTag(FormFieldState<TagsQuery> field, Tag tag) { Widget _buildAnyAssignedTag(FormFieldState<TagsQuery> field) {
return InputChip( return InputChip(
label: Text( label: Text(S.of(context).labelAnyAssignedText),
tag.name, backgroundColor:
style: TextStyle(color: tag.textColor), Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.12),
), onDeleted: () => field.didChange(const IdsTagsQuery()),
backgroundColor: tag.color,
onDeleted: () => field.didChange(
TagsQuery.fromIds(
field.value?.ids.where((element) => element != tag.id).toList() ?? [],
),
),
); );
} }
} }

View File

@@ -7,7 +7,7 @@ import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatefulWidget { class TagsWidget extends StatefulWidget {
final List<int> tagIds; final Iterable<int> tagIds;
final bool isMultiLine; final bool isMultiLine;
final void Function()? afterTagTapped; final void Function()? afterTagTapped;
final bool isClickable; final bool isClickable;

View File

@@ -3,31 +3,29 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart'; import 'package:paperless_mobile/core/bloc/label_bloc_provider.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/correspondent_query.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/document_type_query.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/storage_path_query.dart';
import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart'; import 'package:paperless_mobile/features/documents/model/query_parameters/tags_query.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/edit_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/documents/model/document_filter.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/correspondent/bloc/correspondents_cubit.dart';
import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart'; import 'package:paperless_mobile/features/labels/correspondent/model/correspondent.model.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart'; import 'package:paperless_mobile/features/labels/correspondent/view/pages/add_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/pages/edit_correspondent_page.dart';
import 'package:paperless_mobile/features/labels/document_type/bloc/document_type_cubit.dart';
import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart'; import 'package:paperless_mobile/features/labels/document_type/model/document_type.model.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart'; import 'package:paperless_mobile/features/labels/document_type/view/pages/add_document_type_page.dart';
import 'package:paperless_mobile/features/labels/document_type/view/pages/edit_document_type_page.dart'; import 'package:paperless_mobile/features/labels/document_type/view/pages/edit_document_type_page.dart';
import 'package:paperless_mobile/features/labels/model/label.model.dart';
import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart'; import 'package:paperless_mobile/features/labels/storage_path/bloc/storage_path_cubit.dart';
import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart'; import 'package:paperless_mobile/features/labels/storage_path/model/storage_path.model.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/pages/add_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/pages/edit_storage_path_page.dart'; import 'package:paperless_mobile/features/labels/storage_path/view/pages/edit_storage_path_page.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart'; import 'package:paperless_mobile/features/labels/tags/model/tag.model.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart'; import 'package:paperless_mobile/features/labels/tags/view/pages/add_tag_page.dart';
import 'package:paperless_mobile/features/labels/tags/view/pages/edit_tag_page.dart'; import 'package:paperless_mobile/features/labels/tags/view/pages/edit_tag_page.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart'; import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/features/labels/tags/bloc/tags_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';
class LabelsPage extends StatefulWidget { class LabelsPage extends StatefulWidget {
@@ -147,7 +145,7 @@ class _LabelsPageState extends State<LabelsPage>
LabelTabView<Tag>( LabelTabView<Tag>(
cubit: BlocProvider.of<TagCubit>(context), cubit: BlocProvider.of<TagCubit>(context),
filterBuilder: (label) => DocumentFilter( filterBuilder: (label) => DocumentFilter(
tags: TagsQuery.fromIds([label.id!]), tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0, pageSize: label.documentCount ?? 0,
), ),
onOpenEditPage: _openEditTagPage, onOpenEditPage: _openEditTagPage,

View File

@@ -172,6 +172,9 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
), ),
const TagFormField( const TagFormField(
name: DocumentModel.tagsKey, name: DocumentModel.tagsKey,
notAssignedSelectable: false,
anyAssignedSelectable: false,
excludeAllowed: false,
//Label: "Tags" + " *", //Label: "Tags" + " *",
), ),
Text( Text(
@@ -194,10 +197,9 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
final createdAt = fv[DocumentModel.createdKey] as DateTime?; final createdAt = fv[DocumentModel.createdKey] as DateTime?;
final title = fv[DocumentModel.titleKey] as String; final title = fv[DocumentModel.titleKey] as String;
final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter; final docType = fv[DocumentModel.documentTypeKey] as IdQueryParameter;
final tags = fv[DocumentModel.tagsKey] as TagsQuery; final tags = fv[DocumentModel.tagsKey] as IdsTagsQuery;
final correspondent = final correspondent =
fv[DocumentModel.correspondentKey] as IdQueryParameter; fv[DocumentModel.correspondentKey] as IdQueryParameter;
await BlocProvider.of<DocumentsCubit>(context).addDocument( await BlocProvider.of<DocumentsCubit>(context).addDocument(
widget.fileBytes, widget.fileBytes,
_formKey.currentState?.value[fkFileName], _formKey.currentState?.value[fkFileName],

View File

@@ -14,6 +14,8 @@ import 'package:paperless_mobile/core/model/error_message.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.dart'; import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/repository/document_repository.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart'; import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart'; import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/document_upload_page.dart'; import 'package:paperless_mobile/features/scan/view/document_upload_page.dart';
@@ -53,6 +55,24 @@ class _ScannerPageState extends State<ScannerPage>
return AppBar( return AppBar(
title: Text(S.of(context).documentScannerPageTitle), title: Text(S.of(context).documentScannerPageTitle),
actions: [ actions: [
BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) {
return IconButton(
onPressed: state.isNotEmpty
? () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes:
_buildDocumentFromImageFiles(state).save(),
),
),
)
: null,
icon: const Icon(Icons.preview),
tooltip: S.of(context).documentScannerPageResetButtonTooltipText,
);
},
),
BlocBuilder<DocumentScannerCubit, List<File>>( BlocBuilder<DocumentScannerCubit, List<File>>(
builder: (context, state) { builder: (context, state) {
return IconButton( return IconButton(

View File

@@ -192,5 +192,8 @@
"errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.", "errorMessageMissingClientCertificate": "Ein Client Zerfitikat wurde erwartet, aber nicht gesendet. Bitte konfiguriere ein gültiges Zertifikat.",
"serverInformationPaperlessVersionText": "Paperless Server-Version", "serverInformationPaperlessVersionText": "Paperless Server-Version",
"errorReportLabel": "MELDEN", "errorReportLabel": "MELDEN",
"appDrawerHeaderLoggedInAsText": "Eingeloggt als " "appDrawerHeaderLoggedInAsText": "Eingeloggt als ",
"labelAnyAssignedText": "Beliebig zugewiesen",
"deleteViewDialogContentText": "Möchtest Du diese Ansicht wirklich löschen?",
"deleteViewDialogTitleText": "Lösche Ansicht "
} }

View File

@@ -193,5 +193,8 @@
"errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.", "errorMessageMissingClientCertificate": "A client certificate was expected but not sent. Please provide a valid client certificate.",
"serverInformationPaperlessVersionText": "Paperless server version", "serverInformationPaperlessVersionText": "Paperless server version",
"errorReportLabel": "REPORT", "errorReportLabel": "REPORT",
"appDrawerHeaderLoggedInAsText": "Logged in as " "appDrawerHeaderLoggedInAsText": "Logged in as ",
"labelAnyAssignedText": "Any assigned",
"deleteViewDialogContentText": "Do you really want to delete this view?",
"deleteViewDialogTitleText": "Delete view "
} }

View File

@@ -35,7 +35,7 @@ void main() async {
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
Intl.systemLocale = await findSystemLocale(); Intl.systemLocale = await findSystemLocale();
// Required for client certificates // Required for self signed client certificates
HttpOverrides.global = X509HttpOverrides(); HttpOverrides.global = X509HttpOverrides();
configureDependencies(); configureDependencies();

View File

@@ -31,15 +31,15 @@ void main() {
'value': "69", 'value': "69",
}, },
{ {
'rule_type': FilterRule.tagRule, 'rule_type': FilterRule.includeTagsRule,
'value': "1", 'value': "1",
}, },
{ {
'rule_type': FilterRule.tagRule, 'rule_type': FilterRule.includeTagsRule,
'value': "2", 'value': "2",
}, },
{ {
'rule_type': FilterRule.tagRule, 'rule_type': FilterRule.includeTagsRule,
'value': "3", 'value': "3",
}, },
{ {
@@ -73,7 +73,7 @@ void main() {
correspondent: const CorrespondentQuery.fromId(42), correspondent: const CorrespondentQuery.fromId(42),
documentType: const DocumentTypeQuery.fromId(69), documentType: const DocumentTypeQuery.fromId(69),
storagePath: const StoragePathQuery.fromId(14), storagePath: const StoragePathQuery.fromId(14),
tags: const TagsQuery.fromIds([1, 2, 3]), tags: IdsTagsQuery.fromIds([1, 2, 3]),
createdDateBefore: DateTime.parse("2022-10-27"), createdDateBefore: DateTime.parse("2022-10-27"),
createdDateAfter: DateTime.parse("2022-09-27"), createdDateAfter: DateTime.parse("2022-09-27"),
addedDateBefore: DateTime.parse("2022-09-26"), addedDateBefore: DateTime.parse("2022-09-26"),
@@ -121,7 +121,7 @@ void main() {
'value': null, 'value': null,
}, },
{ {
'rule_type': FilterRule.tagRule, 'rule_type': FilterRule.includeTagsRule,
'value': null, 'value': null,
}, },
{ {
@@ -134,7 +134,7 @@ void main() {
correspondent: const CorrespondentQuery.notAssigned(), correspondent: const CorrespondentQuery.notAssigned(),
documentType: const DocumentTypeQuery.notAssigned(), documentType: const DocumentTypeQuery.notAssigned(),
storagePath: const StoragePathQuery.notAssigned(), storagePath: const StoragePathQuery.notAssigned(),
tags: const TagsQuery.notAssigned(), tags: const OnlyNotAssignedTagsQuery(),
)), )),
); );
}); });
@@ -148,7 +148,7 @@ void main() {
correspondent: const CorrespondentQuery.fromId(1), correspondent: const CorrespondentQuery.fromId(1),
documentType: const DocumentTypeQuery.fromId(2), documentType: const DocumentTypeQuery.fromId(2),
storagePath: const StoragePathQuery.fromId(3), storagePath: const StoragePathQuery.fromId(3),
tags: const TagsQuery.fromIds([4, 5, 6]), tags: IdsTagsQuery.fromIds([4, 5, 6]),
sortField: SortField.added, sortField: SortField.added,
sortOrder: SortOrder.ascending, sortOrder: SortOrder.ascending,
addedDateAfter: DateTime.parse("2020-01-01"), addedDateAfter: DateTime.parse("2020-01-01"),
@@ -173,9 +173,9 @@ void main() {
FilterRule(FilterRule.correspondentRule, "1"), FilterRule(FilterRule.correspondentRule, "1"),
FilterRule(FilterRule.documentTypeRule, "2"), FilterRule(FilterRule.documentTypeRule, "2"),
FilterRule(FilterRule.storagePathRule, "3"), FilterRule(FilterRule.storagePathRule, "3"),
FilterRule(FilterRule.tagRule, "4"), FilterRule(FilterRule.includeTagsRule, "4"),
FilterRule(FilterRule.tagRule, "5"), FilterRule(FilterRule.includeTagsRule, "5"),
FilterRule(FilterRule.tagRule, "6"), FilterRule(FilterRule.includeTagsRule, "6"),
FilterRule(FilterRule.addedAfterRule, "2020-01-01"), FilterRule(FilterRule.addedAfterRule, "2020-01-01"),
FilterRule(FilterRule.addedBeforeRule, "2020-03-01"), FilterRule(FilterRule.addedBeforeRule, "2020-03-01"),
FilterRule(FilterRule.createdAfterRule, "2020-02-01"), FilterRule(FilterRule.createdAfterRule, "2020-02-01"),
@@ -194,7 +194,7 @@ void main() {
correspondent: CorrespondentQuery.unset(), correspondent: CorrespondentQuery.unset(),
documentType: DocumentTypeQuery.unset(), documentType: DocumentTypeQuery.unset(),
storagePath: StoragePathQuery.unset(), storagePath: StoragePathQuery.unset(),
tags: TagsQuery.unset(), tags: IdsTagsQuery.unset(),
sortField: SortField.created, sortField: SortField.created,
sortOrder: SortOrder.descending, sortOrder: SortOrder.descending,
addedDateAfter: null, addedDateAfter: null,
@@ -227,7 +227,7 @@ void main() {
correspondent: CorrespondentQuery.notAssigned(), correspondent: CorrespondentQuery.notAssigned(),
documentType: DocumentTypeQuery.notAssigned(), documentType: DocumentTypeQuery.notAssigned(),
storagePath: StoragePathQuery.notAssigned(), storagePath: StoragePathQuery.notAssigned(),
tags: TagsQuery.notAssigned(), tags: OnlyNotAssignedTagsQuery(),
sortField: SortField.created, sortField: SortField.created,
sortOrder: SortOrder.descending, sortOrder: SortOrder.descending,
), ),
@@ -246,7 +246,7 @@ void main() {
FilterRule(FilterRule.correspondentRule, null), FilterRule(FilterRule.correspondentRule, null),
FilterRule(FilterRule.documentTypeRule, null), FilterRule(FilterRule.documentTypeRule, null),
FilterRule(FilterRule.storagePathRule, null), FilterRule(FilterRule.storagePathRule, null),
FilterRule(FilterRule.tagRule, null), FilterRule(FilterRule.includeTagsRule, null),
], ],
), ),
), ),