fix: Add labels to each cubit using repositories and state properties, remove label cubits

This commit is contained in:
Anton Stubenbord
2023-04-04 20:30:25 +02:00
parent 78fbd042a6
commit a2388b014b
95 changed files with 4790 additions and 1823 deletions

View File

@@ -1,34 +0,0 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:rxdart/subjects.dart';
///
/// Base repository class which all repositories should implement
///
abstract class BaseRepository<T> extends Cubit<IndexedRepositoryState<T>>
with HydratedMixin {
final IndexedRepositoryState<T> _initialState;
BaseRepository(this._initialState) : super(_initialState) {
hydrate();
}
Stream<IndexedRepositoryState<T>?> get values =>
BehaviorSubject.seeded(state)..addStream(super.stream);
IndexedRepositoryState<T>? get current => state;
bool get isInitialized => state.hasLoaded;
Future<T> create(T object);
Future<T?> find(int id);
Future<Iterable<T>> findAll([Iterable<int>? ids]);
Future<T> update(T object);
Future<int> delete(T object);
@override
Future<void> clear() async {
await super.clear();
emit(_initialState);
}
}

View File

@@ -1,69 +0,0 @@
import 'dart:async';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
class CorrespondentRepositoryImpl extends LabelRepository<Correspondent> {
final PaperlessLabelsApi _api;
CorrespondentRepositoryImpl(this._api)
: super(const CorrespondentRepositoryState());
@override
Future<Correspondent> create(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return created;
}
@override
Future<int> delete(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == correspondent.id);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondent.id!;
}
@override
Future<Correspondent?> find(int id) async {
final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) {
final updatedState = {...state.values ?? {}}..[id] = correspondent;
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondent;
}
return null;
}
@override
Future<Iterable<Correspondent>> findAll([Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids);
final updatedState = {...state.values ?? {}}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return correspondents;
}
@override
Future<Correspondent> update(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(CorrespondentRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@override
CorrespondentRepositoryState fromJson(Map<String, dynamic> json) {
return CorrespondentRepositoryState.fromJson(json);
}
@override
Map<String, dynamic> toJson(covariant CorrespondentRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,67 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
class DocumentTypeRepositoryImpl extends LabelRepository<DocumentType> {
final PaperlessLabelsApi _api;
DocumentTypeRepositoryImpl(this._api)
: super(const DocumentTypeRepositoryState());
@override
Future<DocumentType> create(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return created;
}
@override
Future<int> delete(DocumentType documentType) async {
await _api.deleteDocumentType(documentType);
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == documentType.id);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentType.id!;
}
@override
Future<DocumentType?> find(int id) async {
final documentType = await _api.getDocumentType(id);
if (documentType != null) {
final updatedState = {...state.values ?? {}}..[id] = documentType;
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentType;
}
return null;
}
@override
Future<Iterable<DocumentType>> findAll([Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.values ?? {}}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return documentTypes;
}
@override
Future<DocumentType> update(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(DocumentTypeRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@override
DocumentTypeRepositoryState fromJson(Map<String, dynamic> json) {
return DocumentTypeRepositoryState.fromJson(json);
}
@override
Map<String, dynamic> toJson(covariant DocumentTypeRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,62 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
class SavedViewRepositoryImpl extends SavedViewRepository {
final PaperlessSavedViewsApi _api;
SavedViewRepositoryImpl(this._api) : super(const SavedViewRepositoryState());
@override
Future<SavedView> create(SavedView object) async {
final created = await _api.save(object);
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return created;
}
@override
Future<int> delete(SavedView view) async {
await _api.delete(view);
final updatedState = {...state.values ?? {}}..remove(view.id);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return view.id!;
}
@override
Future<SavedView?> find(int id) async {
final found = await _api.find(id);
final updatedState = {...state.values ?? {}}
..update(id, (_) => found, ifAbsent: () => found);
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return found;
}
@override
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids);
final updatedState = {
...state.values ?? {},
...{for (final view in found) view.id!: view},
};
emit(SavedViewRepositoryState(values: updatedState, hasLoaded: true));
return found;
}
@override
Future<SavedView> update(SavedView object) {
throw UnimplementedError(
"Saved view update is not yet implemented as it is not supported by the API.");
}
@override
SavedViewRepositoryState fromJson(Map<String, dynamic> json) {
return SavedViewRepositoryState.fromJson(json);
}
@override
Map<String, dynamic> toJson(covariant SavedViewRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,68 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
class StoragePathRepositoryImpl extends LabelRepository<StoragePath> {
final PaperlessLabelsApi _api;
StoragePathRepositoryImpl(this._api)
: super(const StoragePathRepositoryState());
@override
Future<StoragePath> create(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return created;
}
@override
Future<int> delete(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath);
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == storagePath.id);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePath.id!;
}
@override
Future<StoragePath?> find(int id) async {
final storagePath = await _api.getStoragePath(id);
if (storagePath != null) {
final updatedState = {...state.values ?? {}}..[id] = storagePath;
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePath;
}
return null;
}
@override
Future<Iterable<StoragePath>> findAll([Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.values ?? {}}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return storagePaths;
}
@override
Future<StoragePath> update(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(StoragePathRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@override
StoragePathRepositoryState fromJson(Map<String, dynamic> json) {
return StoragePathRepositoryState.fromJson(json);
}
@override
Map<String, dynamic> toJson(covariant StoragePathRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,66 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
class TagRepositoryImpl extends LabelRepository<Tag> {
final PaperlessLabelsApi _api;
TagRepositoryImpl(this._api) : super(const TagRepositoryState());
@override
Future<Tag> create(Tag object) async {
final created = await _api.saveTag(object);
final updatedState = {...state.values ?? {}}
..putIfAbsent(created.id!, () => created);
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return created;
}
@override
Future<int> delete(Tag tag) async {
await _api.deleteTag(tag);
final updatedState = {...state.values ?? {}}
..removeWhere((k, v) => k == tag.id);
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tag.id!;
}
@override
Future<Tag?> find(int id) async {
final tag = await _api.getTag(id);
if (tag != null) {
final updatedState = {...state.values ?? {}}..[id] = tag;
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tag;
}
return null;
}
@override
Future<Iterable<Tag>> findAll([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids);
final updatedState = {...state.values ?? {}}
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return tags;
}
@override
Future<Tag> update(Tag tag) async {
final updated = await _api.updateTag(tag);
final updatedState = {...state.values ?? {}}
..update(updated.id!, (_) => updated);
emit(TagRepositoryState(values: updatedState, hasLoaded: true));
return updated;
}
@override
TagRepositoryState? fromJson(Map<String, dynamic> json) {
return TagRepositoryState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(covariant TagRepositoryState state) {
return state.toJson();
}
}

View File

@@ -1,7 +1,218 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/base_repository.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'dart:async';
abstract class LabelRepository<T extends Label> extends BaseRepository<T> {
LabelRepository(IndexedRepositoryState<T> initial) : super(initial);
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
class LabelRepository extends HydratedCubit<LabelRepositoryState> {
final PaperlessLabelsApi _api;
final Map<Object, StreamSubscription> _subscribers = {};
void subscribe(
Object source, {
required void Function(LabelRepositoryState) onChanged,
}) {
_subscribers.putIfAbsent(source, () {
onChanged(state);
return stream.listen((event) => onChanged(event));
});
}
void unsubscribe(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
}
LabelRepository(this._api) : super(const LabelRepositoryState());
Future<Tag> createTag(Tag object) async {
final created = await _api.saveTag(object);
final updatedState = {...state.tags}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(tags: updatedState));
return created;
}
Future<int> deleteTag(Tag tag) async {
await _api.deleteTag(tag);
final updatedState = {...state.tags}..removeWhere((k, v) => k == tag.id);
emit(state.copyWith(tags: updatedState));
return tag.id!;
}
Future<Tag?> findTag(int id) async {
final tag = await _api.getTag(id);
if (tag != null) {
final updatedState = {...state.tags}..[id] = tag;
emit(state.copyWith(tags: updatedState));
return tag;
}
return null;
}
Future<Iterable<Tag>> findAllTags([Iterable<int>? ids]) async {
final tags = await _api.getTags(ids);
final updatedState = {...state.tags}
..addEntries(tags.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(tags: updatedState));
return tags;
}
Future<Tag> updateTag(Tag tag) async {
final updated = await _api.updateTag(tag);
final updatedState = {...state.tags}..update(updated.id!, (_) => updated);
emit(state.copyWith(tags: updatedState));
return updated;
}
Future<Correspondent> createCorrespondent(Correspondent correspondent) async {
final created = await _api.saveCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(correspondents: updatedState));
return created;
}
Future<int> deleteCorrespondent(Correspondent correspondent) async {
await _api.deleteCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..removeWhere((k, v) => k == correspondent.id);
emit(state.copyWith(correspondents: updatedState));
return correspondent.id!;
}
Future<Correspondent?> findCorrespondent(int id) async {
final correspondent = await _api.getCorrespondent(id);
if (correspondent != null) {
final updatedState = {...state.correspondents}..[id] = correspondent;
emit(state.copyWith(correspondents: updatedState));
return correspondent;
}
return null;
}
Future<Iterable<Correspondent>> findAllCorrespondents(
[Iterable<int>? ids]) async {
final correspondents = await _api.getCorrespondents(ids);
final updatedState = {...state.correspondents}
..addEntries(correspondents.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(correspondents: updatedState));
return correspondents;
}
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
final updated = await _api.updateCorrespondent(correspondent);
final updatedState = {...state.correspondents}
..update(updated.id!, (_) => updated);
emit(state.copyWith(correspondents: updatedState));
return updated;
}
Future<DocumentType> createDocumentType(DocumentType documentType) async {
final created = await _api.saveDocumentType(documentType);
final updatedState = {...state.documentTypes}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(documentTypes: updatedState));
return created;
}
Future<int> deleteDocumentType(DocumentType documentType) async {
await _api.deleteDocumentType(documentType);
final updatedState = {...state.documentTypes}
..removeWhere((k, v) => k == documentType.id);
emit(state.copyWith(documentTypes: updatedState));
return documentType.id!;
}
Future<DocumentType?> findDocumentType(int id) async {
final documentType = await _api.getDocumentType(id);
if (documentType != null) {
final updatedState = {...state.documentTypes}..[id] = documentType;
emit(state.copyWith(documentTypes: updatedState));
return documentType;
}
return null;
}
Future<Iterable<DocumentType>> findAllDocumentTypes(
[Iterable<int>? ids]) async {
final documentTypes = await _api.getDocumentTypes(ids);
final updatedState = {...state.documentTypes}
..addEntries(documentTypes.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(documentTypes: updatedState));
return documentTypes;
}
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
final updated = await _api.updateDocumentType(documentType);
final updatedState = {...state.documentTypes}
..update(updated.id!, (_) => updated);
emit(state.copyWith(documentTypes: updatedState));
return updated;
}
Future<StoragePath> createStoragePath(StoragePath storagePath) async {
final created = await _api.saveStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(storagePaths: updatedState));
return created;
}
Future<int> deleteStoragePath(StoragePath storagePath) async {
await _api.deleteStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..removeWhere((k, v) => k == storagePath.id);
emit(state.copyWith(storagePaths: updatedState));
return storagePath.id!;
}
Future<StoragePath?> findStoragePath(int id) async {
final storagePath = await _api.getStoragePath(id);
if (storagePath != null) {
final updatedState = {...state.storagePaths}..[id] = storagePath;
emit(state.copyWith(storagePaths: updatedState));
return storagePath;
}
return null;
}
Future<Iterable<StoragePath>> findAllStoragePaths(
[Iterable<int>? ids]) async {
final storagePaths = await _api.getStoragePaths(ids);
final updatedState = {...state.storagePaths}
..addEntries(storagePaths.map((e) => MapEntry(e.id!, e)));
emit(state.copyWith(storagePaths: updatedState));
return storagePaths;
}
Future<StoragePath> updateStoragePath(StoragePath storagePath) async {
final updated = await _api.updateStoragePath(storagePath);
final updatedState = {...state.storagePaths}
..update(updated.id!, (_) => updated);
emit(state.copyWith(storagePaths: updatedState));
return updated;
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
@override
LabelRepositoryState? fromJson(Map<String, dynamic> json) {
return LabelRepositoryState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(LabelRepositoryState state) {
return state.toJson();
}
}

View File

@@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
part 'label_repository_state.freezed.dart';
part 'label_repository_state.g.dart';
@freezed
class LabelRepositoryState with _$LabelRepositoryState {
const factory LabelRepositoryState({
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,
@Default({}) Map<int, StoragePath> storagePaths,
}) = _LabelRepositoryState;
factory LabelRepositoryState.fromJson(Map<String, dynamic> json) =>
_$LabelRepositoryStateFromJson(json);
}

View File

@@ -0,0 +1,258 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'label_repository_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
LabelRepositoryState _$LabelRepositoryStateFromJson(Map<String, dynamic> json) {
return _LabelRepositoryState.fromJson(json);
}
/// @nodoc
mixin _$LabelRepositoryState {
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LabelRepositoryStateCopyWith<LabelRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LabelRepositoryStateCopyWith<$Res> {
factory $LabelRepositoryStateCopyWith(LabelRepositoryState value,
$Res Function(LabelRepositoryState) then) =
_$LabelRepositoryStateCopyWithImpl<$Res, LabelRepositoryState>;
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$LabelRepositoryStateCopyWithImpl<$Res,
$Val extends LabelRepositoryState>
implements $LabelRepositoryStateCopyWith<$Res> {
_$LabelRepositoryStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_LabelRepositoryStateCopyWith<$Res>
implements $LabelRepositoryStateCopyWith<$Res> {
factory _$$_LabelRepositoryStateCopyWith(_$_LabelRepositoryState value,
$Res Function(_$_LabelRepositoryState) then) =
__$$_LabelRepositoryStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_LabelRepositoryStateCopyWithImpl<$Res>
extends _$LabelRepositoryStateCopyWithImpl<$Res, _$_LabelRepositoryState>
implements _$$_LabelRepositoryStateCopyWith<$Res> {
__$$_LabelRepositoryStateCopyWithImpl(_$_LabelRepositoryState _value,
$Res Function(_$_LabelRepositoryState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_LabelRepositoryState(
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_LabelRepositoryState implements _LabelRepositoryState {
const _$_LabelRepositoryState(
{final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
factory _$_LabelRepositoryState.fromJson(Map<String, dynamic> json) =>
_$$_LabelRepositoryStateFromJson(json);
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'LabelRepositoryState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_LabelRepositoryState &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_LabelRepositoryStateCopyWith<_$_LabelRepositoryState> get copyWith =>
__$$_LabelRepositoryStateCopyWithImpl<_$_LabelRepositoryState>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_LabelRepositoryStateToJson(
this,
);
}
}
abstract class _LabelRepositoryState implements LabelRepositoryState {
const factory _LabelRepositoryState(
{final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_LabelRepositoryState;
factory _LabelRepositoryState.fromJson(Map<String, dynamic> json) =
_$_LabelRepositoryState.fromJson;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_LabelRepositoryStateCopyWith<_$_LabelRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,35 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
class LabelRepositoriesProvider extends StatelessWidget {
final Widget child;
const LabelRepositoriesProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider(
create: (context) => context.read<LabelRepository<Correspondent>>(),
),
RepositoryProvider(
create: (context) => context.read<LabelRepository<DocumentType>>(),
),
RepositoryProvider(
create: (context) => context.read<LabelRepository<StoragePath>>(),
),
RepositoryProvider(
create: (context) => context.read<LabelRepository<Tag>>(),
),
],
child: child,
);
}
}

View File

@@ -1,8 +1,80 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/base_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/saved_view_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'dart:async';
abstract class SavedViewRepository extends BaseRepository<SavedView> {
SavedViewRepository(super.initialState);
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository_state.dart';
class SavedViewRepository extends HydratedCubit<SavedViewRepositoryState> {
final PaperlessSavedViewsApi _api;
final Map<Object, StreamSubscription> _subscribers = {};
void subscribe(
Object source,
void Function(Map<int, SavedView>) onChanged,
) {
_subscribers.putIfAbsent(source, () {
onChanged(state.savedViews);
return stream.listen((event) => onChanged(event.savedViews));
});
}
void unsubscribe(Object source) async {
await _subscribers[source]?.cancel();
_subscribers.remove(source);
}
SavedViewRepository(this._api) : super(const SavedViewRepositoryState());
Future<SavedView> create(SavedView object) async {
final created = await _api.save(object);
final updatedState = {...state.savedViews}
..putIfAbsent(created.id!, () => created);
emit(state.copyWith(savedViews: updatedState));
return created;
}
Future<int> delete(SavedView view) async {
await _api.delete(view);
final updatedState = {...state.savedViews}..remove(view.id);
emit(state.copyWith(savedViews: updatedState));
return view.id!;
}
Future<SavedView?> find(int id) async {
final found = await _api.find(id);
if (found != null) {
final updatedState = {...state.savedViews}
..update(id, (_) => found, ifAbsent: () => found);
emit(state.copyWith(savedViews: updatedState));
}
return found;
}
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids);
final updatedState = {
...state.savedViews,
...{for (final view in found) view.id!: view},
};
emit(state.copyWith(savedViews: updatedState));
return found;
}
@override
Future<void> close() {
_subscribers.forEach((key, subscription) {
subscription.cancel();
});
return super.close();
}
@override
SavedViewRepositoryState? fromJson(Map<String, dynamic> json) {
return SavedViewRepositoryState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(SavedViewRepositoryState state) {
return state.toJson();
}
}

View File

@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
part 'saved_view_repository_state.freezed.dart';
part 'saved_view_repository_state.g.dart';
@freezed
class SavedViewRepositoryState with _$SavedViewRepositoryState {
const factory SavedViewRepositoryState({
@Default({}) Map<int, SavedView> savedViews,
}) = _SavedViewRepositoryState;
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$SavedViewRepositoryStateFromJson(json);
}

View File

@@ -0,0 +1,167 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'saved_view_repository_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
SavedViewRepositoryState _$SavedViewRepositoryStateFromJson(
Map<String, dynamic> json) {
return _SavedViewRepositoryState.fromJson(json);
}
/// @nodoc
mixin _$SavedViewRepositoryState {
Map<int, SavedView> get savedViews => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SavedViewRepositoryStateCopyWith<SavedViewRepositoryState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SavedViewRepositoryStateCopyWith<$Res> {
factory $SavedViewRepositoryStateCopyWith(SavedViewRepositoryState value,
$Res Function(SavedViewRepositoryState) then) =
_$SavedViewRepositoryStateCopyWithImpl<$Res, SavedViewRepositoryState>;
@useResult
$Res call({Map<int, SavedView> savedViews});
}
/// @nodoc
class _$SavedViewRepositoryStateCopyWithImpl<$Res,
$Val extends SavedViewRepositoryState>
implements $SavedViewRepositoryStateCopyWith<$Res> {
_$SavedViewRepositoryStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? savedViews = null,
}) {
return _then(_value.copyWith(
savedViews: null == savedViews
? _value.savedViews
: savedViews // ignore: cast_nullable_to_non_nullable
as Map<int, SavedView>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_SavedViewRepositoryStateCopyWith<$Res>
implements $SavedViewRepositoryStateCopyWith<$Res> {
factory _$$_SavedViewRepositoryStateCopyWith(
_$_SavedViewRepositoryState value,
$Res Function(_$_SavedViewRepositoryState) then) =
__$$_SavedViewRepositoryStateCopyWithImpl<$Res>;
@override
@useResult
$Res call({Map<int, SavedView> savedViews});
}
/// @nodoc
class __$$_SavedViewRepositoryStateCopyWithImpl<$Res>
extends _$SavedViewRepositoryStateCopyWithImpl<$Res,
_$_SavedViewRepositoryState>
implements _$$_SavedViewRepositoryStateCopyWith<$Res> {
__$$_SavedViewRepositoryStateCopyWithImpl(_$_SavedViewRepositoryState _value,
$Res Function(_$_SavedViewRepositoryState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? savedViews = null,
}) {
return _then(_$_SavedViewRepositoryState(
savedViews: null == savedViews
? _value._savedViews
: savedViews // ignore: cast_nullable_to_non_nullable
as Map<int, SavedView>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_SavedViewRepositoryState implements _SavedViewRepositoryState {
const _$_SavedViewRepositoryState(
{final Map<int, SavedView> savedViews = const {}})
: _savedViews = savedViews;
factory _$_SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$$_SavedViewRepositoryStateFromJson(json);
final Map<int, SavedView> _savedViews;
@override
@JsonKey()
Map<int, SavedView> get savedViews {
if (_savedViews is EqualUnmodifiableMapView) return _savedViews;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_savedViews);
}
@override
String toString() {
return 'SavedViewRepositoryState(savedViews: $savedViews)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_SavedViewRepositoryState &&
const DeepCollectionEquality()
.equals(other._savedViews, _savedViews));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_savedViews));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_SavedViewRepositoryStateCopyWith<_$_SavedViewRepositoryState>
get copyWith => __$$_SavedViewRepositoryStateCopyWithImpl<
_$_SavedViewRepositoryState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_SavedViewRepositoryStateToJson(
this,
);
}
}
abstract class _SavedViewRepositoryState implements SavedViewRepositoryState {
const factory _SavedViewRepositoryState(
{final Map<int, SavedView> savedViews}) = _$_SavedViewRepositoryState;
factory _SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =
_$_SavedViewRepositoryState.fromJson;
@override
Map<int, SavedView> get savedViews;
@override
@JsonKey(ignore: true)
_$$_SavedViewRepositoryStateCopyWith<_$_SavedViewRepositoryState>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -1,31 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
part 'correspondent_repository_state.g.dart';
@JsonSerializable()
class CorrespondentRepositoryState
extends IndexedRepositoryState<Correspondent> {
const CorrespondentRepositoryState({
super.values = const {},
super.hasLoaded,
});
@override
CorrespondentRepositoryState copyWith({
Map<int, Correspondent>? values,
bool? hasLoaded,
}) {
return CorrespondentRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,
);
}
factory CorrespondentRepositoryState.fromJson(Map<String, dynamic> json) =>
_$CorrespondentRepositoryStateFromJson(json);
Map<String, dynamic> toJson() => _$CorrespondentRepositoryStateToJson(this);
}

View File

@@ -1,29 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart';
part 'document_type_repository_state.g.dart';
@JsonSerializable()
class DocumentTypeRepositoryState extends IndexedRepositoryState<DocumentType> {
const DocumentTypeRepositoryState({
super.values = const {},
super.hasLoaded,
});
@override
DocumentTypeRepositoryState copyWith({
Map<int, DocumentType>? values,
bool? hasLoaded,
}) {
return DocumentTypeRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,
);
}
factory DocumentTypeRepositoryState.fromJson(Map<String, dynamic> json) =>
_$DocumentTypeRepositoryStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentTypeRepositoryStateToJson(this);
}

View File

@@ -1,29 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart';
part 'saved_view_repository_state.g.dart';
@JsonSerializable()
class SavedViewRepositoryState extends IndexedRepositoryState<SavedView> {
const SavedViewRepositoryState({
super.values = const {},
super.hasLoaded = false,
});
@override
SavedViewRepositoryState copyWith({
Map<int, SavedView>? values,
bool? hasLoaded,
}) {
return SavedViewRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,
);
}
factory SavedViewRepositoryState.fromJson(Map<String, dynamic> json) =>
_$SavedViewRepositoryStateFromJson(json);
Map<String, dynamic> toJson() => _$SavedViewRepositoryStateToJson(this);
}

View File

@@ -1,29 +0,0 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:json_annotation/json_annotation.dart';
part 'storage_path_repository_state.g.dart';
@JsonSerializable()
class StoragePathRepositoryState extends IndexedRepositoryState<StoragePath> {
const StoragePathRepositoryState({
super.values = const {},
super.hasLoaded = false,
});
@override
StoragePathRepositoryState copyWith({
Map<int, StoragePath>? values,
bool? hasLoaded,
}) {
return StoragePathRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,
);
}
factory StoragePathRepositoryState.fromJson(Map<String, dynamic> json) =>
_$StoragePathRepositoryStateFromJson(json);
Map<String, dynamic> toJson() => _$StoragePathRepositoryStateToJson(this);
}

View File

@@ -1,29 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
part 'tag_repository_state.g.dart';
@JsonSerializable()
class TagRepositoryState extends IndexedRepositoryState<Tag> {
const TagRepositoryState({
super.values = const {},
super.hasLoaded = false,
});
@override
TagRepositoryState copyWith({
Map<int, Tag>? values,
bool? hasLoaded,
}) {
return TagRepositoryState(
values: values ?? this.values,
hasLoaded: hasLoaded ?? this.hasLoaded,
);
}
factory TagRepositoryState.fromJson(Map<String, dynamic> json) =>
_$TagRepositoryStateFromJson(json);
Map<String, dynamic> toJson() => _$TagRepositoryStateToJson(this);
}

View File

@@ -1,16 +0,0 @@
abstract class IndexedRepositoryState<T> {
final Map<int, T>? values;
final bool hasLoaded;
const IndexedRepositoryState({
required this.values,
this.hasLoaded = false,
}) : assert(!(values == null) || !hasLoaded);
IndexedRepositoryState.loaded(this.values) : hasLoaded = true;
IndexedRepositoryState<T> copyWith({
Map<int, T>? values,
bool? hasLoaded,
});
}

View File

@@ -6,45 +6,28 @@ import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_bulk_action_state.dart';
part 'document_bulk_action_cubit.freezed.dart';
class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
final PaperlessDocumentsApi _documentsApi;
final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType> _documentTypeRepository;
final LabelRepository<Tag> _tagRepository;
final LabelRepository<StoragePath> _storagePathRepository;
final LabelRepository _labelRepository;
final DocumentChangedNotifier _notifier;
final List<StreamSubscription> _subscriptions = [];
DocumentBulkActionCubit(
this._documentsApi,
this._correspondentRepository,
this._documentTypeRepository,
this._tagRepository,
this._storagePathRepository,
this._labelRepository,
this._notifier, {
required List<DocumentModel> selection,
}) : super(
DocumentBulkActionState(
selection: selection,
correspondentOptions:
(_correspondentRepository.current?.hasLoaded ?? false)
? _correspondentRepository.current!.values!
: {},
tagOptions: (_tagRepository.current?.hasLoaded ?? false)
? _tagRepository.current!.values!
: {},
documentTypeOptions:
(_documentTypeRepository.current?.hasLoaded ?? false)
? _documentTypeRepository.current!.values!
: {},
storagePathOptions:
(_storagePathRepository.current?.hasLoaded ?? false)
? _storagePathRepository.current!.values!
: {},
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
),
) {
_notifier.subscribe(
@@ -60,35 +43,18 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
);
},
);
_subscriptions.add(
_tagRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(tagOptions: event!.values));
}
}),
);
_subscriptions.add(
_correspondentRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(
correspondentOptions: event!.values,
));
}
}),
);
_subscriptions.add(
_documentTypeRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(documentTypeOptions: event!.values));
}
}),
);
_subscriptions.add(
_storagePathRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(storagePathOptions: event!.values));
}
}),
_labelRepository.subscribe(
this,
onChanged: (labels) {
emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
),
);
},
);
}
@@ -177,9 +143,7 @@ class DocumentBulkActionCubit extends Cubit<DocumentBulkActionState> {
@override
Future<void> close() {
_notifier.unsubscribe(this);
for (final sub in _subscriptions) {
sub.cancel();
}
_labelRepository.unsubscribe(this);
return super.close();
}
}

