Resetting filter doesn't reset sorting, some bugfixes and UI updates

This commit is contained in:
Anton Stubenbord
2022-12-14 17:57:01 +01:00
parent a3c3810d35
commit 4bf4ff1cbd
23 changed files with 327 additions and 253 deletions

View File

@@ -49,7 +49,6 @@ void main() async {
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
});
// Mocked classes
@@ -97,7 +96,6 @@ void main() async {
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
});
// Mocked classes
@@ -149,7 +147,6 @@ void main() async {
));
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
});
await t.binding.waitUntilFirstFrameRasterized;
@@ -199,7 +196,6 @@ void main() async {
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
});
await t.binding.waitUntilFirstFrameRasterized;

View File

@@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/main.dart';
@@ -35,5 +36,5 @@ Future<void> initAndLaunchTestApp(
Future<void> Function() initializationCallback,
) async {
await initializationCallback();
runApp(const PaperlessMobileEntrypoint());
//runApp(const PaperlessMobileEntrypoint(authenticationCubit: ),));
}

View File

@@ -1,5 +1,4 @@
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/models/saved_view_model.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:rxdart/rxdart.dart';
@@ -8,11 +7,10 @@ class SavedViewRepositoryImpl implements SavedViewRepository {
SavedViewRepositoryImpl(this._api);
final BehaviorSubject<Map<int, SavedView>> _subject =
BehaviorSubject.seeded({});
final BehaviorSubject<Map<int, SavedView>?> _subject = BehaviorSubject();
@override
Stream<Map<int, SavedView>> get savedViews =>
Stream<Map<int, SavedView>?> get savedViews =>
_subject.stream.asBroadcastStream();
@override
@@ -23,7 +21,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository {
@override
Future<SavedView> create(SavedView view) async {
final created = await _api.save(view);
final updatedState = {..._subject.value}
final updatedState = {..._subject.valueOrNull ?? {}}
..putIfAbsent(created.id!, () => created);
_subject.add(updatedState);
return created;
@@ -32,7 +30,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository {
@override
Future<int> delete(SavedView view) async {
await _api.delete(view);
final updatedState = {..._subject.value}..remove(view.id);
final updatedState = {..._subject.valueOrNull ?? {}}..remove(view.id);
_subject.add(updatedState);
return view.id!;
}
@@ -40,7 +38,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository {
@override
Future<SavedView?> find(int id) async {
final found = await _api.find(id);
final updatedState = {..._subject.value}
final updatedState = {..._subject.valueOrNull ?? {}}
..update(id, (_) => found, ifAbsent: () => found);
_subject.add(updatedState);
return found;
@@ -50,7 +48,7 @@ class SavedViewRepositoryImpl implements SavedViewRepository {
Future<Iterable<SavedView>> findAll([Iterable<int>? ids]) async {
final found = await _api.findAll(ids);
final updatedState = {
..._subject.value,
..._subject.valueOrNull ?? {},
...{for (final view in found) view.id!: view},
};
_subject.add(updatedState);

View File

@@ -1,7 +1,7 @@
import 'package:paperless_api/paperless_api.dart';
abstract class SavedViewRepository {
Stream<Map<int, SavedView>> get savedViews;
Stream<Map<int, SavedView>?> get savedViews;
Future<SavedView> create(SavedView view);
Future<SavedView?> find(int id);

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:paperless_mobile/di_initializer.config.dart';
import 'package:paperless_mobile/di_modules.dart';
import 'package:paperless_mobile/di_paperless_api.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
@@ -21,7 +20,7 @@ void configureDependencies(String environment) =>
///
/// Registers new security context, which will be used by the HttpClient, see [RegisterModule].
///
void registerSecurityContext(ClientCertificate? cert) {
Future<void> registerSecurityContext(ClientCertificate? cert) async {
var context = SecurityContext();
if (cert != null) {
context = context
@@ -29,6 +28,6 @@ void registerSecurityContext(ClientCertificate? cert) {
..useCertificateChainBytes(cert.bytes, password: cert.passphrase)
..setTrustedCertificatesBytes(cert.bytes, password: cert.passphrase);
}
getIt.unregister<SecurityContext>();
await getIt.unregister<SecurityContext>();
getIt.registerSingleton<SecurityContext>(context);
}

View File

@@ -107,11 +107,11 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
color: Colors
.black, //TODO: check if there is a way to dynamically determine color...
),
onPressed: () => Navigator.pop(
context,
BlocProvider.of<DocumentDetailsCubit>(context)
.state
.document),
onPressed: () => Navigator.of(context).pop(
BlocProvider.of<DocumentDetailsCubit>(context)
.state
.document,
),
),
floating: true,
pinned: true,
@@ -237,13 +237,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return ListView(
children: [
_DetailsItem.text(DateFormat().format(document.modified),
label: S.of(context).documentModifiedPropertyLabel,
context: context),
_separator(),
label: S.of(context).documentModifiedPropertyLabel,
context: context)
.paddedOnly(bottom: 16),
_DetailsItem.text(DateFormat().format(document.added),
label: S.of(context).documentAddedPropertyLabel,
context: context),
_separator(),
label: S.of(context).documentAddedPropertyLabel,
context: context)
.paddedSymmetrically(vertical: 16),
_DetailsItem(
label: S.of(context).documentArchiveSerialNumberPropertyLongLabel,
content: document.archiveSerialNumber != null
@@ -255,30 +255,26 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
onPressed:
widget.allowEdit ? () => _assignAsn(document) : null,
),
),
_separator(),
).paddedSymmetrically(vertical: 16),
_DetailsItem.text(
meta.mediaFilename,
context: context,
label: S.of(context).documentMetaDataMediaFilenamePropertyLabel,
),
_separator(),
).paddedSymmetrically(vertical: 16),
_DetailsItem.text(
meta.originalChecksum,
context: context,
label: S.of(context).documentMetaDataChecksumLabel,
),
_separator(),
).paddedSymmetrically(vertical: 16),
_DetailsItem.text(formatBytes(meta.originalSize, 2),
label: S.of(context).documentMetaDataOriginalFileSizeLabel,
context: context),
_separator(),
label: S.of(context).documentMetaDataOriginalFileSizeLabel,
context: context)
.paddedSymmetrically(vertical: 16),
_DetailsItem.text(
meta.originalMimeType,
label: S.of(context).documentMetaDataOriginalMimeTypeLabel,
context: context,
),
_separator(),
).paddedSymmetrically(vertical: 16),
],
);
},
@@ -295,16 +291,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Widget _buildDocumentContentView(DocumentModel document, String? match) {
return SingleChildScrollView(
child: _DetailsItem(
content: HighlightedText(
text: document.content ?? "",
highlights: match == null ? [] : match.split(" "),
style: Theme.of(context).textTheme.bodyText2,
caseSensitive: false,
),
label: S.of(context).documentDetailsPageTabContentLabel,
child: HighlightedText(
text: document.content ?? "",
highlights: match == null ? [] : match.split(" "),
style: Theme.of(context).textTheme.bodyText2,
caseSensitive: false,
),
);
).paddedOnly(top: 8);
}
Widget _buildDocumentOverview(DocumentModel document, String? match) {
@@ -314,60 +307,61 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
content: HighlightedText(
text: document.title,
highlights: match?.split(" ") ?? <String>[],
style: Theme.of(context).textTheme.bodyLarge,
),
label: S.of(context).documentTitlePropertyLabel,
),
_separator(),
).paddedOnly(bottom: 16),
_DetailsItem.text(
DateFormat.yMMMd().format(document.created),
context: context,
label: S.of(context).documentCreatedPropertyLabel,
),
_separator(),
_DetailsItem(
content: DocumentTypeWidget(
isClickable: widget.isLabelClickable,
documentTypeId: document.documentType,
afterSelected: () {
Navigator.pop(context);
},
),
label: S.of(context).documentDocumentTypePropertyLabel,
),
_separator(),
_DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel,
content: CorrespondentWidget(
isClickable: widget.isLabelClickable,
correspondentId: document.correspondent,
afterSelected: () {
Navigator.pop(context);
},
),
),
_separator(),
_DetailsItem(
label: S.of(context).documentStoragePathPropertyLabel,
content: StoragePathWidget(
isClickable: widget.isLabelClickable,
pathId: document.storagePath,
afterSelected: () {
Navigator.pop(context);
},
),
),
_separator(),
_DetailsItem(
label: S.of(context).documentTagsPropertyLabel,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
).paddedSymmetrically(vertical: 16),
Visibility(
visible: document.documentType != null,
child: _DetailsItem(
content: DocumentTypeWidget(
textStyle: Theme.of(context).textTheme.bodyLarge,
isClickable: widget.isLabelClickable,
tagIds: document.tags,
isSelectedPredicate: (_) => false,
onTagSelected: (int tagId) {},
documentTypeId: document.documentType,
),
),
label: S.of(context).documentDocumentTypePropertyLabel,
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.correspondent != null,
child: _DetailsItem(
label: S.of(context).documentCorrespondentPropertyLabel,
content: CorrespondentWidget(
textStyle: Theme.of(context).textTheme.bodyLarge,
isClickable: widget.isLabelClickable,
correspondentId: document.correspondent,
),
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.storagePath != null,
child: _DetailsItem(
label: S.of(context).documentStoragePathPropertyLabel,
content: StoragePathWidget(
isClickable: widget.isLabelClickable,
pathId: document.storagePath,
),
).paddedSymmetrically(vertical: 16),
),
Visibility(
visible: document.tags.isNotEmpty,
child: _DetailsItem(
label: S.of(context).documentTagsPropertyLabel,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TagsWidget(
isClickable: widget.isLabelClickable,
tagIds: document.tags,
isSelectedPredicate: (_) => false,
onTagSelected: (int tagId) {},
),
),
).paddedSymmetrically(vertical: 16),
),
// _separator(),
// FutureBuilder<List<SimilarDocumentModel>>(
@@ -396,10 +390,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
);
}
Widget _separator() {
return const SizedBox(height: 32.0);
}
///
/// Downloads file to temporary directory, from which it can then be shared.
///
@@ -477,10 +467,7 @@ class _DetailsItem extends StatelessWidget {
children: [
Text(
label,
style: Theme.of(context)
.textTheme
.headline5
?.copyWith(fontWeight: FontWeight.bold),
style: Theme.of(context).textTheme.caption,
),
content,
],
@@ -492,7 +479,7 @@ class _DetailsItem extends StatelessWidget {
String text, {
required this.label,
required BuildContext context,
}) : content = Text(text, style: Theme.of(context).textTheme.bodyText2);
}) : content = Text(text, style: Theme.of(context).textTheme.bodyLarge);
}
class ColoredTabBar extends Container implements PreferredSizeWidget {

View File

@@ -88,6 +88,13 @@ class DocumentsCubit extends Cubit<DocumentsState> {
emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
}
Future<void> resetFilter() async {
final filter = DocumentFilter.initial.copyWith(
sortField: state.filter.sortField,
sortOrder: state.filter.sortOrder,
);
}
///
/// Convenience method which allows to directly use [DocumentFilter.copyWith] on the current filter.
///

View File

@@ -151,22 +151,28 @@ class _DocumentsPageState extends State<DocumentsPage> {
switch (settings.preferredViewType) {
case ViewType.list:
child = DocumentListView(
onTap: _openDetails,
state: state,
onTap: _openDetails,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected: _addCorrespondentToFilter,
onDocumentTypeSelected: _addDocumentTypeToFilter,
onStoragePathSelected: _addStoragePathToFilter,
);
break;
case ViewType.grid:
child = DocumentGridView(
onTap: _openDetails,
state: state,
onTap: _openDetails,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected: _addCorrespondentToFilter,
onDocumentTypeSelected: _addDocumentTypeToFilter,
onStoragePathSelected: _addStoragePathToFilter,
);
break;
}
@@ -176,7 +182,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
child: DocumentsEmptyState(
state: state,
onReset: () {
_documentsCubit.updateFilter();
_documentsCubit.resetFilter();
_savedViewCubit.resetSelection();
},
),
@@ -195,7 +201,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
listener: (context, state) {
try {
if (state.selectedSavedViewId == null) {
_documentsCubit.updateFilter();
_documentsCubit.resetFilter();
} else {
final newFilter = state
.value[state.selectedSavedViewId]
@@ -280,6 +286,63 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
}
void _addCorrespondentToFilter(int? correspondentId) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.correspondent.id == correspondentId) {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(correspondent: const CorrespondentQuery.unset()),
);
} else {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
correspondent: CorrespondentQuery.fromId(correspondentId)),
);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
void _addDocumentTypeToFilter(int? documentTypeId) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.documentType.id == documentTypeId) {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(documentType: const DocumentTypeQuery.unset()),
);
} else {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
documentType: DocumentTypeQuery.fromId(documentTypeId)),
);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
void _addStoragePathToFilter(int? pathId) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.correspondent.id == pathId) {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(storagePath: const StoragePathQuery.unset()),
);
} else {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)),
);
}
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
Future<void> _loadNewPage(int pageKey) async {
final pageCount = _documentsCubit.state
.inferPageCount(pageSize: _documentsCubit.state.filter.pageSize);
@@ -299,9 +362,10 @@ class _DocumentsPageState extends State<DocumentsPage> {
Future<void> _onRefresh() async {
try {
await _documentsCubit.updateCurrentFilter(
_documentsCubit.updateCurrentFilter(
(filter) => filter.copyWith(page: 1),
);
_savedViewCubit.reload();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}

View File

@@ -12,6 +12,9 @@ class DocumentGridView extends StatelessWidget {
final DocumentsState state;
final bool hasInternetConnection;
final void Function(int tagId) onTagSelected;
final void Function(int correspondentId) onCorrespondentSelected;
final void Function(int correspondentId) onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
const DocumentGridView({
super.key,
@@ -21,6 +24,9 @@ class DocumentGridView extends StatelessWidget {
required this.onSelected,
required this.hasInternetConnection,
required this.onTagSelected,
required this.onCorrespondentSelected,
required this.onDocumentTypeSelected,
this.onStoragePathSelected,
});
@override
Widget build(BuildContext context) {

View File

@@ -1,5 +1,4 @@
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/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
@@ -16,7 +15,10 @@ class DocumentListView extends StatelessWidget {
final DocumentsState state;
final bool hasInternetConnection;
final bool isLabelClickable;
final void Function(int tagId) onTagSelected;
final void Function(int id)? onTagSelected;
final void Function(int? id)? onCorrespondentSelected;
final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
const DocumentListView({
super.key,
@@ -26,7 +28,10 @@ class DocumentListView extends StatelessWidget {
required this.onSelected,
required this.hasInternetConnection,
this.isLabelClickable = true,
required this.onTagSelected,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
});
@override
@@ -52,6 +57,9 @@ class DocumentListView extends StatelessWidget {
: false;
},
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
),
);
},

View File

@@ -14,7 +14,10 @@ class DocumentListItem extends StatelessWidget {
final bool isLabelClickable;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId) onTagSelected;
final void Function(int tagId)? onTagSelected;
final void Function(int? correspondentId)? onCorrespondentSelected;
final void Function(int? documentTypeId)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
const DocumentListItem({
Key? key,
@@ -25,7 +28,10 @@ class DocumentListItem extends StatelessWidget {
required this.isAtLeastOneSelected,
this.isLabelClickable = true,
required this.isTagSelectedPredicate,
required this.onTagSelected,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
}) : super(key: key);
@override
@@ -48,6 +54,7 @@ class DocumentListItem extends StatelessWidget {
child: CorrespondentWidget(
isClickable: isLabelClickable,
correspondentId: document.correspondent,
onSelected: onCorrespondentSelected,
),
),
],
@@ -68,7 +75,7 @@ class DocumentListItem extends StatelessWidget {
tagIds: document.tags,
isMultiLine: false,
isSelectedPredicate: isTagSelectedPredicate,
onTagSelected: onTagSelected,
onTagSelected: (id) => onTagSelected?.call(id),
),
),
),

View File

@@ -123,7 +123,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
void _resetFilter() async {
FocusScope.of(context).unfocus();
Navigator.pop(context, DocumentFilter.initial);
Navigator.pop(
context,
DocumentFilter.initial.copyWith(
sortField: widget.initialFilter.sortField,
sortOrder: widget.initialFilter.sortOrder,
));
}
Widget _buildDocumentTypeFormField() {

View File

@@ -100,7 +100,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
),
FormBuilderCheckbox(
name: Label.isInsensitiveKey,
initialValue: widget.initialValue?.isInsensitive,
initialValue: widget.initialValue?.isInsensitive ?? true,
title: Text(S.of(context).labelIsInsensivitePropertyLabel),
),
...widget.additionalFields,

View File

@@ -5,7 +5,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.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/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';

View File

@@ -9,16 +9,18 @@ import 'package:paperless_mobile/util.dart';
class CorrespondentWidget extends StatelessWidget {
final int? correspondentId;
final void Function()? afterSelected;
final void Function(int? id)? onSelected;
final Color? textColor;
final bool isClickable;
final TextStyle? textStyle;
const CorrespondentWidget({
Key? key,
this.correspondentId,
this.afterSelected,
required this.correspondentId,
this.textColor,
this.isClickable = true,
this.textStyle,
this.onSelected,
}) : super(key: key);
@override
@@ -30,14 +32,15 @@ class CorrespondentWidget extends StatelessWidget {
BlocBuilder<LabelCubit<Correspondent>, LabelState<Correspondent>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addCorrespondentToFilter(context),
onTap: () => onSelected?.call(correspondentId!),
child: Text(
(state.getLabel(correspondentId)?.name) ?? "-",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
style: (textStyle ?? Theme.of(context).textTheme.bodyText2)
?.copyWith(
color: textColor ?? Theme.of(context).colorScheme.primary,
),
),
);
},
@@ -45,24 +48,4 @@ class CorrespondentWidget extends StatelessWidget {
),
);
}
void _addCorrespondentToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.correspondent.id == correspondentId) {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(correspondent: const CorrespondentQuery.unset()),
);
} else {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
correspondent: CorrespondentQuery.fromId(correspondentId)),
);
}
afterSelected?.call();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -2,20 +2,20 @@ 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/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart';
class DocumentTypeWidget extends StatelessWidget {
final int? documentTypeId;
final void Function()? afterSelected;
final bool isClickable;
final TextStyle? textStyle;
final void Function(int? id)? onSelected;
const DocumentTypeWidget({
Key? key,
required this.documentTypeId,
this.afterSelected,
this.isClickable = true,
this.textStyle,
this.onSelected,
}) : super(key: key);
@override
@@ -27,16 +27,14 @@ class DocumentTypeWidget extends StatelessWidget {
child: AbsorbPointer(
absorbing: !isClickable,
child: GestureDetector(
onTap: () => _addDocumentTypeToFilter(context),
onTap: () => onSelected?.call(documentTypeId),
child:
BlocBuilder<LabelCubit<DocumentType>, LabelState<DocumentType>>(
builder: (context, state) {
return Text(
state.labels[documentTypeId]?.toString() ?? "-",
style: Theme.of(context)
.textTheme
.bodyText2!
.copyWith(color: Theme.of(context).colorScheme.tertiary),
style: (textStyle ?? Theme.of(context).textTheme.bodyText2)
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
);
},
),
@@ -44,24 +42,4 @@ class DocumentTypeWidget extends StatelessWidget {
),
);
}
void _addDocumentTypeToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.documentType.id == documentTypeId) {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(documentType: const DocumentTypeQuery.unset()),
);
} else {
cubit.updateCurrentFilter(
(filter) => filter.copyWith(
documentType: DocumentTypeQuery.fromId(documentTypeId)),
);
}
afterSelected?.call();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -2,23 +2,21 @@ 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/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
import 'package:paperless_mobile/util.dart';
class StoragePathWidget extends StatelessWidget {
final int? pathId;
final void Function()? afterSelected;
final Color? textColor;
final bool isClickable;
final void Function(int? id)? onSelected;
const StoragePathWidget({
Key? key,
this.pathId,
this.afterSelected,
this.textColor,
this.isClickable = true,
this.onSelected,
}) : super(key: key);
@override
@@ -32,7 +30,7 @@ class StoragePathWidget extends StatelessWidget {
child: BlocBuilder<LabelCubit<StoragePath>, LabelState<StoragePath>>(
builder: (context, state) {
return GestureDetector(
onTap: () => _addStoragePathToFilter(context),
onTap: () => onSelected?.call(pathId),
child: Text(
state.getLabel(pathId)?.name ?? "-",
maxLines: 1,
@@ -47,24 +45,4 @@ class StoragePathWidget extends StatelessWidget {
),
);
}
void _addStoragePathToFilter(BuildContext context) {
final cubit = BlocProvider.of<DocumentsCubit>(context);
try {
if (cubit.state.filter.correspondent.id == pathId) {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(storagePath: const StoragePathQuery.unset()),
);
} else {
cubit.updateCurrentFilter(
(filter) =>
filter.copyWith(storagePath: StoragePathQuery.fromId(pathId)),
);
}
afterSelected?.call();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
}

View File

@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
@@ -11,12 +10,9 @@ import 'package:paperless_mobile/features/login/model/user_credentials.model.dar
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
@prod
@test
@singleton
class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi;
PaperlessAuthenticationApi _authApi;
final LocalVault _localVault;
AuthenticationCubit(
@@ -25,10 +21,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
this._authApi,
) : super(AuthenticationState.initial);
Future<void> initialize() {
return restoreSessionState();
}
Future<void> login({
required UserCredentials credentials,
required String serverUrl,
@@ -37,6 +29,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
assert(credentials.username != null && credentials.password != null);
try {
registerSecurityContext(clientCertificate);
//TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive.
_authApi = getIt<PaperlessAuthenticationApi>();
// Store information required to make requests
final currentAuth = AuthenticationInformation(
serverUrl: serverUrl,
@@ -82,13 +76,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
if (storedAuth == null || !storedAuth.isValid) {
return emit(
AuthenticationState(isAuthenticated: false, wasLoginStored: false));
AuthenticationState(isAuthenticated: false, wasLoginStored: false),
);
} else {
if (appSettings.isLocalAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in");
if (localAuthSuccess) {
registerSecurityContext(storedAuth.clientCertificate);
await registerSecurityContext(storedAuth.clientCertificate);
//TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive.
_authApi = getIt<PaperlessAuthenticationApi>();
return emit(
AuthenticationState(
isAuthenticated: true,
@@ -105,11 +102,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
));
}
} else {
return emit(AuthenticationState(
await registerSecurityContext(storedAuth.clientCertificate);
final authState = AuthenticationState(
isAuthenticated: true,
authentication: storedAuth,
wasLoginStored: true,
));
);
return emit(authState);
}
}
}

View File

@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
@@ -12,22 +11,26 @@ class SavedViewCubit extends Cubit<SavedViewState> {
SavedViewCubit(this._repository) : super(SavedViewState(value: {})) {
_subscription = _repository.savedViews.listen(
(savedViews) => emit(state.copyWith(value: savedViews)),
(savedViews) {
if (savedViews == null) {
emit(state.copyWith(isLoaded: false));
} else {
emit(state.copyWith(value: savedViews, isLoaded: true));
}
},
);
}
void selectView(SavedView? view) {
emit(SavedViewState(value: state.value, selectedSavedViewId: view?.id));
emit(state.copyWith(
selectedSavedViewId: view?.id,
overwriteSelectedSavedViewId: true,
));
}
Future<SavedView> add(SavedView view) async {
final savedView = await _repository.create(view);
emit(
SavedViewState(
value: {...state.value, savedView.id!: savedView},
selectedSavedViewId: state.selectedSavedViewId,
),
);
emit(state.copyWith(value: {...state.value, savedView.id!: savedView}));
return savedView;
}
@@ -42,11 +45,16 @@ class SavedViewCubit extends Cubit<SavedViewState> {
Future<void> initialize() async {
final views = await _repository.findAll();
final values = {for (var element in views) element.id!: element};
emit(SavedViewState(value: values));
emit(SavedViewState(value: values, isLoaded: true));
}
Future<void> reload() => initialize();
void resetSelection() {
emit(SavedViewState(value: state.value));
emit(SavedViewState(
value: state.value,
isLoaded: true,
));
}
@override

View File

@@ -2,11 +2,13 @@ import 'package:equatable/equatable.dart';
import 'package:paperless_api/paperless_api.dart';
class SavedViewState with EquatableMixin {
final bool isLoaded;
final Map<int, SavedView> value;
final int? selectedSavedViewId;
SavedViewState({
required this.value,
this.isLoaded = false,
this.selectedSavedViewId,
});
@@ -20,9 +22,11 @@ class SavedViewState with EquatableMixin {
Map<int, SavedView>? value,
int? selectedSavedViewId,
bool overwriteSelectedSavedViewId = false,
bool? isLoaded,
}) {
return SavedViewState(
value: value ?? this.value,
isLoaded: isLoaded ?? this.isLoaded,
selectedSavedViewId: overwriteSelectedSavedViewId
? selectedSavedViewId
: this.selectedSavedViewId,

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
@@ -8,6 +10,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:shimmer/shimmer.dart';
class SavedViewSelectionWidget extends StatelessWidget {
final DocumentFilter currentFilter;
@@ -29,6 +32,9 @@ class SavedViewSelectionWidget extends StatelessWidget {
children: [
BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
if (!state.isLoaded) {
return _buildLoadingWidget(context);
}
if (state.value.isEmpty) {
return Text(S.of(context).savedViewsEmptyStateText);
}
@@ -58,32 +64,61 @@ class SavedViewSelectionWidget extends StatelessWidget {
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).savedViewsLabel,
style: Theme.of(context).textTheme.titleSmall,
),
BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) =>
previous.filter != current.filter,
builder: (context, docState) {
return TextButton.icon(
icon: const Icon(Icons.add),
onPressed: enabled
? () => _onCreatePressed(context, docState.filter)
: null,
label: Text(S.of(context).savedViewCreateNewLabel),
);
},
),
],
BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
S.of(context).savedViewsLabel,
style: Theme.of(context).textTheme.titleSmall,
),
BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) =>
previous.filter != current.filter,
builder: (context, docState) {
return TextButton.icon(
icon: const Icon(Icons.add),
onPressed: (enabled && state.isLoaded)
? () => _onCreatePressed(context, docState.filter)
: null,
label: Text(S.of(context).savedViewCreateNewLabel),
);
},
),
],
);
},
),
],
);
}
Widget _buildLoadingWidget(BuildContext context) {
final r = Random(123456789);
return SizedBox(
height: height,
width: double.infinity,
child: Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: ListView.separated(
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (context, index) => FilterChip(
label: SizedBox(width: r.nextInt((index * 20) + 50).toDouble()),
onSelected: null),
separatorBuilder: (context, index) => SizedBox(width: 8.0),
),
),
);
}
void _onCreatePressed(BuildContext context, DocumentFilter filter) async {
final newView = await Navigator.of(context).push<SavedView?>(
MaterialPageRoute(

View File

@@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -34,6 +35,7 @@ import 'package:paperless_mobile/features/document_upload/cubit/document_upload_
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
@@ -57,7 +59,13 @@ void main() async {
// Load application settings and stored authentication data
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await getIt<AuthenticationCubit>().initialize();
final authCubit = AuthenticationCubit(
getIt<LocalVault>(),
getIt<LocalAuthenticationService>(),
getIt<PaperlessAuthenticationApi>(),
);
await authCubit.restoreSessionState();
// Create repositories
final LabelRepository<Tag> tagRepository =
@@ -80,13 +88,17 @@ void main() async {
RepositoryProvider.value(value: storagePathRepository),
RepositoryProvider.value(value: savedViewRepository),
],
child: const PaperlessMobileEntrypoint(),
child: PaperlessMobileEntrypoint(authenticationCubit: authCubit),
),
);
}
class PaperlessMobileEntrypoint extends StatefulWidget {
const PaperlessMobileEntrypoint({Key? key}) : super(key: key);
final AuthenticationCubit authenticationCubit;
const PaperlessMobileEntrypoint({
Key? key,
required this.authenticationCubit,
}) : super(key: key);
@override
State<PaperlessMobileEntrypoint> createState() =>
@@ -165,8 +177,8 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
],
home: BlocProvider<AuthenticationCubit>.value(
value: getIt<AuthenticationCubit>(),
home: BlocProvider.value(
value: widget.authenticationCubit,
child: const AuthenticationWrapper(),
),
);
@@ -291,10 +303,10 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
if (authentication.isAuthenticated) {
return const HomePage();
} else {
// if (authentication.wasLoginStored &&
// !(authentication.wasLocalAuthenticationSuccessful ?? false)) {
// return const BiometricAuthenticationPage();
// }
if (authentication.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return const BiometricAuthenticationPage();
}
return const LoginPage();
}
},

View File

@@ -261,7 +261,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<StoragePath?> getStoragePath(int id) {
return getSingleResult(
"/api/storage_paths/?page=1&page_size=100000",
"/api/storage_paths/$id/",
StoragePath.fromJson,
ErrorCode.storagePathLoadFailed,
client: client,