View File

@@ -0,0 +1,269 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document_bulk_action_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$DocumentBulkActionState {
List<DocumentModel> get selection => throw _privateConstructorUsedError;
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DocumentBulkActionStateCopyWith<DocumentBulkActionState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentBulkActionStateCopyWith<$Res> {
factory $DocumentBulkActionStateCopyWith(DocumentBulkActionState value,
$Res Function(DocumentBulkActionState) then) =
_$DocumentBulkActionStateCopyWithImpl<$Res, DocumentBulkActionState>;
@useResult
$Res call(
{List<DocumentModel> selection,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$DocumentBulkActionStateCopyWithImpl<$Res,
$Val extends DocumentBulkActionState>
implements $DocumentBulkActionStateCopyWith<$Res> {
_$DocumentBulkActionStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? selection = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
selection: null == selection
? _value.selection
: selection // ignore: cast_nullable_to_non_nullable
as List<DocumentModel>,
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_DocumentBulkActionStateCopyWith<$Res>
implements $DocumentBulkActionStateCopyWith<$Res> {
factory _$$_DocumentBulkActionStateCopyWith(_$_DocumentBulkActionState value,
$Res Function(_$_DocumentBulkActionState) then) =
__$$_DocumentBulkActionStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<DocumentModel> selection,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_DocumentBulkActionStateCopyWithImpl<$Res>
extends _$DocumentBulkActionStateCopyWithImpl<$Res,
_$_DocumentBulkActionState>
implements _$$_DocumentBulkActionStateCopyWith<$Res> {
__$$_DocumentBulkActionStateCopyWithImpl(_$_DocumentBulkActionState _value,
$Res Function(_$_DocumentBulkActionState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? selection = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_DocumentBulkActionState(
selection: null == selection
? _value._selection
: selection // ignore: cast_nullable_to_non_nullable
as List<DocumentModel>,
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_DocumentBulkActionState extends _DocumentBulkActionState {
const _$_DocumentBulkActionState(
{required final List<DocumentModel> selection,
required final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes,
required final Map<int, Tag> tags,
required final Map<int, StoragePath> storagePaths})
: _selection = selection,
_correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths,
super._();
final List<DocumentModel> _selection;
@override
List<DocumentModel> get selection {
if (_selection is EqualUnmodifiableListView) return _selection;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_selection);
}
final Map<int, Correspondent> _correspondents;
@override
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'DocumentBulkActionState(selection: $selection, correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_DocumentBulkActionState &&
const DeepCollectionEquality()
.equals(other._selection, _selection) &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_selection),
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_DocumentBulkActionStateCopyWith<_$_DocumentBulkActionState>
get copyWith =>
__$$_DocumentBulkActionStateCopyWithImpl<_$_DocumentBulkActionState>(
this, _$identity);
}
abstract class _DocumentBulkActionState extends DocumentBulkActionState {
const factory _DocumentBulkActionState(
{required final List<DocumentModel> selection,
required final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes,
required final Map<int, Tag> tags,
required final Map<int, StoragePath> storagePaths}) =
_$_DocumentBulkActionState;
const _DocumentBulkActionState._() : super._();
@override
List<DocumentModel> get selection;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_DocumentBulkActionStateCopyWith<_$_DocumentBulkActionState>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -1,44 +1,15 @@
part of 'document_bulk_action_cubit.dart';
class DocumentBulkActionState extends Equatable {
final List<DocumentModel> selection;
final Map<int, Correspondent> correspondentOptions;
final Map<int, DocumentType> documentTypeOptions;
final Map<int, Tag> tagOptions;
final Map<int, StoragePath> storagePathOptions;
const DocumentBulkActionState({
this.correspondentOptions = const {},
this.documentTypeOptions = const {},
this.tagOptions = const {},
this.storagePathOptions = const {},
this.selection = const [],
});
@override
List<Object> get props => [
selection,
correspondentOptions,
documentTypeOptions,
tagOptions,
storagePathOptions,
];
@freezed
class DocumentBulkActionState with _$DocumentBulkActionState {
const DocumentBulkActionState._();
const factory DocumentBulkActionState({
required List<DocumentModel> selection,
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _DocumentBulkActionState;
Iterable<int> get selectedIds => selection.map((d) => d.id);
DocumentBulkActionState copyWith({
List<DocumentModel>? selection,
Map<int, Correspondent>? correspondentOptions,
Map<int, DocumentType>? documentTypeOptions,
Map<int, Tag>? tagOptions,
Map<int, StoragePath>? storagePathOptions,
}) {
return DocumentBulkActionState(
selection: selection ?? this.selection,
correspondentOptions: correspondentOptions ?? this.correspondentOptions,
documentTypeOptions: documentTypeOptions ?? this.documentTypeOptions,
storagePathOptions: storagePathOptions ?? this.storagePathOptions,
tagOptions: tagOptions ?? this.tagOptions,
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
class BulkEditPage<int, T extends Label> extends StatefulWidget {
final bool enableMultipleChoice;
final Map<int, T> availableOptions;
const BulkEditPage({
super.key,
required this.enableMultipleChoice,
required this.availableOptions,
});
@override
State<BulkEditPage> createState() => _BulkEditPageState();
}
class _BulkEditPageState extends State<BulkEditPage> {
@override
Widget build(BuildContext context) {
return Container();
}
}

View File

@@ -2,12 +2,11 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class BulkEditTagsBottomSheet extends StatefulWidget {
@@ -20,29 +19,42 @@ class BulkEditTagsBottomSheet extends StatefulWidget {
class _BulkEditTagsBottomSheetState extends State<BulkEditTagsBottomSheet> {
final _formKey = GlobalKey<FormBuilderState>();
List<int> _tagsToRemove = [];
List<int> _tagsToAdd = [];
final _textEditingController = TextEditingController();
late Set<int> _sharedTags;
late Set<int> _nonSharedTags;
final Set<int> _sharedTagsToRemove = {};
final Set<int> _nonSharedTagsToRemove = {};
final Set<int> _tagsToAdd = {};
@override
void initState() {
super.initState();
final state = context.read<DocumentBulkActionCubit>().state;
_sharedTags = state.selection
.map((doc) => doc.tags)
.reduce((previousValue, element) =>
previousValue.toSet().intersection(element.toSet()))
.toSet();
print(_sharedTags.map((e) => e).join(", "));
_nonSharedTags = state.selection
.map((doc) => doc.tags)
.flattened
.toSet()
.difference(_sharedTags)
.toSet();
print(_nonSharedTags.map((e) => e).join(", "));
}
@override
Widget build(BuildContext context) {
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
final sharedTags = state.selection
.map((doc) => doc.tags)
.reduce((previousValue, element) =>
previousValue.toSet().intersection(element.toSet()))
.toList();
final nonSharedTags = state.selection
.map((doc) => doc.tags)
.flattened
.toSet()
.difference(sharedTags.toSet())
.toList();
return Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
print(state);
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
@@ -54,25 +66,123 @@ class _BulkEditTagsBottomSheetState extends State<BulkEditTagsBottomSheet> {
"Bulk modify tags",
style: Theme.of(context).textTheme.titleLarge,
).paddedOnly(bottom: 24),
FormBuilder(
key: _formKey,
child: TagFormField(
initialValue: IdsTagsQuery(
sharedTags.map((tag) => IncludeTagIdQuery(tag)),
TypeAheadFormField<Tag>(
textFieldConfiguration: TextFieldConfiguration(
controller: _textEditingController,
decoration: const InputDecoration(
labelText: "Tags",
hintText: "Start typing to add tags...",
),
name: "labelFormField",
selectableOptions: state.tagOptions,
allowCreation: false,
anyAssignedSelectable: false,
excludeAllowed: false,
),
onSuggestionSelected: (suggestion) {
setState(() {
_tagsToAdd.add(suggestion.id!);
});
_textEditingController.clear();
},
itemBuilder: (context, option) {
return ListTile(
leading: SizedBox(
width: 32,
height: 32,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: option.color!,
),
),
),
title: Text(option.name),
);
},
suggestionsCallback: (pattern) {
final searchString = pattern.toLowerCase();
return state.tags.entries
.where(
(tag) => tag.value.name
.toLowerCase()
.contains(searchString),
)
.map((e) => e.key)
.toSet()
.difference(_sharedTags)
.difference(_nonSharedTags)
.map((e) => state.tags[e]!);
},
),
Text("Shared tags"),
Wrap(
children: _sharedTags
.map(
(tag) => RemovableTagWidget(
tag: state.tags[tag]!,
onDeleted: (tag) {
setState(() {
_sharedTagsToRemove.add(tag);
_sharedTags.remove(tag);
});
},
),
)
.toList(),
),
const SizedBox(height: 8),
Text("Tags removed after apply"),
Wrap(),
Text("Non-shared tags"),
Wrap(
children: _nonSharedTags
.map(
(tag) => RemovableTagWidget(
tag: state.tags[tag]!,
onDeleted: (tag) {
setState(() {
_nonSharedTagsToRemove.add(tag);
_nonSharedTags.remove(tag);
});
},
),
)
.toList(),
),
Text("Remove"),
Wrap(
children: _sharedTagsToRemove.map((tag) {
return RemovableTagWidget(
tag: state.tags[tag]!,
onDeleted: (tag) {
setState(() {
_sharedTagsToRemove.remove(tag);
_sharedTags.add(tag);
});
},
);
}).toList() +
_nonSharedTagsToRemove.map((tag) {
return RemovableTagWidget(
tag: state.tags[tag]!,
onDeleted: (tag) {
setState(() {
_nonSharedTagsToRemove.remove(tag);
_nonSharedTags.add(tag);
});
},
);
}).toList(),
),
const SizedBox(height: 8),
Text("Tags added after apply"),
Wrap(),
Text("Add"),
Wrap(
children: _tagsToAdd
.map(
(tag) => RemovableTagWidget(
tag: state.tags[tag]!,
onDeleted: (tag) {
setState(() {
_tagsToAdd.remove(tag);
});
}),
)
.toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
@@ -107,3 +217,28 @@ class _BulkEditTagsBottomSheetState extends State<BulkEditTagsBottomSheet> {
);
}
}
class RemovableTagWidget extends StatelessWidget {
final Tag tag;
final void Function(int tagId) onDeleted;
const RemovableTagWidget(
{super.key, required this.tag, required this.onDeleted});
@override
Widget build(BuildContext context) {
return Chip(
label: Text(
tag.name,
style: TextStyle(
color: tag.textColor,
),
),
onDeleted: () => onDeleted(tag.id!),
deleteIcon: Icon(Icons.clear),
backgroundColor: tag.color,
deleteIconColor: tag.textColor,
padding: EdgeInsets.zero,
side: BorderSide.none,
);
}
}

View File

@@ -0,0 +1,30 @@
// import 'package:flutter/material.dart';
// import 'package:flutter/src/widgets/framework.dart';
// import 'package:flutter/src/widgets/placeholder.dart';
// class LabelBulkSelectionWidget extends StatelessWidget {
// final int labelId;
// final String title;
// final bool selected;
// final bool excluded;
// final Widget Function(int id) leadingWidgetBuilder;
// final void Function(int id) onSelected;
// final void Function(int id) onUnselected;
// final void Function(int id) onRemoved;
// const LabelBulkSelectionWidget({
// super.key,
// required this.labelId,
// required this.title,
// required this.leadingWidgetBuilder,
// required this.onSelected,
// required this.onUnselected,
// required this.onRemoved,
// });
// @override
// Widget build(BuildContext context) {
// return ListTile(
// title: Text(title),
// );
// }
// }

View File

@@ -2,32 +2,47 @@ import 'dart:async';
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/file_description.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
part 'document_details_cubit.freezed.dart';
part 'document_details_state.dart';
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final PaperlessDocumentsApi _api;
final DocumentChangedNotifier _notifier;
final LocalNotificationService _notificationService;
final LabelRepository _labelRepository;
final List<StreamSubscription> _subscriptions = [];
DocumentDetailsCubit(
this._api,
this._labelRepository,
this._notifier,
this._notificationService, {
required DocumentModel initialDocument,
}) : super(DocumentDetailsState(document: initialDocument)) {
}) : super(DocumentDetailsState(
document: initialDocument,
)) {
_notifier.subscribe(this, onUpdated: replace);
_labelRepository.subscribe(
this,
onChanged: (labels) => emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
),
),
);
loadSuggestions();
loadMetaData();
}

View File

@@ -0,0 +1,350 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document_details_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$DocumentDetailsState {
DocumentModel get document => throw _privateConstructorUsedError;
DocumentMetaData? get metaData => throw _privateConstructorUsedError;
bool get isFullContentLoaded => throw _privateConstructorUsedError;
String? get fullContent => throw _privateConstructorUsedError;
FieldSuggestions? get suggestions => throw _privateConstructorUsedError;
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DocumentDetailsStateCopyWith<DocumentDetailsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentDetailsStateCopyWith<$Res> {
factory $DocumentDetailsStateCopyWith(DocumentDetailsState value,
$Res Function(DocumentDetailsState) then) =
_$DocumentDetailsStateCopyWithImpl<$Res, DocumentDetailsState>;
@useResult
$Res call(
{DocumentModel document,
DocumentMetaData? metaData,
bool isFullContentLoaded,
String? fullContent,
FieldSuggestions? suggestions,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$DocumentDetailsStateCopyWithImpl<$Res,
$Val extends DocumentDetailsState>
implements $DocumentDetailsStateCopyWith<$Res> {
_$DocumentDetailsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? metaData = freezed,
Object? isFullContentLoaded = null,
Object? fullContent = freezed,
Object? suggestions = freezed,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
metaData: freezed == metaData
? _value.metaData
: metaData // ignore: cast_nullable_to_non_nullable
as DocumentMetaData?,
isFullContentLoaded: null == isFullContentLoaded
? _value.isFullContentLoaded
: isFullContentLoaded // ignore: cast_nullable_to_non_nullable
as bool,
fullContent: freezed == fullContent
? _value.fullContent
: fullContent // ignore: cast_nullable_to_non_nullable
as String?,
suggestions: freezed == suggestions
? _value.suggestions
: suggestions // ignore: cast_nullable_to_non_nullable
as FieldSuggestions?,
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_DocumentDetailsStateCopyWith<$Res>
implements $DocumentDetailsStateCopyWith<$Res> {
factory _$$_DocumentDetailsStateCopyWith(_$_DocumentDetailsState value,
$Res Function(_$_DocumentDetailsState) then) =
__$$_DocumentDetailsStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{DocumentModel document,
DocumentMetaData? metaData,
bool isFullContentLoaded,
String? fullContent,
FieldSuggestions? suggestions,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_DocumentDetailsStateCopyWithImpl<$Res>
extends _$DocumentDetailsStateCopyWithImpl<$Res, _$_DocumentDetailsState>
implements _$$_DocumentDetailsStateCopyWith<$Res> {
__$$_DocumentDetailsStateCopyWithImpl(_$_DocumentDetailsState _value,
$Res Function(_$_DocumentDetailsState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? metaData = freezed,
Object? isFullContentLoaded = null,
Object? fullContent = freezed,
Object? suggestions = freezed,
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_DocumentDetailsState(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
metaData: freezed == metaData
? _value.metaData
: metaData // ignore: cast_nullable_to_non_nullable
as DocumentMetaData?,
isFullContentLoaded: null == isFullContentLoaded
? _value.isFullContentLoaded
: isFullContentLoaded // ignore: cast_nullable_to_non_nullable
as bool,
fullContent: freezed == fullContent
? _value.fullContent
: fullContent // ignore: cast_nullable_to_non_nullable
as String?,
suggestions: freezed == suggestions
? _value.suggestions
: suggestions // ignore: cast_nullable_to_non_nullable
as FieldSuggestions?,
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_DocumentDetailsState implements _DocumentDetailsState {
const _$_DocumentDetailsState(
{required this.document,
this.metaData,
this.isFullContentLoaded = false,
this.fullContent,
this.suggestions,
final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
@override
final DocumentModel document;
@override
final DocumentMetaData? metaData;
@override
@JsonKey()
final bool isFullContentLoaded;
@override
final String? fullContent;
@override
final FieldSuggestions? suggestions;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'DocumentDetailsState(document: $document, metaData: $metaData, isFullContentLoaded: $isFullContentLoaded, fullContent: $fullContent, suggestions: $suggestions, correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_DocumentDetailsState &&
(identical(other.document, document) ||
other.document == document) &&
(identical(other.metaData, metaData) ||
other.metaData == metaData) &&
(identical(other.isFullContentLoaded, isFullContentLoaded) ||
other.isFullContentLoaded == isFullContentLoaded) &&
(identical(other.fullContent, fullContent) ||
other.fullContent == fullContent) &&
(identical(other.suggestions, suggestions) ||
other.suggestions == suggestions) &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
document,
metaData,
isFullContentLoaded,
fullContent,
suggestions,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_DocumentDetailsStateCopyWith<_$_DocumentDetailsState> get copyWith =>
__$$_DocumentDetailsStateCopyWithImpl<_$_DocumentDetailsState>(
this, _$identity);
}
abstract class _DocumentDetailsState implements DocumentDetailsState {
const factory _DocumentDetailsState(
{required final DocumentModel document,
final DocumentMetaData? metaData,
final bool isFullContentLoaded,
final String? fullContent,
final FieldSuggestions? suggestions,
final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_DocumentDetailsState;
@override
DocumentModel get document;
@override
DocumentMetaData? get metaData;
@override
bool get isFullContentLoaded;
@override
String? get fullContent;
@override
FieldSuggestions? get suggestions;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_DocumentDetailsStateCopyWith<_$_DocumentDetailsState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,42 +1,16 @@
part of 'document_details_cubit.dart';
class DocumentDetailsState with EquatableMixin {
final DocumentModel document;
final DocumentMetaData? metaData;
final bool isFullContentLoaded;
final String? fullContent;
final FieldSuggestions suggestions;
const DocumentDetailsState({
required this.document,
this.metaData,
this.suggestions = const FieldSuggestions(),
this.isFullContentLoaded = false,
this.fullContent,
});
@override
List<Object?> get props => [
document,
suggestions,
isFullContentLoaded,
fullContent,
metaData,
];
DocumentDetailsState copyWith({
DocumentModel? document,
FieldSuggestions? suggestions,
bool? isFullContentLoaded,
String? fullContent,
@freezed
class DocumentDetailsState with _$DocumentDetailsState {
const factory DocumentDetailsState({
required DocumentModel document,
DocumentMetaData? metaData,
}) {
return DocumentDetailsState(
document: document ?? this.document,
suggestions: suggestions ?? this.suggestions,
isFullContentLoaded: isFullContentLoaded ?? this.isFullContentLoaded,
fullContent: fullContent ?? this.fullContent,
metaData: metaData ?? this.metaData,
);
}
@Default(false) bool isFullContentLoaded,
String? fullContent,
FieldSuggestions? suggestions,
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,
@Default({}) Map<int, StoragePath> storagePaths,
}) = _DocumentDetailsState;
}

View File

@@ -186,6 +186,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
document: state.document,
itemSpacing: _itemSpacing,
queryString: widget.titleAndContentQueryString,
),
DocumentContentWidget(
isFullContentLoaded: state.isFullContentLoaded,

View File

@@ -11,6 +11,10 @@ import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class DocumentOverviewWidget extends StatelessWidget {
final DocumentModel document;
final Map<int, Correspondent> availableCorrespondents;
final Map<int, DocumentType> availableDocumentTypes;
final Map<int, Tag> availableTags;
final Map<int, StoragePath> availableStoragePaths;
final String? queryString;
final double itemSpacing;
const DocumentOverviewWidget({
@@ -18,6 +22,10 @@ class DocumentOverviewWidget extends StatelessWidget {
required this.document,
this.queryString,
required this.itemSpacing,
required this.availableCorrespondents,
required this.availableDocumentTypes,
required this.availableTags,
required this.availableStoragePaths,
});
@override
@@ -47,7 +55,7 @@ class DocumentOverviewWidget extends StatelessWidget {
label: S.of(context)!.documentType,
content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.documentType,
label: availableDocumentTypes[document.documentType],
),
).paddedOnly(bottom: itemSpacing),
),
@@ -57,7 +65,7 @@ class DocumentOverviewWidget extends StatelessWidget {
label: S.of(context)!.correspondent,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
id: document.correspondent,
label: availableCorrespondents[document.correspondent],
),
).paddedOnly(bottom: itemSpacing),
),
@@ -65,8 +73,8 @@ class DocumentOverviewWidget extends StatelessWidget {
visible: document.storagePath != null,
child: DetailsItem(
label: S.of(context)!.storagePath,
content: StoragePathWidget(
pathId: document.storagePath,
content: LabelText<StoragePath>(
label: availableStoragePaths[document.storagePath],
),
).paddedOnly(bottom: itemSpacing),
),
@@ -78,7 +86,7 @@ class DocumentOverviewWidget extends StatelessWidget {
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: false,
tagIds: document.tags,
tags: document.tags.map((e) => availableTags[e]!).toList(),
),
),
).paddedOnly(bottom: itemSpacing),

View File

@@ -1,66 +1,41 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
part 'document_edit_state.dart';
part 'document_edit_cubit.freezed.dart';
class DocumentEditCubit extends Cubit<DocumentEditState> {
final DocumentModel _initialDocument;
final PaperlessDocumentsApi _docsApi;
final DocumentChangedNotifier _notifier;
final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType> _documentTypeRepository;
final LabelRepository<StoragePath> _storagePathRepository;
final LabelRepository<Tag> _tagRepository;
final LabelRepository _labelRepository;
final List<StreamSubscription> _subscriptions = [];
DocumentEditCubit(
DocumentModel document, {
required PaperlessDocumentsApi documentsApi,
required LabelRepository<Correspondent> correspondentRepository,
required LabelRepository<DocumentType> documentTypeRepository,
required LabelRepository<StoragePath> storagePathRepository,
required LabelRepository<Tag> tagRepository,
required DocumentChangedNotifier notifier,
this._labelRepository,
this._docsApi,
this._notifier, {
required DocumentModel document,
}) : _initialDocument = document,
_docsApi = documentsApi,
_correspondentRepository = correspondentRepository,
_documentTypeRepository = documentTypeRepository,
_storagePathRepository = storagePathRepository,
_tagRepository = tagRepository,
_notifier = notifier,
super(
DocumentEditState(
document: document,
correspondents: correspondentRepository.current?.values ?? {},
documentTypes: documentTypeRepository.current?.values ?? {},
storagePaths: storagePathRepository.current?.values ?? {},
tags: tagRepository.current?.values ?? {},
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
),
) {
_notifier.subscribe(this, onUpdated: replace);
_subscriptions.add(
_correspondentRepository.values
.listen((v) => emit(state.copyWith(correspondents: v?.values))),
);
_subscriptions.add(
_documentTypeRepository.values
.listen((v) => emit(state.copyWith(documentTypes: v?.values))),
);
_subscriptions.add(
_storagePathRepository.values
.listen((v) => emit(state.copyWith(storagePaths: v?.values))),
);
_subscriptions.add(
_tagRepository.values.listen(
(v) => emit(state.copyWith(tags: v?.values)),
),
_labelRepository.subscribe(
this,
onStateChanged: (labels) => emit(state.copyWith()),
);
}
@@ -70,20 +45,20 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
// Reload changed labels (documentCount property changes with removal/add)
if (document.documentType != _initialDocument.documentType) {
_documentTypeRepository
.find((document.documentType ?? _initialDocument.documentType)!);
_labelRepository.findDocumentType(
(document.documentType ?? _initialDocument.documentType)!);
}
if (document.correspondent != _initialDocument.correspondent) {
_correspondentRepository
.find((document.correspondent ?? _initialDocument.correspondent)!);
_labelRepository.findCorrespondent(
(document.correspondent ?? _initialDocument.correspondent)!);
}
if (document.storagePath != _initialDocument.storagePath) {
_storagePathRepository
.find((document.storagePath ?? _initialDocument.storagePath)!);
_labelRepository.findStoragePath(
(document.storagePath ?? _initialDocument.storagePath)!);
}
if (!const DeepCollectionEquality.unordered()
.equals(document.tags, _initialDocument.tags)) {
_tagRepository.findAll(document.tags);
_labelRepository.findAllTags(document.tags);
}
}

View File

@@ -0,0 +1,256 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'document_edit_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$DocumentEditState {
DocumentModel get document => throw _privateConstructorUsedError;
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$DocumentEditStateCopyWith<DocumentEditState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DocumentEditStateCopyWith<$Res> {
factory $DocumentEditStateCopyWith(
DocumentEditState value, $Res Function(DocumentEditState) then) =
_$DocumentEditStateCopyWithImpl<$Res, DocumentEditState>;
@useResult
$Res call(
{DocumentModel document,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, StoragePath> storagePaths,
Map<int, Tag> tags});
}
/// @nodoc
class _$DocumentEditStateCopyWithImpl<$Res, $Val extends DocumentEditState>
implements $DocumentEditStateCopyWith<$Res> {
_$DocumentEditStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? storagePaths = null,
Object? tags = null,
}) {
return _then(_value.copyWith(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_DocumentEditStateCopyWith<$Res>
implements $DocumentEditStateCopyWith<$Res> {
factory _$$_DocumentEditStateCopyWith(_$_DocumentEditState value,
$Res Function(_$_DocumentEditState) then) =
__$$_DocumentEditStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{DocumentModel document,
Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, StoragePath> storagePaths,
Map<int, Tag> tags});
}
/// @nodoc
class __$$_DocumentEditStateCopyWithImpl<$Res>
extends _$DocumentEditStateCopyWithImpl<$Res, _$_DocumentEditState>
implements _$$_DocumentEditStateCopyWith<$Res> {
__$$_DocumentEditStateCopyWithImpl(
_$_DocumentEditState _value, $Res Function(_$_DocumentEditState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? document = null,
Object? correspondents = null,
Object? documentTypes = null,
Object? storagePaths = null,
Object? tags = null,
}) {
return _then(_$_DocumentEditState(
document: null == document
? _value.document
: document // ignore: cast_nullable_to_non_nullable
as DocumentModel,
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
));
}
}
/// @nodoc
class _$_DocumentEditState implements _DocumentEditState {
const _$_DocumentEditState(
{required this.document,
required final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes,
required final Map<int, StoragePath> storagePaths,
required final Map<int, Tag> tags})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_storagePaths = storagePaths,
_tags = tags;
@override
final DocumentModel document;
final Map<int, Correspondent> _correspondents;
@override
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, StoragePath> _storagePaths;
@override
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
final Map<int, Tag> _tags;
@override
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
@override
String toString() {
return 'DocumentEditState(document: $document, correspondents: $correspondents, documentTypes: $documentTypes, storagePaths: $storagePaths, tags: $tags)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_DocumentEditState &&
(identical(other.document, document) ||
other.document == document) &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths) &&
const DeepCollectionEquality().equals(other._tags, _tags));
}
@override
int get hashCode => Object.hash(
runtimeType,
document,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_storagePaths),
const DeepCollectionEquality().hash(_tags));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_DocumentEditStateCopyWith<_$_DocumentEditState> get copyWith =>
__$$_DocumentEditStateCopyWithImpl<_$_DocumentEditState>(
this, _$identity);
}
abstract class _DocumentEditState implements DocumentEditState {
const factory _DocumentEditState(
{required final DocumentModel document,
required final Map<int, Correspondent> correspondents,
required final Map<int, DocumentType> documentTypes,
required final Map<int, StoragePath> storagePaths,
required final Map<int, Tag> tags}) = _$_DocumentEditState;
@override
DocumentModel get document;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, StoragePath> get storagePaths;
@override
Map<int, Tag> get tags;
@override
@JsonKey(ignore: true)
_$$_DocumentEditStateCopyWith<_$_DocumentEditState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,43 +1,12 @@
part of 'document_edit_cubit.dart';
class DocumentEditState extends Equatable {
final DocumentModel document;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, StoragePath> storagePaths;
final Map<int, Tag> tags;
const DocumentEditState({
required this.correspondents,
required this.documentTypes,
required this.storagePaths,
required this.tags,
required this.document,
});
@override
List<Object> get props => [
correspondents,
documentTypes,
storagePaths,
tags,
document,
];
DocumentEditState copyWith({
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, StoragePath>? storagePaths,
Map<int, Tag>? tags,
DocumentModel? document,
}) {
return DocumentEditState(
document: document ?? this.document,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
storagePaths: storagePaths ?? this.storagePaths,
tags: tags ?? this.tags,
);
}
@freezed
class DocumentEditState with _$DocumentEditState {
const factory DocumentEditState({
required DocumentModel document,
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, StoragePath> storagePaths,
required Map<int, Tag> tags,
}) = _DocumentEditState;
}

View File

@@ -194,8 +194,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
LabelFormField<StoragePath>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read<LabelRepository<StoragePath>>(),
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(initalName: initialValue),
),
textFieldLabel: S.of(context)!.storagePath,

View File

@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -19,10 +20,18 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
@override
final PaperlessDocumentsApi api;
final LabelRepository _labelRepository;
@override
final DocumentChangedNotifier notifier;
DocumentsCubit(this.api, this.notifier) : super(const DocumentsState()) {
DocumentsCubit(this.api, this.notifier, this._labelRepository)
: super(DocumentsState(
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
)) {
notifier.subscribe(
this,
onUpdated: (document) {
@@ -45,6 +54,17 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
);
},
);
_labelRepository.subscribe(
this,
onStateChanged: (labels) => emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
),
),
);
}
Future<void> bulkDelete(List<DocumentModel> documents) async {
@@ -101,6 +121,7 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
@override
Future<void> close() {
notifier.unsubscribe(this);
_labelRepository.unsubscribe(this);
return super.close();
}

View File

@@ -1,10 +1,15 @@
part of 'documents_cubit.dart';
@JsonSerializable()
@JsonSerializable(ignoreUnannotated: true)
class DocumentsState extends DocumentPagingState {
@JsonKey(includeFromJson: false, includeToJson: false)
final List<DocumentModel> selection;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
@JsonKey()
final ViewType viewType;
const DocumentsState({
@@ -14,6 +19,10 @@ class DocumentsState extends DocumentPagingState {
super.filter = const DocumentFilter(),
super.hasLoaded = false,
super.isLoading = false,
this.correspondents = const {},
this.documentTypes = const {},
this.tags = const {},
this.storagePaths = const {},
});
List<int> get selectedIds => selection.map((e) => e.id).toList();
@@ -25,6 +34,10 @@ class DocumentsState extends DocumentPagingState {
DocumentFilter? filter,
List<DocumentModel>? selection,
ViewType? viewType,
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, Tag>? tags,
Map<int, StoragePath>? storagePaths,
}) {
return DocumentsState(
hasLoaded: hasLoaded ?? this.hasLoaded,
@@ -33,6 +46,10 @@ class DocumentsState extends DocumentPagingState {
filter: filter ?? this.filter,
selection: selection ?? this.selection,
viewType: viewType ?? this.viewType,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
tags: tags ?? this.tags,
storagePaths: storagePaths ?? this.storagePaths,
);
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
@@ -25,7 +24,13 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
bool get showLoadingPlaceholder => (!hasLoaded && isLoading);
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, Tag> tags;
final Map<int, StoragePath> storagePaths;
bool get showLoadingPlaceholder => !hasLoaded && isLoading;
const AdaptiveDocumentsView({
super.key,
this.selectedDocumentIds = const [],
@@ -42,6 +47,10 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
required this.isLoading,
required this.hasLoaded,
this.enableHeroAnimation = true,
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
});
AdaptiveDocumentsView.fromPagedState(
@@ -58,6 +67,10 @@ abstract class AdaptiveDocumentsView extends StatelessWidget {
required this.hasInternetConnection,
this.viewType = ViewType.list,
this.selectedDocumentIds = const [],
required this.correspondents,
required this.documentTypes,
required this.tags,
required this.storagePaths,
}) : documents = state.documents,
isLoading = state.isLoading,
hasLoaded = state.hasLoaded;
@@ -80,6 +93,10 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
super.enableHeroAnimation,
required super.isLoading,
required super.hasLoaded,
required super.correspondents,
required super.documentTypes,
required super.tags,
required super.storagePaths,
});
@override
@@ -96,27 +113,29 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
Widget _buildListView() {
if (showLoadingPlaceholder) {
return DocumentsListLoadingWidget.sliver();
return const DocumentsListLoadingWidget.sliver();
}
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: documents.length,
(context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
),
return DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
),
@@ -133,21 +152,23 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
childCount: documents.length,
(context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentDetailedItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
highlights: document.searchHit?.highlights,
),
return DocumentDetailedItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
highlights: document.searchHit?.highlights,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
),
@@ -180,6 +201,10 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);
@@ -205,6 +230,10 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
super.selectedDocumentIds,
super.viewType,
super.enableHeroAnimation = true,
required super.correspondents,
required super.documentTypes,
required super.tags,
required super.storagePaths,
});
@override
@@ -231,20 +260,22 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
itemCount: documents.length,
itemBuilder: (context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
),
return DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);
@@ -252,7 +283,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
Widget _buildFullView() {
if (showLoadingPlaceholder) {
return DocumentsListLoadingWidget();
return const DocumentsListLoadingWidget();
}
return ListView.builder(
@@ -263,20 +294,22 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
itemCount: documents.length,
itemBuilder: (context, index) {
final document = documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentDetailedItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
),
return DocumentDetailedItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: selectedDocumentIds.contains(document.id),
onSelected: onSelected,
isSelectionActive: selectedDocumentIds.isNotEmpty,
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);
@@ -284,7 +317,7 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
Widget _buildGridView() {
if (showLoadingPlaceholder) {
return DocumentGridLoadingWidget();
return const DocumentGridLoadingWidget();
}
return GridView.builder(
padding: EdgeInsets.zero,
@@ -311,6 +344,10 @@ class DefaultAdaptiveDocumentsView extends AdaptiveDocumentsView {
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
enableHeroAnimation: enableHeroAnimation,
correspondents: correspondents,
documentTypes: documentTypes,
storagePaths: storagePaths,
tags: tags,
);
},
);

View File

@@ -1,8 +1,10 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
@@ -26,6 +28,10 @@ class DocumentDetailedItem extends DocumentItem {
super.onStoragePathSelected,
super.onTagSelected,
super.onTap,
required super.tags,
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
});
@override
@@ -113,7 +119,7 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondentId: document.correspondent,
correspondent: context.read<DocumentsCubit>().correspondent,
),
],
).paddedLTRB(8, 0, 8, 4),

View File

@@ -21,6 +21,10 @@ class DocumentGridItem extends DocumentItem {
super.onTagSelected,
super.onTap,
required super.enableHeroAnimation,
required super.tags,
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
});
@override
@@ -54,10 +58,10 @@ class DocumentGridItem extends DocumentItem {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CorrespondentWidget(
correspondentId: document.correspondent,
correspondent: correspondents[document.correspondent],
),
DocumentTypeWidget(
documentTypeId: document.documentType,
documentType: documentTypes[document.documentType],
),
Text(
document.title,
@@ -67,7 +71,7 @@ class DocumentGridItem extends DocumentItem {
),
const Spacer(),
TagsWidget(
tagIds: document.tags,
tags: document.tags.map((e) => tags[e]!).toList(),
isMultiLine: false,
onTagSelected: onTagSelected,
),

View File

@@ -10,6 +10,11 @@ abstract class DocumentItem extends StatelessWidget {
final bool isLabelClickable;
final bool enableHeroAnimation;
final Map<int, Tag> tags;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, StoragePath> storagePaths;
final void Function(int tagId)? onTagSelected;
final void Function(int? correspondentId)? onCorrespondentSelected;
final void Function(int? documentTypeId)? onDocumentTypeSelected;
@@ -28,5 +33,9 @@ abstract class DocumentItem extends StatelessWidget {
this.onDocumentTypeSelected,
this.onStoragePathSelected,
required this.enableHeroAnimation,
required this.tags,
required this.correspondents,
required this.documentTypes,
required this.storagePaths,
});
}

View File

@@ -5,7 +5,6 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/document_type_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
@@ -25,113 +24,109 @@ class DocumentListItem extends DocumentItem {
super.onTagSelected,
super.onTap,
super.enableHeroAnimation = true,
required super.tags,
required super.correspondents,
required super.documentTypes,
required super.storagePaths,
});
@override
Widget build(BuildContext context) {
return DocumentTypeBlocProvider(
child: Material(
child: ListTile(
dense: true,
selected: isSelected,
onTap: () => _onTap(),
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
onLongPress: () => onSelected?.call(document),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
children: [
AbsorbPointer(
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondentId: document.correspondent,
onSelected: onCorrespondentSelected,
),
return Material(
child: ListTile(
dense: true,
selected: isSelected,
onTap: () => _onTap(),
selectedTileColor: Theme.of(context).colorScheme.inversePrimary,
onLongPress: () => onSelected?.call(document),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Row(
children: [
AbsorbPointer(
absorbing: isSelectionActive,
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondent: correspondents[document.correspondent],
onSelected: onCorrespondentSelected,
),
],
),
Text(
document.title,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
AbsorbPointer(
absorbing: isSelectionActive,
child: TagsWidget(
isClickable: isLabelClickable,
tagIds: document.tags,
isMultiLine: false,
onTagSelected: (id) => onTagSelected?.call(id),
),
)
],
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: BlocBuilder<LabelCubit<DocumentType>,
LabelState<DocumentType>>(
builder: (context, docTypes) {
return RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
text: DateFormat.yMMMd().format(document.created),
style: Theme.of(context)
.textTheme
.labelSmall
?.apply(color: Colors.grey),
children: document.documentType != null
? [
const TextSpan(text: '\u30FB'),
TextSpan(
text: docTypes
.labels[document.documentType]?.name,
),
]
: null,
),
);
},
)
// Row(
// children: [
// Text(
// DateFormat.yMMMd().format(document.created),
// style: Theme.of(context)
// .textTheme
// .bodySmall
// ?.apply(color: Colors.grey),
// ),
// if (document.documentType != null) ...[
// Text("\u30FB"),
// DocumentTypeWidget(
// documentTypeId: document.documentType,
// textStyle: Theme.of(context).textTheme.bodySmall?.apply(
// color: Colors.grey,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// ],
// ],
// ),
),
isThreeLine: document.tags.isNotEmpty,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: GestureDetector(
child: DocumentPreview(
document: document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: enableHeroAnimation,
],
),
Text(
document.title,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
AbsorbPointer(
absorbing: isSelectionActive,
child: TagsWidget(
isClickable: isLabelClickable,
tags: document.tags.map((e) => tags[e]!).toList(),
isMultiLine: false,
onTagSelected: (id) => onTagSelected?.call(id),
),
)
],
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: RichText(
maxLines: 1,
overflow: TextOverflow.ellipsis,
text: TextSpan(
text: DateFormat.yMMMd().format(document.created),
style: Theme.of(context)
.textTheme
.labelSmall
?.apply(color: Colors.grey),
children: document.documentType != null
? [
const TextSpan(text: '\u30FB'),
TextSpan(
text: documentTypes[document.documentType]?.name,
),
]
: null,
),
),
contentPadding: const EdgeInsets.all(8.0),
// Row(
// children: [
// Text(
// DateFormat.yMMMd().format(document.created),
// style: Theme.of(context)
// .textTheme
// .bodySmall
// ?.apply(color: Colors.grey),
// ),
// if (document.documentType != null) ...[
// Text("\u30FB"),
// DocumentTypeWidget(
// documentTypeId: document.documentType,
// textStyle: Theme.of(context).textTheme.bodySmall?.apply(
// color: Colors.grey,
// overflow: TextOverflow.ellipsis,
// ),
// ),
// ],
// ],
// ),
),
isThreeLine: document.tags.isNotEmpty,
leading: AspectRatio(
aspectRatio: _a4AspectRatio,
child: GestureDetector(
child: DocumentPreview(
document: document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: enableHeroAnimation,
),
),
),
contentPadding: const EdgeInsets.all(8.0),
),
);
}

View File

@@ -71,7 +71,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
.paddedOnly(left: 4, right: 4),
_buildBulkEditStoragePathChip(context)
.paddedOnly(left: 4, right: 4),
// _buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),
],
),
),
@@ -100,9 +100,6 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
builder: (_) {
return BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
@@ -112,8 +109,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
return BulkEditLabelBottomSheet<Correspondent>(
initialValue: initialValue,
title: "Bulk edit correspondent",
availableOptionsSelector: (state) =>
state.correspondentOptions,
availableOptionsSelector: (state) => state.correspondents,
formFieldLabel: S.of(context)!.correspondent,
formFieldPrefixIcon: const Icon(Icons.person_outline),
onSubmit: (selectedId) async {
@@ -152,9 +148,6 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
builder: (_) {
return BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
@@ -164,8 +157,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
return BulkEditLabelBottomSheet<DocumentType>(
initialValue: initialValue,
title: "Bulk edit document type",
availableOptionsSelector: (state) =>
state.documentTypeOptions,
availableOptionsSelector: (state) => state.documentTypes,
formFieldLabel: S.of(context)!.documentType,
formFieldPrefixIcon: const Icon(Icons.person_outline),
onSubmit: (selectedId) async {
@@ -204,9 +196,6 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
builder: (_) {
return BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
@@ -216,7 +205,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
return BulkEditLabelBottomSheet<StoragePath>(
initialValue: initialValue,
title: "Bulk edit storage path",
availableOptionsSelector: (state) => state.storagePathOptions,
availableOptionsSelector: (state) => state.storagePaths,
formFieldLabel: S.of(context)!.storagePath,
formFieldPrefixIcon: const Icon(Icons.folder_open_outlined),
onSubmit: (selectedId) async {
@@ -246,14 +235,11 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
topRight: Radius.circular(16),
),
),
isScrollControlled: false,
isScrollControlled: true,
context: context,
builder: (_) {
return BlocProvider(
create: (context) => DocumentBulkActionCubit(
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),

View File

@@ -43,14 +43,7 @@ class SortDocumentsButton extends StatelessWidget {
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LabelCubit<DocumentType>(
context.read<LabelRepository<DocumentType>>(),
),
),
BlocProvider(
create: (context) => LabelCubit<Correspondent>(
context.read<LabelRepository<Correspondent>>(),
),
create: (context) => LabelCubit(context.read()),
),
],
child: SortFieldSelectionBottomSheet(

View File

@@ -1,34 +1,44 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'edit_label_state.dart';
part 'edit_label_cubit.freezed.dart';
class EditLabelCubit<T extends Label> extends Cubit<EditLabelState<T>> {
final LabelRepository<T> _repository;
class EditLabelCubit extends Cubit<EditLabelState> with LabelCubitMixin {
@override
final LabelRepository labelRepository;
StreamSubscription? _subscription;
EditLabelCubit(LabelRepository<T> repository)
: _repository = repository,
super(EditLabelState<T>(labels: repository.current?.values ?? {})) {
_subscription = repository.values.listen(
(event) => emit(EditLabelState(labels: event?.values ?? {})),
EditLabelCubit(this.labelRepository) : super(const EditLabelState()) {
labelRepository.subscribe(
this,
onChanged: (labels) => state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
),
);
}
Future<T> create(T label) => _repository.create(label);
Future<T> update(T label) => _repository.update(label);
Future<void> delete(T label) => _repository.delete(label);
@override
Future<void> close() {
_subscription?.cancel();
labelRepository.unsubscribe(this);
return super.close();
}
@override
Map<int, Correspondent> get correspondents => state.correspondents;
@override
Map<int, DocumentType> get documentTypes => state.documentTypes;
@override
Map<int, StoragePath> get storagePaths => state.storagePaths;
@override
Map<int, Tag> get tags => state.tags;
}

View File

@@ -0,0 +1,237 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'edit_label_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$EditLabelState {
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$EditLabelStateCopyWith<EditLabelState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $EditLabelStateCopyWith<$Res> {
factory $EditLabelStateCopyWith(
EditLabelState value, $Res Function(EditLabelState) then) =
_$EditLabelStateCopyWithImpl<$Res, EditLabelState>;
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$EditLabelStateCopyWithImpl<$Res, $Val extends EditLabelState>
implements $EditLabelStateCopyWith<$Res> {
_$EditLabelStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_EditLabelStateCopyWith<$Res>
implements $EditLabelStateCopyWith<$Res> {
factory _$$_EditLabelStateCopyWith(
_$_EditLabelState value, $Res Function(_$_EditLabelState) then) =
__$$_EditLabelStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_EditLabelStateCopyWithImpl<$Res>
extends _$EditLabelStateCopyWithImpl<$Res, _$_EditLabelState>
implements _$$_EditLabelStateCopyWith<$Res> {
__$$_EditLabelStateCopyWithImpl(
_$_EditLabelState _value, $Res Function(_$_EditLabelState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_EditLabelState(
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_EditLabelState implements _EditLabelState {
const _$_EditLabelState(
{final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'EditLabelState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_EditLabelState &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_EditLabelStateCopyWith<_$_EditLabelState> get copyWith =>
__$$_EditLabelStateCopyWithImpl<_$_EditLabelState>(this, _$identity);
}
abstract class _EditLabelState implements EditLabelState {
const factory _EditLabelState(
{final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_EditLabelState;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_EditLabelStateCopyWith<_$_EditLabelState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,10 +1,11 @@
part of 'edit_label_cubit.dart';
class EditLabelState<T> extends Equatable {
final Map<int, T> labels;
const EditLabelState({this.labels = const {}});
@override
List<Object> get props => [labels];
@freezed
class EditLabelState with _$EditLabelState {
const factory EditLabelState({
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,
@Default({}) Map<int, StoragePath> storagePaths,
}) = _EditLabelState;
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -12,6 +11,7 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
final Widget pageTitle;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
final Future<T> Function(BuildContext context, T label) onSubmit;
const AddLabelPage({
super.key,
@@ -19,19 +19,21 @@ class AddLabelPage<T extends Label> extends StatelessWidget {
required this.pageTitle,
required this.fromJsonT,
this.additionalFields = const [],
required this.onSubmit,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
context.read<LabelRepository<T>>(),
context.read<LabelRepository>(),
),
child: AddLabelFormWidget(
pageTitle: pageTitle,
label: initialName != null ? fromJsonT({'name': initialName}) : null,
additionalFields: additionalFields,
fromJsonT: fromJsonT,
onSubmit: onSubmit,
),
);
}
@@ -41,6 +43,7 @@ class AddLabelFormWidget<T extends Label> extends StatelessWidget {
final T? label;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
final Future<T> Function(BuildContext context, T label) onSubmit;
final Widget pageTitle;
const AddLabelFormWidget({
@@ -49,6 +52,7 @@ class AddLabelFormWidget<T extends Label> extends StatelessWidget {
required this.fromJsonT,
required this.additionalFields,
required this.pageTitle,
required this.onSubmit,
});
@override
@@ -63,7 +67,7 @@ class AddLabelFormWidget<T extends Label> extends StatelessWidget {
submitButtonConfig: SubmitButtonConfig<T>(
icon: const Icon(Icons.add),
label: Text(S.of(context)!.create),
onSubmit: context.read<EditLabelCubit<T>>().create,
onSubmit: (label) => onSubmit(context, label),
),
additionalFields: additionalFields,
),

View File

@@ -1,40 +1,43 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/indexed_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/constants.dart';
class EditLabelPage<T extends Label> extends StatelessWidget {
final T label;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
final Future<T> Function(BuildContext context, T label) onSubmit;
final Future<void> Function(BuildContext context, T label) onDelete;
const EditLabelPage({
super.key,
required this.label,
required this.fromJsonT,
this.additionalFields = const [],
required this.onSubmit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit(
context.read<LabelRepository<T>>(),
context.read<LabelRepository>(),
),
child: EditLabelForm(
label: label,
additionalFields: additionalFields,
fromJsonT: fromJsonT,
onSubmit: onSubmit,
onDelete: onDelete,
),
);
}
@@ -44,12 +47,16 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
final T label;
final T Function(Map<String, dynamic> json) fromJsonT;
final List<Widget> additionalFields;
final Future<T> Function(BuildContext context, T label) onSubmit;
final Future<void> Function(BuildContext context, T label) onDelete;
const EditLabelForm({
super.key,
required this.label,
required this.fromJsonT,
required this.additionalFields,
required this.onSubmit,
required this.onDelete,
});
@override
@@ -70,7 +77,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
submitButtonConfig: SubmitButtonConfig<T>(
icon: const Icon(Icons.save),
label: Text(S.of(context)!.saveChanges),
onSubmit: context.read<EditLabelCubit<T>>().update,
onSubmit: (label) => onSubmit(context, label),
),
additionalFields: additionalFields,
),
@@ -107,7 +114,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
false;
if (shouldDelete) {
try {
context.read<EditLabelCubit<T>>().delete(label);
onDelete(context, label);
} on PaperlessServerException catch (error) {
showErrorMessage(context, error);
} catch (error, stackTrace) {
@@ -116,7 +123,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
Navigator.pop(context);
}
} else {
context.read<EditLabelCubit<T>>().delete(label);
onDelete(context, label);
Navigator.pop(context);
}
}

View File

@@ -1,8 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -14,13 +12,15 @@ class AddCorrespondentPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Correspondent>(
context.read<LabelRepository<Correspondent>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: AddLabelPage<Correspondent>(
pageTitle: Text(S.of(context)!.addCorrespondent),
fromJsonT: Correspondent.fromJson,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addCorrespondent(label),
),
);
}

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -17,13 +15,15 @@ class AddDocumentTypePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<DocumentType>(
context.read<LabelRepository<DocumentType>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: AddLabelPage<DocumentType>(
pageTitle: Text(S.of(context)!.addDocumentType),
fromJsonT: DocumentType.fromJson,
initialName: initialName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addDocumentType(label),
),
);
}

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
@@ -15,13 +13,15 @@ class AddStoragePathPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<StoragePath>(
context.read<LabelRepository<StoragePath>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: AddLabelPage<StoragePath>(
pageTitle: Text(S.of(context)!.addStoragePath),
fromJsonT: StoragePath.fromJson,
initialName: initalName,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label),
additionalFields: const [
StoragePathAutofillFormBuilderField(name: StoragePath.pathKey),
SizedBox(height: 120.0),

View File

@@ -4,8 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/add_label_page.dart';
@@ -18,13 +16,15 @@ class AddTagPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Tag>(
context.read<LabelRepository<Tag>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: AddLabelPage<Tag>(
pageTitle: Text(S.of(context)!.addTag),
fromJsonT: Tag.fromJson,
initialName: initialValue,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addTag(label),
additionalFields: [
FormBuilderColorPickerField(
name: Tag.colorKey,

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
@@ -13,12 +11,16 @@ class EditCorrespondentPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Correspondent>(
context.read<LabelRepository<Correspondent>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: EditLabelPage<Correspondent>(
label: correspondent,
fromJsonT: Correspondent.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
),
);
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
@@ -13,12 +12,16 @@ class EditDocumentTypePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<DocumentType>(
context.read<LabelRepository<DocumentType>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: EditLabelPage<DocumentType>(
label: documentType,
fromJsonT: DocumentType.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
),
);
}

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
import 'package:paperless_mobile/features/labels/storage_path/view/widgets/storage_path_autofill_form_builder_field.dart';
@@ -14,12 +12,16 @@ class EditStoragePathPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<StoragePath>(
context.read<LabelRepository<StoragePath>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: EditLabelPage<StoragePath>(
label: storagePath,
fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
additionalFields: [
StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey,

View File

@@ -2,8 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_color_picker.dart';
import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/edit_label_page.dart';
@@ -17,12 +15,16 @@ class EditTagPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => EditLabelCubit<Tag>(
context.read<LabelRepository<Tag>>(),
create: (context) => EditLabelCubit(
context.read(),
),
child: EditLabelPage<Tag>(
label: tag,
fromJsonT: Tag.fromJson,
onSubmit: (context, label) =>
context.read<EditLabelCubit>().addTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
additionalFields: [
FormBuilderColorPickerField(
initialValue: tag.color,

View File

@@ -31,9 +31,7 @@ import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/file_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:path/path.dart' as p;
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:responsive_builder/responsive_builder.dart';
@@ -60,8 +58,6 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
context.read(),
context.read(),
context.read(),
context.read(),
context.read(),
);
_listenToInboxChanges();
@@ -260,17 +256,8 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => LabelCubit<Correspondent>(context.read()),
),
BlocProvider(
create: (context) => LabelCubit<DocumentType>(context.read()),
),
BlocProvider(
create: (context) => LabelCubit<Tag>(context.read()),
),
BlocProvider(
create: (context) => LabelCubit<StoragePath>(context.read()),
),
create: (context) => LabelCubit(context.read()),
)
],
child: const LabelsPage(),
),
@@ -345,15 +332,13 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
void _initializeData(BuildContext context) {
try {
context.read<LabelRepository<Tag>>().findAll();
context.read<LabelRepository<Correspondent>>().findAll();
context.read<LabelRepository<DocumentType>>().findAll();
context.read<LabelRepository<StoragePath>>().findAll();
context.read<SavedViewRepository>().findAll();
context.read<PaperlessServerInformationCubit>().updateInformtion();
} on PaperlessServerException catch (error, stackTrace) {
Future.wait([
context.read<LabelRepository>().initialize(),
context.read<SavedViewRepository>().findAll(),
context.read<PaperlessServerInformationCubit>().updateInformtion(),
]).onError<PaperlessServerException>((error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
throw error;
});
}
}

View File

@@ -65,10 +65,7 @@ class VerifyIdentityPage extends StatelessWidget {
void _logout(BuildContext context) {
context.read<AuthenticationCubit>().logout();
context.read<LabelRepository<Tag>>().clear();
context.read<LabelRepository<Correspondent>>().clear();
context.read<LabelRepository<DocumentType>>().clear();
context.read<LabelRepository<StoragePath>>().clear();
context.read<LabelRepository>().clear();
context.read<SavedViewRepository>().clear();
HydratedBloc.storage.clear();
}

View File

@@ -5,6 +5,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
@@ -13,9 +14,7 @@ part 'inbox_state.dart';
class InboxCubit extends HydratedCubit<InboxState>
with DocumentPagingBlocMixin {
final LabelRepository<Tag> _tagsRepository;
final LabelRepository<Correspondent> _correspondentRepository;
final LabelRepository<DocumentType> _documentTypeRepository;
final LabelRepository _labelRepository;
final PaperlessDocumentsApi _documentsApi;
@@ -24,27 +23,15 @@ class InboxCubit extends HydratedCubit<InboxState>
final PaperlessServerStatsApi _statsApi;
final List<StreamSubscription> _subscriptions = [];
@override
PaperlessDocumentsApi get api => _documentsApi;
InboxCubit(
this._tagsRepository,
this._documentsApi,
this._correspondentRepository,
this._documentTypeRepository,
this._statsApi,
this._labelRepository,
this.notifier,
) : super(
InboxState(
availableCorrespondents:
_correspondentRepository.current?.values ?? {},
availableDocumentTypes:
_documentTypeRepository.current?.values ?? {},
availableTags: _tagsRepository.current?.values ?? {},
),
) {
) : super(InboxState(labels: _labelRepository.state)) {
notifier.subscribe(
this,
onDeleted: remove,
@@ -60,28 +47,11 @@ class InboxCubit extends HydratedCubit<InboxState>
}
},
);
_subscriptions.add(
_tagsRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(availableTags: event!.values));
}
}),
);
_subscriptions.add(
_correspondentRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(
availableCorrespondents: event!.values,
));
}
}),
);
_subscriptions.add(
_documentTypeRepository.values.listen((event) {
if (event?.hasLoaded ?? false) {
emit(state.copyWith(availableDocumentTypes: event!.values));
}
}),
_labelRepository.subscribe(
this,
onChanged: (labels) {
emit(state.copyWith(labels: labels));
},
);
refreshItemsInInboxCount(false);
@@ -105,7 +75,7 @@ class InboxCubit extends HydratedCubit<InboxState>
/// Fetches inbox tag ids and loads the inbox items (documents).
///
Future<void> loadInbox() async {
final inboxTags = await _tagsRepository.findAll().then(
final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
);
@@ -133,7 +103,7 @@ class InboxCubit extends HydratedCubit<InboxState>
///
Future<void> reloadInbox() async {
emit(state.copyWith(hasLoaded: false, isLoading: true));
final inboxTags = await _tagsRepository.findAll().then(
final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
);
@@ -239,9 +209,7 @@ class InboxCubit extends HydratedCubit<InboxState>
@override
Future<void> close() {
for (var sub in _subscriptions) {
sub.cancel();
}
_labelRepository.unsubscribe(this);
return super.close();
}
}

View File

@@ -4,11 +4,7 @@ part of 'inbox_cubit.dart';
class InboxState extends DocumentPagingState {
final Iterable<int> inboxTags;
final Map<int, Tag> availableTags;
final Map<int, DocumentType> availableDocumentTypes;
final Map<int, Correspondent> availableCorrespondents;
final LabelRepositoryState labels;
final int itemsInInboxCount;
@@ -22,10 +18,8 @@ class InboxState extends DocumentPagingState {
super.filter = const DocumentFilter(),
this.inboxTags = const [],
this.isHintAcknowledged = false,
this.availableTags = const {},
this.availableDocumentTypes = const {},
this.availableCorrespondents = const {},
this.itemsInInboxCount = 0,
this.labels = const LabelRepositoryState(),
});
@override
@@ -37,10 +31,8 @@ class InboxState extends DocumentPagingState {
inboxTags,
documents,
isHintAcknowledged,
availableTags,
availableDocumentTypes,
availableCorrespondents,
itemsInInboxCount,
labels,
];
InboxState copyWith({
@@ -50,9 +42,7 @@ class InboxState extends DocumentPagingState {
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
bool? isHintAcknowledged,
Map<int, Tag>? availableTags,
Map<int, Correspondent>? availableCorrespondents,
Map<int, DocumentType>? availableDocumentTypes,
LabelRepositoryState? labels,
Map<int, FieldSuggestions>? suggestions,
int? itemsInInboxCount,
}) {
@@ -62,11 +52,7 @@ class InboxState extends DocumentPagingState {
value: value ?? super.value,
inboxTags: inboxTags ?? this.inboxTags,
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
availableCorrespondents:
availableCorrespondents ?? this.availableCorrespondents,
availableDocumentTypes:
availableDocumentTypes ?? this.availableDocumentTypes,
availableTags: availableTags ?? this.availableTags,
labels: labels ?? this.labels,
filter: filter ?? super.filter,
itemsInInboxCount: itemsInInboxCount ?? this.itemsInInboxCount,
);

View File

@@ -32,60 +32,98 @@ class _InboxItemState extends State<InboxItem> {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: widget.document,
isLabelClickable: false,
return BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
Navigator.pushNamed(
context,
DocumentDetailsRoute.routeName,
arguments: DocumentDetailsRouteArguments(
document: widget.document,
isLabelClickable: false,
),
);
},
child: SizedBox(
height: 200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Row(
children: [
AspectRatio(
aspectRatio: InboxItem.a4AspectRatio,
child: DocumentPreview(
document: widget.document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: false,
),
).padded(),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle().paddedOnly(left: 8, right: 8, top: 8),
const Spacer(),
_buildTextWithLeadingIcon(
Icon(
Icons.person_outline,
size: Theme.of(context)
.textTheme
.bodyMedium
?.fontSize,
),
LabelText<Correspondent>(
label: state.labels.correspondents[
widget.document.correspondent],
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
),
).paddedSymmetrically(horizontal: 8),
_buildTextWithLeadingIcon(
Icon(
Icons.description_outlined,
size: Theme.of(context)
.textTheme
.bodyMedium
?.fontSize,
),
LabelText<DocumentType>(
label: state.labels.documentTypes[
widget.document.documentType],
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
),
).paddedSymmetrically(horizontal: 8),
const Spacer(),
TagsWidget(
tags: widget.document.tags
.map((e) => state.labels.tags[e]!)
.toList(),
isMultiLine: false,
isClickable: false,
showShortNames: true,
dense: true,
).paddedOnly(left: 8, bottom: 8),
],
),
),
],
),
),
SizedBox(
height: 56,
child: _buildActions(context),
),
],
).paddedOnly(left: 8, top: 8, bottom: 8),
),
);
},
child: SizedBox(
height: 200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Row(
children: [
AspectRatio(
aspectRatio: InboxItem.a4AspectRatio,
child: DocumentPreview(
document: widget.document,
fit: BoxFit.cover,
alignment: Alignment.topCenter,
enableHero: false,
),
).padded(),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle().paddedOnly(left: 8, right: 8, top: 8),
const Spacer(),
_buildCorrespondent(context)
.paddedSymmetrically(horizontal: 8),
_buildDocumentType(context)
.paddedSymmetrically(horizontal: 8),
const Spacer(),
_buildTags().paddedOnly(left: 8, bottom: 8),
],
),
),
],
),
),
SizedBox(
height: 56,
child: _buildActions(context),
),
],
).paddedOnly(left: 8, top: 8, bottom: 8),
),
);
}
@@ -211,44 +249,6 @@ class _InboxItemState extends State<InboxItem> {
);
}
TagsWidget _buildTags() {
return TagsWidget(
tagIds: widget.document.tags,
isMultiLine: false,
isClickable: false,
showShortNames: true,
dense: true,
);
}
Row _buildDocumentType(BuildContext context) {
return _buildTextWithLeadingIcon(
Icon(
Icons.description_outlined,
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
LabelText<DocumentType>(
id: widget.document.documentType,
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
),
);
}
Row _buildCorrespondent(BuildContext context) {
return _buildTextWithLeadingIcon(
Icon(
Icons.person_outline,
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
),
LabelText<Correspondent>(
id: widget.document.correspondent,
style: Theme.of(context).textTheme.bodyMedium,
placeholder: "-",
),
);
}
Text _buildTitle() {
return Text(
widget.document.title,

View File

@@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/correspondent_bloc_provider.dart';
class CorrespondentWidget extends StatelessWidget {
final int? correspondentId;
final Correspondent? correspondent;
final void Function(int? id)? onSelected;
final Color? textColor;
final bool isClickable;
@@ -13,7 +10,7 @@ class CorrespondentWidget extends StatelessWidget {
const CorrespondentWidget({
Key? key,
required this.correspondentId,
required this.correspondent,
this.textColor,
this.isClickable = true,
this.textStyle,
@@ -22,25 +19,18 @@ class CorrespondentWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CorrespondentBlocProvider(
child: AbsorbPointer(
absorbing: !isClickable,
child:
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return GestureDetector(
onTap: () => onSelected?.call(correspondentId!),
child: Text(
(state.getLabel(correspondentId)?.name) ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
);
},
return AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => onSelected?.call(correspondent?.id),
child: Text(
correspondent?.name ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
(textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
),
);

View File

@@ -1,68 +1,45 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
part 'label_state.dart';
part 'label_cubit.freezed.dart';
class LabelCubit<T extends Label> extends Cubit<LabelState<T>> {
final LabelRepository<T> _repository;
class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
@override
final LabelRepository labelRepository;
late StreamSubscription _subscription;
LabelCubit(LabelRepository<T> repository)
: _repository = repository,
super(LabelState(
isLoaded: repository.isInitialized,
labels: repository.current?.values ?? {},
)) {
_subscription = _repository.values.listen(
(event) {
if (event == null) {
emit(LabelState());
}
emit(
LabelState(isLoaded: event!.hasLoaded, labels: event.values ?? {}));
LabelCubit(this.labelRepository) : super(const LabelState()) {
labelRepository.subscribe(
this,
onChanged: (labels) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
storagePaths: labels.storagePaths,
tags: labels.tags,
));
},
);
}
///
/// Adds [item] to the current state. A new state is automatically pushed
/// due to the subscription to the repository, which updates the state on
/// operation.
///
Future<T> add(T item) async {
assert(item.id == null);
final addedItem = await _repository.create(item);
return addedItem;
}
Future<void> reload() {
return _repository.findAll();
}
Future<T> replace(T item) async {
assert(item.id != null);
final updatedItem = await _repository.update(item);
return updatedItem;
}
Future<void> remove(T item) async {
assert(item.id != null);
if (state.labels.containsKey(item.id)) {
await _repository.delete(item);
}
}
void reset() {
emit(LabelState(isLoaded: false, labels: {}));
@override
Future<void> close() {
labelRepository.unsubscribe(this);
return super.close();
}
@override
Future<void> close() {
_subscription.cancel();
return super.close();
}
Map<int, Correspondent> get correspondents => state.correspondents;
@override
Map<int, DocumentType> get documentTypes => state.documentTypes;
@override
Map<int, StoragePath> get storagePaths => state.storagePaths;
@override
Map<int, Tag> get tags => state.tags;
}

View File

@@ -0,0 +1,237 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'label_cubit.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$LabelState {
Map<int, Correspondent> get correspondents =>
throw _privateConstructorUsedError;
Map<int, DocumentType> get documentTypes =>
throw _privateConstructorUsedError;
Map<int, Tag> get tags => throw _privateConstructorUsedError;
Map<int, StoragePath> get storagePaths => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LabelStateCopyWith<LabelState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LabelStateCopyWith<$Res> {
factory $LabelStateCopyWith(
LabelState value, $Res Function(LabelState) then) =
_$LabelStateCopyWithImpl<$Res, LabelState>;
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class _$LabelStateCopyWithImpl<$Res, $Val extends LabelState>
implements $LabelStateCopyWith<$Res> {
_$LabelStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_value.copyWith(
correspondents: null == correspondents
? _value.correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value.documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value.storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
) as $Val);
}
}
/// @nodoc
abstract class _$$_LabelStateCopyWith<$Res>
implements $LabelStateCopyWith<$Res> {
factory _$$_LabelStateCopyWith(
_$_LabelState value, $Res Function(_$_LabelState) then) =
__$$_LabelStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<int, Correspondent> correspondents,
Map<int, DocumentType> documentTypes,
Map<int, Tag> tags,
Map<int, StoragePath> storagePaths});
}
/// @nodoc
class __$$_LabelStateCopyWithImpl<$Res>
extends _$LabelStateCopyWithImpl<$Res, _$_LabelState>
implements _$$_LabelStateCopyWith<$Res> {
__$$_LabelStateCopyWithImpl(
_$_LabelState _value, $Res Function(_$_LabelState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? correspondents = null,
Object? documentTypes = null,
Object? tags = null,
Object? storagePaths = null,
}) {
return _then(_$_LabelState(
correspondents: null == correspondents
? _value._correspondents
: correspondents // ignore: cast_nullable_to_non_nullable
as Map<int, Correspondent>,
documentTypes: null == documentTypes
? _value._documentTypes
: documentTypes // ignore: cast_nullable_to_non_nullable
as Map<int, DocumentType>,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as Map<int, Tag>,
storagePaths: null == storagePaths
? _value._storagePaths
: storagePaths // ignore: cast_nullable_to_non_nullable
as Map<int, StoragePath>,
));
}
}
/// @nodoc
class _$_LabelState implements _LabelState {
const _$_LabelState(
{final Map<int, Correspondent> correspondents = const {},
final Map<int, DocumentType> documentTypes = const {},
final Map<int, Tag> tags = const {},
final Map<int, StoragePath> storagePaths = const {}})
: _correspondents = correspondents,
_documentTypes = documentTypes,
_tags = tags,
_storagePaths = storagePaths;
final Map<int, Correspondent> _correspondents;
@override
@JsonKey()
Map<int, Correspondent> get correspondents {
if (_correspondents is EqualUnmodifiableMapView) return _correspondents;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_correspondents);
}
final Map<int, DocumentType> _documentTypes;
@override
@JsonKey()
Map<int, DocumentType> get documentTypes {
if (_documentTypes is EqualUnmodifiableMapView) return _documentTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_documentTypes);
}
final Map<int, Tag> _tags;
@override
@JsonKey()
Map<int, Tag> get tags {
if (_tags is EqualUnmodifiableMapView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_tags);
}
final Map<int, StoragePath> _storagePaths;
@override
@JsonKey()
Map<int, StoragePath> get storagePaths {
if (_storagePaths is EqualUnmodifiableMapView) return _storagePaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_storagePaths);
}
@override
String toString() {
return 'LabelState(correspondents: $correspondents, documentTypes: $documentTypes, tags: $tags, storagePaths: $storagePaths)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_LabelState &&
const DeepCollectionEquality()
.equals(other._correspondents, _correspondents) &&
const DeepCollectionEquality()
.equals(other._documentTypes, _documentTypes) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality()
.equals(other._storagePaths, _storagePaths));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_correspondents),
const DeepCollectionEquality().hash(_documentTypes),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_storagePaths));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_LabelStateCopyWith<_$_LabelState> get copyWith =>
__$$_LabelStateCopyWithImpl<_$_LabelState>(this, _$identity);
}
abstract class _LabelState implements LabelState {
const factory _LabelState(
{final Map<int, Correspondent> correspondents,
final Map<int, DocumentType> documentTypes,
final Map<int, Tag> tags,
final Map<int, StoragePath> storagePaths}) = _$_LabelState;
@override
Map<int, Correspondent> get correspondents;
@override
Map<int, DocumentType> get documentTypes;
@override
Map<int, Tag> get tags;
@override
Map<int, StoragePath> get storagePaths;
@override
@JsonKey(ignore: true)
_$$_LabelStateCopyWith<_$_LabelState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
mixin LabelCubitMixin<T> on BlocBase<T> {
LabelRepository get labelRepository;
Map<int, Correspondent> get correspondents;
Map<int, DocumentType> get documentTypes;
Map<int, Tag> get tags;
Map<int, StoragePath> get storagePaths;
Future<Correspondent> addCorrespondent(Correspondent item) async {
assert(item.id == null);
final addedItem = await labelRepository.createCorrespondent(item);
return addedItem;
}
Future<void> reloadCorrespondents() {
return labelRepository.findAllCorrespondents();
}
Future<Correspondent> replaceCorrespondent(Correspondent item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateCorrespondent(item);
return updatedItem;
}
Future<void> removeCorrespondent(Correspondent item) async {
assert(item.id != null);
if (correspondents.containsKey(item.id)) {
await labelRepository.deleteCorrespondent(item);
}
}
Future<DocumentType> addDocumentType(DocumentType item) async {
assert(item.id == null);
final addedItem = await labelRepository.createDocumentType(item);
return addedItem;
}
Future<void> reloadDocumentTypes() {
return labelRepository.findAllDocumentTypes();
}
Future<DocumentType> replaceDocumentType(DocumentType item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateDocumentType(item);
return updatedItem;
}
Future<void> removeDocumentType(DocumentType item) async {
assert(item.id != null);
if (documentTypes.containsKey(item.id)) {
await labelRepository.deleteDocumentType(item);
}
}
Future<StoragePath> addStoragePath(StoragePath item) async {
assert(item.id == null);
final addedItem = await labelRepository.createStoragePath(item);
return addedItem;
}
Future<void> reloadStoragePaths() {
return labelRepository.findAllStoragePaths();
}
Future<StoragePath> replaceStoragePath(StoragePath item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateStoragePath(item);
return updatedItem;
}
Future<void> removeStoragePath(StoragePath item) async {
assert(item.id != null);
if (storagePaths.containsKey(item.id)) {
await labelRepository.deleteStoragePath(item);
}
}
Future<Tag> addTag(Tag item) async {
assert(item.id == null);
final addedItem = await labelRepository.createTag(item);
return addedItem;
}
Future<void> reloadTags() {
return labelRepository.findAllTags();
}
Future<Tag> replaceTag(Tag item) async {
assert(item.id != null);
final updatedItem = await labelRepository.updateTag(item);
return updatedItem;
}
Future<void> removeTag(Tag item) async {
assert(item.id != null);
if (tags.containsKey(item.id)) {
await labelRepository.deleteTag(item);
}
}
}

View File

@@ -1,19 +1,11 @@
part of 'label_cubit.dart';
class LabelState<T extends Label> {
LabelState.initial() : this(isLoaded: false, labels: {});
final bool isLoaded;
final Map<int, T> labels;
LabelState({
this.isLoaded = false,
this.labels = const {},
});
T? getLabel(int? key) {
if (isLoaded) {
return labels[key];
}
return null;
}
@freezed
class LabelState with _$LabelState {
const factory LabelState({
@Default({}) Map<int, Correspondent> correspondents,
@Default({}) Map<int, DocumentType> documentTypes,
@Default({}) Map<int, Tag> tags,
@Default({}) Map<int, StoragePath> storagePaths,
}) = _LabelState;
}

View File

@@ -1,24 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class CorrespondentBlocProvider extends StatelessWidget {
final Widget child;
const CorrespondentBlocProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Correspondent>(
context.read<LabelRepository<Correspondent>>(),
),
child: child,
);
}
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class DocumentTypeBlocProvider extends StatelessWidget {
final Widget child;
const DocumentTypeBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<DocumentType>(
context.read<LabelRepository<DocumentType>>(),
),
child: child,
);
}
}

View File

@@ -1,43 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class LabelsBlocProvider extends StatelessWidget {
final Widget child;
const LabelsBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<LabelCubit<StoragePath>>(
create: (context) => LabelCubit<StoragePath>(
context.read<LabelRepository<StoragePath>>(),
),
),
BlocProvider<LabelCubit<Correspondent>>(
create: (context) => LabelCubit<Correspondent>(
context.read<LabelRepository<Correspondent>>(),
),
),
BlocProvider<LabelCubit<DocumentType>>(
create: (context) => LabelCubit<DocumentType>(
context.read<LabelRepository<DocumentType>>(),
),
),
BlocProvider<LabelCubit<Tag>>(
create: (context) => LabelCubit<Tag>(
context.read<LabelRepository<Tag>>(),
),
),
],
child: child,
);
}
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class StoragePathBlocProvider extends StatelessWidget {
final Widget child;
const StoragePathBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<StoragePath>(
context.read<LabelRepository<StoragePath>>(),
),
child: child,
);
}
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class TagBlocProvider extends StatelessWidget {
final Widget child;
const TagBlocProvider({super.key, required this.child});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<Tag>(
context.read<LabelRepository<Tag>>(),
),
child: child,
);
}
}

View File

@@ -1,17 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/document_type_bloc_provider.dart';
class DocumentTypeWidget extends StatelessWidget {
final int? documentTypeId;
final DocumentType? documentType;
final bool isClickable;
final TextStyle? textStyle;
final void Function(int? id)? onSelected;
const DocumentTypeWidget({
Key? key,
required this.documentTypeId,
required this.documentType,
this.isClickable = true,
this.textStyle,
this.onSelected,
@@ -19,23 +16,16 @@ class DocumentTypeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DocumentTypeBlocProvider(
child: AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => onSelected?.call(documentTypeId),
child:
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return Text(
state.labels[documentTypeId]?.toString() ?? "-",
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
overflow: TextOverflow.ellipsis,
maxLines: 1,
);
},
),
return AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => onSelected?.call(documentType?.id),
child: Text(
documentType?.toString() ?? "-",
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
);

View File

@@ -1,18 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/storage_path_bloc_provider.dart';
class StoragePathWidget extends StatelessWidget {
final int? pathId;
final StoragePath? storagePath;
final Color? textColor;
final bool isClickable;
final void Function(int? id)? onSelected;
const StoragePathWidget({
Key? key,
this.pathId,
this.storagePath,
this.textColor,
this.isClickable = true,
this.onSelected,
@@ -20,23 +17,17 @@ class StoragePathWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoragePathBlocProvider(
child: AbsorbPointer(
absorbing: !isClickable,
child: BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return GestureDetector(
onTap: () => onSelected?.call(pathId),
child: Text(
state.getLabel(pathId)?.name ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
return AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => onSelected?.call(storagePath?.id),
child: Text(
storagePath?.name ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
);
},
),
),
);

View File

@@ -6,7 +6,6 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
@@ -21,6 +20,8 @@ class TagFormField extends StatefulWidget {
final bool excludeAllowed;
final Map<int, Tag> selectableOptions;
final Widget? suggestions;
final String? labelText;
final String? hintText;
const TagFormField({
super.key,
@@ -32,6 +33,8 @@ class TagFormField extends StatefulWidget {
this.excludeAllowed = true,
required this.selectableOptions,
this.suggestions,
this.labelText,
this.hintText,
});
@override
@@ -94,8 +97,8 @@ class _TagFormFieldState extends State<TagFormField> {
Icons.label_outline,
),
suffixIcon: _buildSuffixIcon(context, field),
labelText: S.of(context)!.tags,
hintText: S.of(context)!.filterTags,
labelText: widget.labelText ?? S.of(context)!.tags,
hintText: widget.hintText ?? S.of(context)!.filterTags,
),
controller: _textEditingController,
),
@@ -240,8 +243,8 @@ class _TagFormFieldState extends State<TagFormField> {
void _onAddTag(BuildContext context, FormFieldState<TagsQuery> field) async {
final Tag? tag = await Navigator.of(context).push<Tag>(
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<Tag>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddTagPage(initialValue: _textEditingController.text),
),
),

View File

@@ -1,12 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/providers/tag_bloc_provider.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart';
class TagsWidget extends StatelessWidget {
final Iterable<int> tagIds;
final List<Tag> tags;
final bool isMultiLine;
final void Function(int tagId)? onTagSelected;
final bool isClickable;
@@ -15,7 +12,7 @@ class TagsWidget extends StatelessWidget {
const TagsWidget({
Key? key,
required this.tagIds,
required this.tags,
this.isMultiLine = true,
this.isClickable = true,
this.onTagSelected,
@@ -25,36 +22,33 @@ class TagsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TagBlocProvider(
child: BlocBuilder<LabelCubit<Tag>, LabelState<Tag>>(
builder: (context, state) {
final children = tagIds
.where((id) => state.labels.containsKey(id))
.map(
(id) => TagWidget(
tag: state.getLabel(id)!,
isClickable: isClickable,
onSelected: () => onTagSelected?.call(id),
showShortName: showShortNames,
dense: dense,
),
)
.toList();
if (isMultiLine) {
return Wrap(
runAlignment: WrapAlignment.start,
children: children,
runSpacing: 4,
spacing: 4,
);
} else {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: children),
);
}
},
),
return Builder(
builder: (context) {
final children = tags
.map(
(tag) => TagWidget(
tag: tag,
isClickable: isClickable,
onSelected: () => onTagSelected?.call(tag.id!),
showShortName: showShortNames,
dense: dense,
),
)
.toList();
if (isMultiLine) {
return Wrap(
runAlignment: WrapAlignment.start,
children: children,
runSpacing: 4,
spacing: 4,
);
} else {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: children),
);
}
},
);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_he
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/material/search/colored_tab_bar.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
@@ -17,6 +16,7 @@ import 'package:paperless_mobile/features/edit_label/view/impl/edit_document_typ
import 'package:paperless_mobile/features/edit_label/view/impl/edit_storage_path_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/edit_tag_page.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit_mixin.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_tab_view.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -141,12 +141,12 @@ class _LabelsPageState extends State<LabelsPage>
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () => [
context.read<LabelCubit<Correspondent>>(),
context.read<LabelCubit<DocumentType>>(),
context.read<LabelCubit<Tag>>(),
context.read<LabelCubit<StoragePath>>(),
context.read<LabelCubit>().reloadCorrespondents,
context.read<LabelCubit>().reloadDocumentTypes,
context.read<LabelCubit>().reloadTags,
context.read<LabelCubit>().reloadStoragePaths,
][_currentIndex]
.reload(),
.call(),
child: TabBarView(
controller: _tabController,
children: [
@@ -157,6 +157,10 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: context
.watch<LabelCubit>()
.state
.correspondents,
filterBuilder: (label) => DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id),
@@ -180,6 +184,10 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: context
.watch<LabelCubit>()
.state
.documentTypes,
filterBuilder: (label) => DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id),
@@ -203,6 +211,8 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Tag>(
labels:
context.watch<LabelCubit>().state.tags,
filterBuilder: (label) => DocumentFilter(
tags: IdsTagsQuery.fromIds([label.id!]),
pageSize: label.documentCount ?? 0,
@@ -234,6 +244,10 @@ class _LabelsPageState extends State<LabelsPage>
SliverOverlapInjector(handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: context
.watch<LabelCubit>()
.state
.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath:
@@ -267,8 +281,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<Correspondent>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: EditCorrespondentPage(correspondent: correspondent),
),
),
@@ -279,8 +293,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<DocumentType>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: EditDocumentTypePage(documentType: docType),
),
),
@@ -291,8 +305,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<Tag>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: EditTagPage(tag: tag),
),
),
@@ -303,8 +317,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<StoragePath>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: EditStoragePathPage(
storagePath: path,
),
@@ -317,8 +331,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<Correspondent>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: const AddCorrespondentPage(),
),
),
@@ -329,8 +343,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<DocumentType>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: const AddDocumentTypePage(),
),
),
@@ -341,8 +355,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<Tag>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: const AddTagPage(),
),
),
@@ -353,8 +367,8 @@ class _LabelsPageState extends State<LabelsPage>
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RepositoryProvider(
create: (context) => context.read<LabelRepository<StoragePath>>(),
builder: (_) => RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: const AddStoragePathPage(),
),
),

View File

@@ -51,6 +51,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
filter,
context.read(),
context.read(),
context.read(),
),
child: const LinkedDocumentsPage(),
),

View File

@@ -3,12 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
class LabelTabView<T extends Label> extends StatelessWidget {
final Map<int, T> labels;
final DocumentFilter Function(Label) filterBuilder;
final void Function(T) onEdit;
final void Function() onAddNew;
@@ -32,69 +32,61 @@ class LabelTabView<T extends Label> extends StatelessWidget {
required this.emptyStateDescription,
required this.onAddNew,
required this.emptyStateActionButtonLabel,
required this.labels,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<T>(
context.read(),
),
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
return BlocBuilder<LabelCubit<T>, LabelState<T>>(
builder: (context, state) {
if (!state.isLoaded && !connectivityState.isConnected) {
return const OfflineWidget();
}
final labels = state.labels.values.toList()..sort();
if (labels.isEmpty) {
return SliverFillRemaining(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
emptyStateDescription,
textAlign: TextAlign.center,
),
TextButton(
onPressed: onAddNew,
child: Text(emptyStateActionButtonLabel),
),
].padded(),
),
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
if (!connectivityState.isConnected) {
return const OfflineWidget();
}
final sortedLabels = labels.values.toList()..sort();
if (labels.isEmpty) {
return SliverFillRemaining(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
emptyStateDescription,
textAlign: TextAlign.center,
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final l = labels.elementAt(index);
return LabelItem<T>(
name: l.name,
content: contentBuilder?.call(l) ??
Text(
translateMatchingAlgorithmName(
context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false)
? ": ${l.match}"
: ""),
maxLines: 2,
),
onOpenEditPage: onEdit,
filterBuilder: filterBuilder,
leading: leadingBuilder?.call(l),
label: l,
);
},
childCount: labels.length,
),
TextButton(
onPressed: onAddNew,
child: Text(emptyStateActionButtonLabel),
),
].padded(),
),
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final l = sortedLabels.elementAt(index);
return LabelItem<T>(
name: l.name,
content: contentBuilder?.call(l) ??
Text(
translateMatchingAlgorithmName(
context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false)
? ": ${l.match}"
: ""),
maxLines: 2,
),
onOpenEditPage: onEdit,
filterBuilder: filterBuilder,
leading: leadingBuilder?.call(l),
label: l,
);
},
);
},
),
childCount: labels.length,
),
);
},
);
}
}

View File

@@ -1,37 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
class LabelText<T extends Label> extends StatelessWidget {
final int? id;
final T? label;
final String placeholder;
final TextStyle? style;
const LabelText({
super.key,
this.style,
this.id,
this.placeholder = "",
required this.label,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LabelCubit<T>(
context.read<LabelRepository<T>>(),
),
child: BlocBuilder<LabelCubit<T>, LabelState<T>>(
builder: (context, state) {
return Text(
state.labels[id]?.toString() ?? placeholder,
style: style,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
),
return Text(
label?.toString() ?? placeholder,
style: style,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
;
}

View File

@@ -2,6 +2,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@@ -17,12 +18,26 @@ class LinkedDocumentsCubit extends HydratedCubit<LinkedDocumentsState>
@override
final DocumentChangedNotifier notifier;
final LabelRepository _labelRepository;
LinkedDocumentsCubit(
DocumentFilter filter,
this.api,
this.notifier,
this._labelRepository,
) : super(const LinkedDocumentsState()) {
updateFilter(filter: filter);
_labelRepository.subscribe(
this,
onChanged: (labels) {
emit(state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
));
},
);
notifier.subscribe(
this,
onUpdated: replace,

View File

@@ -4,12 +4,22 @@ part of 'linked_documents_cubit.dart';
class LinkedDocumentsState extends DocumentPagingState {
@JsonKey()
final ViewType viewType;
final Map<int, Correspondent> correspondents;
final Map<int, DocumentType> documentTypes;
final Map<int, StoragePath> storagePaths;
final Map<int, Tag> tags;
const LinkedDocumentsState({
this.viewType = ViewType.list,
super.filter,
super.isLoading,
super.hasLoaded,
super.value,
this.correspondents = const {},
this.documentTypes = const {},
this.storagePaths = const {},
this.tags = const {},
});
LinkedDocumentsState copyWith({
@@ -18,6 +28,10 @@ class LinkedDocumentsState extends DocumentPagingState {
bool? hasLoaded,
List<PagedSearchResult<DocumentModel>>? value,
ViewType? viewType,
Map<int, Correspondent>? correspondents,
Map<int, DocumentType>? documentTypes,
Map<int, StoragePath>? storagePaths,
Map<int, Tag>? tags,
}) {
return LinkedDocumentsState(
filter: filter ?? this.filter,
@@ -25,6 +39,10 @@ class LinkedDocumentsState extends DocumentPagingState {
hasLoaded: hasLoaded ?? this.hasLoaded,
value: value ?? this.value,
viewType: viewType ?? this.viewType,
correspondents: correspondents ?? this.correspondents,
documentTypes: documentTypes ?? this.documentTypes,
storagePaths: storagePaths ?? this.storagePaths,
tags: tags ?? this.tags,
);
}

View File

@@ -61,6 +61,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
),
);
},
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
),
],
);

View File

@@ -2,42 +2,72 @@ import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
part 'saved_view_state.dart';
part 'saved_view_cubit.freezed.dart';
class SavedViewCubit extends Cubit<SavedViewState> {
final SavedViewRepository _repository;
StreamSubscription? _subscription;
final SavedViewRepository _savedViewRepository;
final LabelRepository _labelRepository;
SavedViewCubit(this._repository) : super(const SavedViewState()) {
_subscription = _repository.values.listen(
(savedViews) {
if (savedViews?.hasLoaded ?? false) {
emit(state.copyWith(value: savedViews?.values, hasLoaded: true));
} else {
emit(state.copyWith(hasLoaded: false));
}
SavedViewCubit(this._savedViewRepository, this._labelRepository)
: super(SavedViewState.initial(
correspondents: _labelRepository.state.correspondents,
documentTypes: _labelRepository.state.documentTypes,
storagePaths: _labelRepository.state.storagePaths,
tags: _labelRepository.state.tags,
)) {
_labelRepository.subscribe(
this,
onChanged: (labels) {
emit(
state.copyWith(
correspondents: labels.correspondents,
documentTypes: labels.documentTypes,
tags: labels.tags,
storagePaths: labels.storagePaths,
),
);
},
);
_savedViewRepository.subscribe(this, (views) {
emit(
state.maybeWhen(
loaded:
(savedViews, correspondents, documentTypes, tags, storagePaths) =>
(state as _SavedViewLoadedState).copyWith(
savedViews: views,
),
orElse: () => state,
),
);
});
}
Future<SavedView> add(SavedView view) async {
final savedView = await _repository.create(view);
emit(state.copyWith(value: {...state.value, savedView.id!: savedView}));
return savedView;
return _savedViewRepository.create(view);
}
Future<int> remove(SavedView view) {
return _repository.delete(view);
return _savedViewRepository.delete(view);
}
Future<void> initialize() async {
final views = await _repository.findAll();
final views = await _savedViewRepository.findAll();
final values = {for (var element in views) element.id!: element};
if (!isClosed) {
emit(SavedViewState(value: values, hasLoaded: true));
emit(SavedViewState.loaded(
savedViews: values,
correspondents: state.correspondents,
documentTypes: state.documentTypes,
storagePaths: state.storagePaths,
tags: state.tags,
));
}
}
@@ -45,7 +75,8 @@ class SavedViewCubit extends Cubit<SavedViewState> {
@override
Future<void> close() {
_subscription?.cancel();
_savedViewRepository.unsubscribe(this);
_labelRepository.unsubscribe(this);
return super.close();
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,33 @@
part of 'saved_view_cubit.dart';
class SavedViewState extends Equatable {
final bool hasLoaded;
final Map<int, SavedView> value;
@freezed
class SavedViewState with _$SavedViewState {
const factory SavedViewState.initial({
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _SavedViewIntialState;
const SavedViewState({
this.value = const {},
this.hasLoaded = false,
});
const factory SavedViewState.loading({
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _SavedViewLoadingState;
@override
List<Object?> get props => [
hasLoaded,
value,
];
const factory SavedViewState.loaded({
required Map<int, SavedView> savedViews,
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _SavedViewLoadedState;
SavedViewState copyWith({
Map<int, SavedView>? value,
int? selectedSavedViewId,
bool overwriteSelectedSavedViewId = false,
bool? hasLoaded,
}) {
return SavedViewState(
value: value ?? this.value,
hasLoaded: hasLoaded ?? this.hasLoaded,
);
}
const factory SavedViewState.error({
required Map<int, Correspondent> correspondents,
required Map<int, DocumentType> documentTypes,
required Map<int, Tag> tags,
required Map<int, StoragePath> storagePaths,
}) = _SavedViewErrorState;
}

View File

@@ -62,7 +62,7 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
],
),
),
Divider(),
const Divider(),
Text(
"Review filter",
style: Theme.of(context).textTheme.bodyLarge,
@@ -80,42 +80,6 @@ class _AddSavedViewPageState extends State<AddSavedViewPage> {
);
}
Padding _buildOld(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
FormBuilder(
key: _savedViewFormKey,
child: Expanded(
child: ListView(
children: [
FormBuilderTextField(
name: fkName,
validator: FormBuilderValidators.required(),
decoration: InputDecoration(
label: Text(S.of(context)!.name),
),
),
FormBuilderCheckbox(
name: fkShowOnDashboard,
initialValue: false,
title: Text(S.of(context)!.showOnDashboard),
),
FormBuilderCheckbox(
name: fkShowInSidebar,
initialValue: false,
title: Text(S.of(context)!.showInSidebar),
),
],
),
),
),
],
),
);
}
void _onCreate(BuildContext context) {
if (_savedViewFormKey.currentState?.saveAndValidate() ?? false) {
Navigator.pop(

View File

@@ -12,52 +12,70 @@ class SavedViewList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final savedViewCubit = context.read<SavedViewCubit>();
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivity) {
return BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
if (state.value.isEmpty) {
return SliverToBoxAdapter(
child: HintCard(
hintText:
S.of(context)!.createViewsToQuicklyFilterYourDocuments,
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final view = state.value.values.elementAt(index);
return ListTile(
enabled: connectivity.isConnected,
title: Text(view.name),
subtitle: Text(
S.of(context)!.nFiltersSet(view.filterRules.length),
return state.when(
initial: (correspondents, documentTypes, tags, storagePaths) =>
Container(),
loading: (correspondents, documentTypes, tags, storagePaths) =>
Center(
child: Text("Saved views loading..."),
),
loaded: (savedViews, correspondents, documentTypes, tags,
storagePaths) {
if (savedViews.isEmpty) {
return SliverToBoxAdapter(
child: HintCard(
hintText: S
.of(context)!
.createViewsToQuicklyFilterYourDocuments,
),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SavedViewDetailsCubit(
context.read(),
context.read(),
savedView: view,
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final view = savedViews.values.elementAt(index);
return ListTile(
enabled: connectivity.isConnected,
title: Text(view.name),
subtitle: Text(
S.of(context)!.nFiltersSet(view.filterRules.length),
),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SavedViewDetailsCubit(
context.read(),
context.read(),
savedView: view,
),
),
],
child: SavedViewDetailsPage(
onDelete:
context.read<SavedViewCubit>().remove,
),
),
],
child: SavedViewDetailsPage(
onDelete: savedViewCubit.remove,
),
),
),
);
},
);
},
);
},
childCount: state.value.length,
childCount: savedViews.length,
),
);
},
error: (correspondents, documentTypes, tags, storagePaths) =>
Center(
child: Text(
"An error occurred while trying to load the saved views.",
),
),
);
},

View File

@@ -82,10 +82,7 @@ class AccountSettingsDialog extends StatelessWidget {
try {
await context.read<AuthenticationCubit>().logout();
await context.read<ApplicationSettingsCubit>().clear();
await context.read<LabelRepository<Tag>>().clear();
await context.read<LabelRepository<Correspondent>>().clear();
await context.read<LabelRepository<DocumentType>>().clear();
await context.read<LabelRepository<StoragePath>>().clear();
await context.read<LabelRepository>().clear();
await context.read<SavedViewRepository>().clear();
await HydratedBloc.storage.clear();
} on PaperlessServerException catch (error, stackTrace) {

View File

@@ -21,17 +21,12 @@ import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.da
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/storage_path_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/service/dio_file_service.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
@@ -105,10 +100,7 @@ void main() async {
await connectivityCubit.initialize();
// Create repositories
final tagRepository = TagRepositoryImpl(labelsApi);
final correspondentRepository = CorrespondentRepositoryImpl(labelsApi);
final documentTypeRepository = DocumentTypeRepositoryImpl(labelsApi);
final storagePathRepository = StoragePathRepositoryImpl(labelsApi);
final labelRepository = LabelRepository(labelsApi);
final savedViewRepository = SavedViewRepositoryImpl(savedViewsApi);
//Create cubits/blocs
@@ -163,17 +155,8 @@ void main() async {
],
child: MultiRepositoryProvider(
providers: [
RepositoryProvider<LabelRepository<Tag>>.value(
value: tagRepository,
),
RepositoryProvider<LabelRepository<Correspondent>>.value(
value: correspondentRepository,
),
RepositoryProvider<LabelRepository<DocumentType>>.value(
value: documentTypeRepository,
),
RepositoryProvider<LabelRepository<StoragePath>>.value(
value: storagePathRepository,
RepositoryProvider<LabelRepository>.value(
value: labelRepository,
),
RepositoryProvider<SavedViewRepository>.value(
value: savedViewRepository,

View File

@@ -1,7 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
@@ -19,9 +18,11 @@ class DocumentDetailsRoute extends StatelessWidget {
context.read(),
context.read(),
context.read(),
context.read(),
initialDocument: args.document,
),
child: LabelRepositoriesProvider(
child: RepositoryProvider.value(
value: context.read(),
child: DocumentDetailsPage(
allowEdit: args.allowEdit,
isLabelClickable: args.isLabelClickable,

View File

@@ -1,7 +1,7 @@
import 'package:paperless_api/src/models/saved_view_model.dart';
abstract class PaperlessSavedViewsApi {
Future<SavedView> find(int id);
Future<SavedView?> find(int id);
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]);
Future<SavedView> save(SavedView view);

View File

@@ -60,7 +60,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
}
@override
Future<SavedView> find(int id) {
Future<SavedView?> find(int id) {
return getSingleResult(
"/api/saved_views/$id/",
SavedView.fromJson,

View File

@@ -4,7 +4,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
Future<T> getSingleResult<T>(
Future<T?> getSingleResult<T>(
String url,
T Function(Map<String, dynamic>) fromJson,
ErrorCode errorCode, {

View File

@@ -731,6 +731,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.4.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454
url: "https://pub.dev"
source: hosted
version: "2.3.2"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338
url: "https://pub.dev"
source: hosted
version: "2.2.0"
frontend_server_client:
dependency: transitive
description:

View File

@@ -91,6 +91,7 @@ dependencies:
dynamic_color: ^1.5.4
flutter_html: ^3.0.0-alpha.6
in_app_review: ^2.0.6
freezed_annotation: ^2.2.0
dev_dependencies:
integration_test:
@@ -105,6 +106,7 @@ dev_dependencies:
json_serializable: ^6.5.4
dart_code_metrics: ^5.4.0
auto_route_generator: ^5.0.3
freezed: ^2.3.2
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec