Hooked notifications to status changes on document upload - some refactorings

This commit is contained in:
Anton Stubenbord
2023-01-11 01:26:36 +01:00
parent 8cf3020335
commit a4c4726c16
55 changed files with 1128 additions and 761 deletions

View File

@@ -1,10 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart';
import 'package:injectable/injectable.dart';
@prod
@test
@lazySingleton
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
DocumentStatusCubit() : super(null);

View File

@@ -1,20 +1,17 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
@prod
@test
@lazySingleton
class PaperlessServerInformationCubit
extends Cubit<PaperlessServerInformationState> {
final PaperlessServerStatsApi service;
final PaperlessServerStatsApi _api;
PaperlessServerInformationCubit(this.service)
PaperlessServerInformationCubit(this._api)
: super(PaperlessServerInformationState());
Future<void> updateInformtion() async {
final information = await service.getServerInformation();
final information = await _api.getServerInformation();
emit(PaperlessServerInformationState(
isLoaded: true,
information: information,

View File

@@ -68,5 +68,9 @@ String translateError(BuildContext context, ErrorCode code) {
return S.of(context).errorMessageUnsupportedFileFormat;
case ErrorCode.missingClientCertificate:
return S.of(context).errorMessageMissingClientCertificate;
case ErrorCode.suggestionsQueryError:
return S.of(context).errorMessageSuggestionsQueryError;
case ErrorCode.acknowledgeTasksError:
return S.of(context).errorMessageAcknowledgeTasksError;
}
}

View File

@@ -2,15 +2,20 @@ import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/retry_on_connection_change_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
class AuthenticationAwareDioManager {
class SessionManager {
final Dio client;
final List<Interceptor> interceptors;
PaperlessServerInformationModel serverInformation;
AuthenticationAwareDioManager([this.interceptors = const []])
: client = _initDio(interceptors);
SessionManager([this.interceptors = const []])
: client = _initDio(interceptors),
serverInformation = PaperlessServerInformationModel();
static Dio _initDio(List<Interceptor> interceptors) {
//en- and decoded by utf8 by default
@@ -19,8 +24,19 @@ class AuthenticationAwareDioManager {
dio.options.responseType = ResponseType.json;
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) => client..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll(interceptors);
dio.interceptors.add(RetryOnConnectionChangeInterceptor(dio: dio));
dio.interceptors.addAll([
...interceptors,
DioHttpErrorInterceptor(),
PrettyDioLogger(
compact: true,
responseBody: false,
responseHeader: false,
request: false,
requestBody: false,
requestHeader: false,
),
RetryOnConnectionChangeInterceptor(dio: dio)
]);
return dio;
}
@@ -28,6 +44,7 @@ class AuthenticationAwareDioManager {
String? baseUrl,
String? authToken,
ClientCertificate? clientCertificate,
PaperlessServerInformationModel? serverInformation,
}) {
if (clientCertificate != null) {
final context = SecurityContext()
@@ -58,11 +75,16 @@ class AuthenticationAwareDioManager {
if (authToken != null) {
client.options.headers.addAll({'Authorization': 'Token $authToken'});
}
if (serverInformation != null) {
this.serverInformation = serverInformation;
}
}
void resetSettings() {
client.httpClientAdapter = DefaultHttpClientAdapter();
client.options.baseUrl = '';
client.options.headers.remove('Authorization');
serverInformation = PaperlessServerInformationModel();
}
}

View File

@@ -0,0 +1,14 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
extension AddressableHydratedStorage on Storage {
ApplicationSettingsState get settings {
return ApplicationSettingsState.fromJson(read('ApplicationSettingsCubit'));
}
AuthenticationState get authentication {
return AuthenticationState.fromJson(read('AuthenticationCubit'));
}
}

View File

@@ -8,22 +8,40 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
final PaperlessDocumentsApi _api;
DocumentDetailsCubit(this._api, DocumentModel initialDocument)
: super(DocumentDetailsState(document: initialDocument));
: super(DocumentDetailsState(document: initialDocument)) {
loadSuggestions();
}
Future<void> delete(DocumentModel document) async {
await _api.delete(document);
}
Future<void> loadSuggestions() async {
final suggestions = await _api.findSuggestions(state.document);
emit(state.copyWith(suggestions: suggestions));
}
Future<void> loadFullContent() async {
final doc = await _api.find(state.document.id);
if (doc == null) {
return;
}
emit(state.copyWith(
isFullContentLoaded: true,
fullContent: doc.content,
));
}
Future<void> assignAsn(DocumentModel document) async {
if (document.archiveSerialNumber == null) {
final int asn = await _api.findNextAsn();
final updatedDocument =
await _api.update(document.copyWith(archiveSerialNumber: asn));
emit(DocumentDetailsState(document: updatedDocument));
emit(state.copyWith(document: updatedDocument));
}
}
void replaceDocument(DocumentModel document) {
emit(DocumentDetailsState(document: document));
emit(state.copyWith(document: document));
}
}

View File

@@ -2,11 +2,36 @@ part of 'document_details_cubit.dart';
class DocumentDetailsState with EquatableMixin {
final DocumentModel document;
final bool isFullContentLoaded;
final String? fullContent;
final FieldSuggestions suggestions;
const DocumentDetailsState({
required this.document,
this.suggestions = const FieldSuggestions(),
this.isFullContentLoaded = false,
this.fullContent,
});
@override
List<Object?> get props => [document];
List<Object?> get props => [
document,
suggestions,
isFullContentLoaded,
fullContent,
];
DocumentDetailsState copyWith({
DocumentModel? document,
FieldSuggestions? suggestions,
bool? isFullContentLoaded,
String? fullContent,
}) {
return DocumentDetailsState(
document: document ?? this.document,
suggestions: suggestions ?? this.suggestions,
isFullContentLoaded: isFullContentLoaded ?? this.isFullContentLoaded,
fullContent: fullContent ?? this.fullContent,
);
}
}

View File

@@ -25,6 +25,7 @@ import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:badges/badges.dart' as b;
class DocumentDetailsPage extends StatefulWidget {
final bool allowEdit;
@@ -63,9 +64,21 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
if (!connectivityState.isConnected) {
return Container();
}
return FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
return b.Badge(
position: b.BadgePosition.topEnd(top: -12, end: -6),
showBadge: state.suggestions.hasSuggestions,
child: FloatingActionButton(
child: const Icon(Icons.edit),
onPressed: () => _onEdit(state.document),
),
badgeContent: Text(
'${state.suggestions.suggestionsCount}',
style: const TextStyle(
color: Colors.white,
),
),
badgeColor: Theme.of(context).colorScheme.error,
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
);
},
);
@@ -182,6 +195,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
_buildDocumentContentView(
state.document,
widget.titleAndContentQueryString,
state,
),
_buildDocumentMetaDataView(
state.document,
@@ -217,7 +231,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
listener: (context, state) {
cubit.replaceDocument(state.document);
},
child: const DocumentEditPage(),
child: DocumentEditPage(
suggestions: cubit.state.suggestions,
),
),
),
maintainState: true,
@@ -303,14 +319,30 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}
}
Widget _buildDocumentContentView(DocumentModel document, String? match) {
return SingleChildScrollView(
child: HighlightedText(
text: document.content ?? "",
highlights: match == null ? [] : match.split(" "),
style: Theme.of(context).textTheme.bodyMedium,
caseSensitive: false,
),
Widget _buildDocumentContentView(
DocumentModel document,
String? match,
DocumentDetailsState state,
) {
return ListView(
children: [
HighlightedText(
text: (state.isFullContentLoaded
? state.fullContent
: document.content) ??
"",
highlights: match == null ? [] : match.split(" "),
style: Theme.of(context).textTheme.bodyMedium,
caseSensitive: false,
),
if (!state.isFullContentLoaded && (document.content ?? '').isNotEmpty)
TextButton(
child: Text("Show full content ..."),
onPressed: () {
context.read<DocumentDetailsCubit>().loadFullContent();
},
),
],
).paddedOnly(top: 8);
}

View File

@@ -55,17 +55,16 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
));
}
Future<void> upload(
Future<String?> upload(
Uint8List bytes, {
required String filename,
required String title,
required void Function(DocumentModel document)? onConsumptionFinished,
int? documentType,
int? correspondent,
Iterable<int> tags = const [],
DateTime? createdAt,
}) async {
await _documentApi.create(
return await _documentApi.create(
bytes,
filename: filename,
title: title,
@@ -74,11 +73,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
tags: tags,
createdAt: createdAt,
);
if (onConsumptionFinished != null) {
_documentApi
.waitForConsumptionFinished(filename, title)
.then((value) => onConsumptionFinished(value));
}
}
@override

View File

@@ -25,14 +25,12 @@ class DocumentUploadPreparationPage extends StatefulWidget {
final String? title;
final String? filename;
final String? fileExtension;
final void Function(DocumentModel)? onSuccessfullyConsumed;
const DocumentUploadPreparationPage({
Key? key,
required this.fileBytes,
this.title,
this.filename,
this.onSuccessfullyConsumed,
this.fileExtension,
}) : super(key: key);
@@ -236,19 +234,18 @@ class _DocumentUploadPreparationPageState
final correspondent =
fv[DocumentModel.correspondentKey] as IdQueryParameter;
await cubit.upload(
final taskId = await cubit.upload(
widget.fileBytes,
filename:
_padWithPdfExtension(_formKey.currentState?.value[fkFileName]),
title: title,
onConsumptionFinished: widget.onSuccessfullyConsumed,
documentType: docType.id,
correspondent: correspondent.id,
tags: tags.ids,
createdAt: createdAt,
);
showSnackBar(context, S.of(context).documentUploadSuccessText);
Navigator.pop(context, true);
Navigator.pop(context, taskId);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (errors) {

View File

@@ -52,7 +52,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
log("[DocumentsCubit] load");
emit(state.copyWith(isLoading: true));
try {
final result = await _api.find(state.filter);
final result = await _api.findAll(state.filter);
emit(state.copyWith(
isLoading: false,
hasLoaded: true,
@@ -67,11 +67,13 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
log("[DocumentsCubit] reload");
emit(state.copyWith(isLoading: true));
try {
final result = await _api.find(state.filter.copyWith(page: 1));
final filter = state.filter.copyWith(page: 1);
final result = await _api.findAll(filter);
emit(state.copyWith(
hasLoaded: true,
value: [result],
isLoading: false,
filter: filter,
));
} finally {
emit(state.copyWith(isLoading: false));
@@ -81,7 +83,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
Future<void> _bulkReloadDocuments() async {
emit(state.copyWith(isLoading: true));
try {
final result = await _api.find(
final result = await _api.findAll(
state.filter.copyWith(
page: 1,
pageSize: state.documents.length,
@@ -106,7 +108,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
emit(state.copyWith(isLoading: true));
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
try {
final result = await _api.find(newFilter);
final result = await _api.findAll(newFilter);
emit(
DocumentsState(
hasLoaded: true,
@@ -129,7 +131,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
log("[DocumentsCubit] updateFilter");
try {
emit(state.copyWith(isLoading: true));
final result = await _api.find(filter.copyWith(page: 1));
final result = await _api.findAll(filter.copyWith(page: 1));
emit(
DocumentsState(
@@ -163,7 +165,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
void toggleDocumentSelection(DocumentModel model) {
log("[DocumentsCubit] toggleSelection");
if (state.selection.contains(model)) {
if (state.selectedIds.contains(model.id)) {
emit(
state.copyWith(
selection: state.selection
@@ -183,6 +185,12 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
emit(state.copyWith(selection: []));
}
@override
void onChange(Change<DocumentsState> change) {
super.onChange(change);
log("[DocumentsCubit] state changed: ${change.currentState.selection.map((e) => e.id).join(",")} to ${change.nextState.selection.map((e) => e.id).join(",")}");
}
void reset() {
log("[DocumentsCubit] reset");
emit(const DocumentsState());
@@ -196,7 +204,7 @@ class DocumentsCubit extends Cubit<DocumentsState> with HydratedMixin {
if (filter == null) {
return;
}
final results = await _api.find(filter.copyWith(page: 1));
final results = await _api.findAll(filter.copyWith(page: 1));
emit(
DocumentsState(
filter: filter,

View File

@@ -23,6 +23,8 @@ class DocumentsState extends Equatable {
this.selectedSavedViewId,
});
List<int> get selectedIds => selection.map((e) => e.id).toList();
int get currentPageNumber {
return filter.page;
}

View File

@@ -3,6 +3,7 @@ import 'dart:async';
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:form_builder_validators/form_builder_validators.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
@@ -15,14 +16,18 @@ import 'package:paperless_mobile/features/edit_document/cubit/edit_document_cubi
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';
import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.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/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class DocumentEditPage extends StatefulWidget {
final FieldSuggestions suggestions;
const DocumentEditPage({
Key? key,
required this.suggestions,
}) : super(key: key);
@override
@@ -74,14 +79,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
_buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created).padded(),
_buildDocumentTypeFormField(
state.document.documentType, state.documentTypes)
.padded(),
state.document.documentType,
state.documentTypes,
).padded(),
_buildCorrespondentFormField(
state.document.correspondent, state.correspondents)
.padded(),
state.document.correspondent,
state.correspondents,
).padded(),
_buildStoragePathFormField(
state.document.storagePath, state.storagePaths)
.padded(),
state.document.storagePath,
state.storagePaths,
).padded(),
TagFormField(
initialValue:
IdsTagsQuery.included(state.document.tags.toList()),
@@ -99,58 +107,101 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
}
Widget _buildStoragePathFormField(
int? initialId, Map<int, StoragePath> options) {
return LabelFormField<StoragePath>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context
.read<LabelRepository<StoragePath, StoragePathRepositoryState>>(),
child: AddStoragePathPage(initalValue: initialValue),
),
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
labelOptions: options,
initialValue: IdQueryParameter.fromId(initialId),
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
int? initialId,
Map<int, StoragePath> options,
) {
return Column(
children: [
LabelFormField<StoragePath>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read<
LabelRepository<StoragePath, StoragePathRepositoryState>>(),
child: AddStoragePathPage(initalValue: initialValue),
),
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
labelOptions: options,
initialValue: IdQueryParameter.fromId(initialId),
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
),
if (widget.suggestions.hasSuggestedStoragePaths)
_buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.storagePaths,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkStoragePath]
?.didChange((IdQueryParameter.fromId(itemData))),
),
),
],
);
}
Widget _buildCorrespondentFormField(
int? initialId, Map<int, Correspondent> options) {
return LabelFormField<Correspondent>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read<
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
child: AddCorrespondentPage(initialName: initialValue),
),
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
labelOptions: options,
initialValue: IdQueryParameter.fromId(initialId),
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
return Column(
children: [
LabelFormField<Correspondent>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider(
create: (context) => context.read<
LabelRepository<Correspondent, CorrespondentRepositoryState>>(),
child: AddCorrespondentPage(initialName: initialValue),
),
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
labelOptions: options,
initialValue: IdQueryParameter.fromId(initialId),
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
),
if (widget.suggestions.hasSuggestedCorrespondents)
_buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.correspondents,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkCorrespondent]
?.didChange((IdQueryParameter.fromId(itemData))),
),
),
],
);
}
Widget _buildDocumentTypeFormField(
int? initialId, Map<int, DocumentType> options) {
return LabelFormField<DocumentType>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
create: (context) => context
.read<LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
child: AddDocumentTypePage(
initialName: currentInput,
int? initialId,
Map<int, DocumentType> options,
) {
return Column(
children: [
LabelFormField<DocumentType>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider(
create: (context) => context.read<
LabelRepository<DocumentType, DocumentTypeRepositoryState>>(),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
initialValue: IdQueryParameter.fromId(initialId),
labelOptions: options,
name: fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined),
),
),
textFieldLabel: S.of(context).documentDocumentTypePropertyLabel,
initialValue: IdQueryParameter.fromId(initialId),
labelOptions: options,
name: fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined),
if (widget.suggestions.hasSuggestedDocumentTypes)
_buildSuggestionsSkeleton<int>(
suggestions: widget.suggestions.documentTypes,
itemBuilder: (context, itemData) => ActionChip(
label: Text(options[itemData]!.name),
onPressed: () => _formKey.currentState?.fields[fkDocumentType]
?.didChange(IdQueryParameter.fromId(itemData)),
),
),
],
);
}
@@ -198,16 +249,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
}
Widget _buildCreatedAtFormField(DateTime? initialCreatedAtDate) {
return FormBuilderDateTimePicker(
inputType: InputType.date,
name: fkCreatedDate,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.calendar_month_outlined),
label: Text(S.of(context).documentCreatedPropertyLabel),
),
initialValue: initialCreatedAtDate,
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
initialEntryMode: DatePickerEntryMode.calendar,
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderDateTimePicker(
inputType: InputType.date,
name: fkCreatedDate,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.calendar_month_outlined),
label: Text(S.of(context).documentCreatedPropertyLabel),
),
initialValue: initialCreatedAtDate,
format: DateFormat("dd. MMMM yyyy"), //TODO: Localized date format
initialEntryMode: DatePickerEntryMode.calendar,
),
if (widget.suggestions.hasSuggestedDates)
_buildSuggestionsSkeleton<DateTime>(
suggestions: widget.suggestions.dates,
itemBuilder: (context, itemData) => ActionChip(
label: Text(DateFormat.yMd().format(itemData)),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
?.didChange(itemData),
),
),
],
);
}
Widget _buildSuggestionsSkeleton<T>({
required Iterable<T> suggestions,
required ItemBuilder<T> itemBuilder,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Suggestions: ",
style: Theme.of(context).textTheme.bodySmall,
),
SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: suggestions.length,
itemBuilder: (context, index) =>
itemBuilder(context, suggestions.elementAt(index)),
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(width: 4.0),
),
),
],
).padded();
}
}

View File

@@ -1,27 +1,30 @@
import 'package:badges/badges.dart' as b;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/documents_page_app_bar.dart';
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class DocumentFilterIntent {
@@ -42,9 +45,11 @@ class DocumentsPage extends StatefulWidget {
}
class _DocumentsPageState extends State<DocumentsPage> {
final _pagingController = PagingController<int, DocumentModel>(
firstPageKey: 1,
);
final ScrollController _scrollController = ScrollController();
double _offset = 0;
double _last = 0;
static const double _savedViewWidgetHeight = 78 + 16;
@override
void initState() {
@@ -55,12 +60,36 @@ class _DocumentsPageState extends State<DocumentsPage> {
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
_pagingController.addPageRequestListener(_loadNewPage);
_scrollController
..addListener(_listenForScrollChanges)
..addListener(_listenForLoadDataTrigger);
}
void _listenForLoadDataTrigger() {
final currState = context.read<DocumentsCubit>().state;
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!currState.isLoading &&
!currState.isLastPageLoaded) {
_loadNewPage();
}
}
void _listenForScrollChanges() {
final current = _scrollController.offset;
_offset += _last - current;
if (_offset <= -_savedViewWidgetHeight) _offset = -_savedViewWidgetHeight;
if (_offset >= 0) _offset = 0;
_last = current;
if (_offset <= 0 && _offset >= -_savedViewWidgetHeight) {
setState(() {});
}
}
@override
void dispose() {
_pagingController.dispose();
_scrollController.dispose();
super.dispose();
}
@@ -78,6 +107,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
},
builder: (context, connectivityState) {
const linearProgressIndicatorHeight = 4.0;
return Scaffold(
drawer: BlocProvider.value(
value: context.read<AuthenticationCubit>(),
@@ -85,16 +115,65 @@ class _DocumentsPageState extends State<DocumentsPage> {
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
),
),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(
kToolbarHeight + linearProgressIndicatorHeight,
),
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return AppBar(
title: Text(
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
),
actions: [
const SortDocumentsButton(),
BlocBuilder<ApplicationSettingsCubit,
ApplicationSettingsState>(
builder: (context, settingsState) => IconButton(
icon: Icon(
settingsState.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view_rounded,
),
onPressed: () {
// Reset saved view widget position as scroll offset will be reset anyway.
setState(() {
_offset = 0;
_last = 0;
});
final cubit =
context.read<ApplicationSettingsCubit>();
cubit.setViewType(
cubit.state.preferredViewType.toggle());
},
),
),
],
bottom: PreferredSize(
preferredSize:
const Size.fromHeight(linearProgressIndicatorHeight),
child: state.isLoading
? const LinearProgressIndicator()
: const SizedBox(height: 4.0),
),
);
},
),
),
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
return Badge.count(
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
alignment: const AlignmentDirectional(44, -4),
isLabelVisible: appliedFiltersCount > 0,
count: state.filter.appliedFiltersCount,
backgroundColor: Colors.red,
textColor: Colors.white,
return b.Badge(
position: b.BadgePosition.topEnd(top: -12, end: -6),
showBadge: appliedFiltersCount > 0,
badgeContent: Text(
'$appliedFiltersCount',
style: const TextStyle(
color: Colors.white,
),
),
animationType: b.BadgeAnimationType.fade,
badgeColor: Theme.of(context).colorScheme.error,
child: FloatingActionButton(
child: const Icon(Icons.filter_alt_outlined),
onPressed: _openDocumentFilter,
@@ -103,12 +182,71 @@ class _DocumentsPageState extends State<DocumentsPage> {
},
),
resizeToAvoidBottomInset: true,
body: _buildBody(connectivityState),
body: WillPopScope(
onWillPop: () async {
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
context.read<DocumentsCubit>().resetSelection();
}
return false;
},
child: RefreshIndicator(
onRefresh: _onRefresh,
notificationPredicate: (_) => connectivityState.isConnected,
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
builder: (context, taskState) {
return Stack(
children: [
_buildBody(connectivityState),
Positioned(
left: 0,
right: 0,
top: _offset,
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
return ColoredBox(
color: Theme.of(context).colorScheme.background,
child: SavedViewSelectionWidget(
height: _savedViewWidgetHeight,
currentFilter: state.filter,
enabled: state.selection.isEmpty &&
connectivityState.isConnected,
),
);
},
),
),
if (taskState.task != null &&
taskState.isSuccess &&
!taskState.task!.acknowledged)
_buildNewDocumentAvailableButton(context),
],
);
},
),
),
),
);
},
);
}
Align _buildNewDocumentAvailableButton(BuildContext context) {
return Align(
alignment: Alignment.bottomLeft,
child: FilledButton(
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Theme.of(context).colorScheme.error),
),
child: Text("New document available!"),
onPressed: () {
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
context.read<DocumentsCubit>().reload();
},
).paddedOnly(bottom: 24, left: 24),
);
}
void _openDocumentFilter() async {
final draggableSheetController = DraggableScrollableController();
final filterIntent = await showModalBottomSheet<DocumentFilterIntent>(
@@ -160,88 +298,45 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
}
String _formatDocumentCount(int count) {
return count > 99 ? "99+" : count.toString();
}
Widget _buildBody(ConnectivityState connectivityState) {
final isConnected = connectivityState == ConnectivityState.connected;
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return BlocBuilder<DocumentsCubit, DocumentsState>(
buildWhen: (previous, current) => !const ListEquality()
.equals(previous.documents, current.documents),
buildWhen: (previous, current) =>
!const ListEquality()
.equals(previous.documents, current.documents) ||
previous.selectedIds != current.selectedIds,
builder: (context, state) {
// Some ugly tricks to make it work with bloc, update pageController
_pagingController.value = PagingState(
itemList: state.documents,
nextPageKey: state.nextPageNumber,
);
late Widget child;
switch (settings.preferredViewType) {
case ViewType.list:
child = DocumentListView(
state: state,
onTap: _openDetails,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected: _addCorrespondentToFilter,
onDocumentTypeSelected: _addDocumentTypeToFilter,
onStoragePathSelected: _addStoragePathToFilter,
);
break;
case ViewType.grid:
child = DocumentGridView(
state: state,
onTap: _openDetails,
onSelected: _onSelected,
pagingController: _pagingController,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected: _addCorrespondentToFilter,
onDocumentTypeSelected: _addDocumentTypeToFilter,
onStoragePathSelected: _addStoragePathToFilter,
);
break;
}
if (state.hasLoaded && state.documents.isEmpty) {
child = SliverToBoxAdapter(
child: DocumentsEmptyState(
state: state,
onReset: () {
context.read<DocumentsCubit>().resetFilter();
context.read<DocumentsCubit>().unselectView();
},
),
return DocumentsEmptyState(
state: state,
onReset: () {
context.read<DocumentsCubit>().resetFilter();
context.read<DocumentsCubit>().unselectView();
},
);
}
return RefreshIndicator(
onRefresh: _onRefresh,
notificationPredicate: (_) => isConnected,
child: CustomScrollView(
slivers: [
DocumentsPageAppBar(
isOffline: connectivityState != ConnectivityState.connected,
actions: [
const SortDocumentsButton(),
IconButton(
icon: Icon(
settings.preferredViewType == ViewType.grid
? Icons.list
: Icons.grid_view,
),
onPressed: () => context
.read<ApplicationSettingsCubit>()
.setViewType(
settings.preferredViewType.toggle(),
),
),
],
),
child,
],
),
return AdaptiveDocumentsView(
viewType: settings.preferredViewType,
state: state,
scrollController: _scrollController,
onTap: _openDetails,
onSelected: _onSelected,
hasInternetConnection: isConnected,
onTagSelected: _addTagToFilter,
onCorrespondentSelected: _addCorrespondentToFilter,
onDocumentTypeSelected: _addDocumentTypeToFilter,
onStoragePathSelected: _addStoragePathToFilter,
pageLoadingWidget: const NewItemsLoadingWidget(),
beforeItems: const SizedBox(height: _savedViewWidgetHeight),
);
},
);
@@ -355,15 +450,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
}
Future<void> _loadNewPage(int pageKey) async {
final documentsCubit = context.read<DocumentsCubit>();
final pageCount = documentsCubit.state
.inferPageCount(pageSize: documentsCubit.state.filter.pageSize);
if (pageCount <= pageKey + 1) {
_pagingController.nextPageKey = null;
}
Future<void> _loadNewPage() async {
try {
await documentsCubit.loadMore();
await context.read<DocumentsCubit>().loadMore();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
@@ -376,8 +465,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
Future<void> _onRefresh() async {
try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<DocumentsCubit>().reload();
await context.read<SavedViewCubit>().reload();
context.read<DocumentsCubit>().reload();
context.read<SavedViewCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}

View File

@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class DocumentGridView extends StatelessWidget {
final void Function(DocumentModel model) onTap;
final void Function(DocumentModel) onSelected;
final PagingController<int, DocumentModel> pagingController;
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,
required this.onTap,
required this.pagingController,
required this.state,
required this.onSelected,
required this.hasInternetConnection,
required this.onTagSelected,
required this.onCorrespondentSelected,
required this.onDocumentTypeSelected,
this.onStoragePathSelected,
});
@override
Widget build(BuildContext context) {
return PagedSliverGrid<int, DocumentModel>(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1 / 2,
),
pagingController: pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (context, item, index) {
return DocumentGridItem(
document: item,
onTap: onTap,
isSelected: state.selection.contains(item),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
);
},
noItemsFoundIndicatorBuilder: (context) =>
const DocumentsListLoadingWidget(), //TODO: Replace with grid loading widget
),
);
}
}

View File

@@ -13,7 +13,7 @@ class DocumentGridItem extends StatelessWidget {
final void Function(DocumentModel) onSelected;
final bool isAtLeastOneSelected;
final bool Function(int tagId) isTagSelectedPredicate;
final void Function(int tagId) onTagSelected;
final void Function(int tagId)? onTagSelected;
const DocumentGridItem({
Key? key,
@@ -57,9 +57,11 @@ class DocumentGridItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CorrespondentWidget(
correspondentId: document.correspondent),
correspondentId: document.correspondent,
),
DocumentTypeWidget(
documentTypeId: document.documentType),
documentTypeId: document.documentType,
),
Text(
document.title,
maxLines: document.tags.isEmpty ? 3 : 2,

View File

@@ -0,0 +1,118 @@
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/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/grid/document_grid_item.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
class AdaptiveDocumentsView extends StatelessWidget {
final ViewType viewType;
final Widget beforeItems;
final void Function(DocumentModel) onTap;
final void Function(DocumentModel) onSelected;
final ScrollController scrollController;
final DocumentsState state;
final bool hasInternetConnection;
final bool isLabelClickable;
final void Function(int id)? onTagSelected;
final void Function(int? id)? onCorrespondentSelected;
final void Function(int? id)? onDocumentTypeSelected;
final void Function(int? id)? onStoragePathSelected;
final Widget pageLoadingWidget;
const AdaptiveDocumentsView({
super.key,
required this.onTap,
required this.scrollController,
required this.state,
required this.onSelected,
required this.hasInternetConnection,
this.isLabelClickable = true,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
required this.pageLoadingWidget,
required this.beforeItems,
required this.viewType,
});
@override
Widget build(BuildContext context) {
return CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(child: beforeItems),
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
],
);
}
SliverList _buildListView() {
return SliverList(
delegate: SliverChildBuilderDelegate(
childCount: state.documents.length,
(context, index) {
final document = state.documents.elementAt(index);
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: state.selectedIds.contains(document.id),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
),
);
},
),
);
}
Widget _buildGridView() {
return SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 178,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 1 / 2,
),
itemCount: state.documents.length,
itemBuilder: (context, index) {
if (state.hasLoaded &&
state.isLoading &&
index == state.documents.length) {
return Center(child: pageLoadingWidget);
}
final document = state.documents.elementAt(index);
return DocumentGridItem(
document: document,
onTap: onTap,
isSelected: state.selectedIds.contains(document.id),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
);
},
);
}
}

View File

@@ -1,73 +0,0 @@
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/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class DocumentListView extends StatelessWidget {
final void Function(DocumentModel) onTap;
final void Function(DocumentModel) onSelected;
final PagingController<int, DocumentModel> pagingController;
final DocumentsState state;
final bool hasInternetConnection;
final bool isLabelClickable;
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,
required this.onTap,
required this.pagingController,
required this.state,
required this.onSelected,
required this.hasInternetConnection,
this.isLabelClickable = true,
this.onTagSelected,
this.onCorrespondentSelected,
this.onDocumentTypeSelected,
this.onStoragePathSelected,
});
@override
Widget build(BuildContext context) {
return PagedSliverList<int, DocumentModel>(
pagingController: pagingController,
builderDelegate: PagedChildBuilderDelegate(
animateTransitions: true,
itemBuilder: (context, document, index) {
return LabelRepositoriesProvider(
child: DocumentListItem(
isLabelClickable: isLabelClickable,
document: document,
onTap: onTap,
isSelected: state.selection.contains(document),
onSelected: onSelected,
isAtLeastOneSelected: state.selection.isNotEmpty,
isTagSelectedPredicate: (int tagId) {
return state.filter.tags is IdsTagsQuery
? (state.filter.tags as IdsTagsQuery)
.includedIds
.contains(tagId)
: false;
},
onTagSelected: onTagSelected,
onCorrespondentSelected: onCorrespondentSelected,
onDocumentTypeSelected: onDocumentTypeSelected,
onStoragePathSelected: onStoragePathSelected,
),
);
},
noItemsFoundIndicatorBuilder: (context) => hasInternetConnection
? const DocumentsListLoadingWidget()
: const OfflineWidget(),
),
);
}
}

View File

@@ -38,6 +38,7 @@ class DocumentListItem extends StatelessWidget {
Widget build(BuildContext context) {
return SizedBox(
child: ListTile(
trailing: Text("${document.id}"),
dense: true,
selected: isSelected,
onTap: () => _onTap(),

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class NewItemsLoadingWidget extends StatelessWidget {
const NewItemsLoadingWidget({super.key});
@override
Widget build(BuildContext context) {
return const CircularProgressIndicator();
}
}

View File

@@ -138,3 +138,19 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
return count > 99 ? "99+" : count.toString();
}
}
class ScrollListener extends ChangeNotifier {
double top = 0;
double _last = 0;
ScrollListener.initialise(ScrollController controller, [double height = 56]) {
controller.addListener(() {
final current = controller.offset;
top += _last - current;
if (top <= -height) top = -height;
if (top >= 0) top = 0;
_last = current;
if (top <= 0 && top >= -height) notifyListeners();
});
}
}

View File

@@ -47,46 +47,6 @@ class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
LocalNotificationService.instance.notifyTaskChanged(
Task(
id: 100,
dateCreated: DateTime.now(),
dateDone: DateTime.now(),
taskFileName: "test_file.pdf",
status: TaskStatus.started,
taskId: "abc-def-123-456",
type: "file",
),
);
Future.delayed(const Duration(seconds: 5), () {
LocalNotificationService.instance.notifyTaskChanged(
Task(
id: 100,
dateCreated: DateTime.now(),
dateDone: DateTime.now(),
taskFileName: "test_file.pdf",
status: TaskStatus.pending,
taskId: "abc-def-123-456",
type: "file",
),
);
});
Future.delayed(const Duration(seconds: 10), () {
LocalNotificationService.instance.notifyTaskChanged(
Task(
id: 100,
acknowledged: false,
dateCreated: DateTime.now(),
dateDone: DateTime.now(),
relatedDocumentId: 180,
result: "New document successfully created.",
status: TaskStatus.success,
taskFileName: "test_file.pdf",
taskId: "abc-def-123-456",
type: "file",
),
);
});
_initializeData(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles();
@@ -195,7 +155,14 @@ class _HomePageState extends State<HomePage> {
},
),
BlocListener<TaskStatusCubit, TaskStatusState>(
listener: (context, state) {},
listener: (context, state) {
if (state.task != null) {
// Handle local notifications on task change (only when app is running for now).
context
.read<LocalNotificationService>()
.notifyTaskChanged(state.task!);
}
},
),
],
child: Scaffold(

View File

@@ -1,5 +1,4 @@
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/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
@@ -28,7 +27,7 @@ class InboxCubit extends Cubit<InboxState> {
));
}
final inboxDocuments = await _documentsApi
.find(DocumentFilter(
.findAll(DocumentFilter(
tags: AnyAssignedTagsQuery(tagIds: inboxTags),
sortField: SortField.added,
))

View File

@@ -10,7 +10,7 @@ class TagsWidget extends StatefulWidget {
final Iterable<int> tagIds;
final bool isMultiLine;
final VoidCallback? afterTagTapped;
final void Function(int tagId) onTagSelected;
final void Function(int tagId)? onTagSelected;
final bool isClickable;
final bool Function(int id) isSelectedPredicate;
@@ -42,7 +42,7 @@ class _TagsWidgetState extends State<TagsWidget> {
afterTagTapped: widget.afterTagTapped,
isClickable: widget.isClickable,
isSelected: widget.isSelectedPredicate(id),
onSelected: () => widget.onTagSelected(id),
onSelected: () => widget.onTagSelected?.call(id),
),
)
.toList();

View File

@@ -11,7 +11,7 @@ class LinkedDocumentsCubit extends Cubit<LinkedDocumentsState> {
}
Future<void> _initialize() async {
final documents = await _api.find(
final documents = await _api.findAll(
state.filter.copyWith(
pageSize: 100,
),

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
@@ -18,15 +17,6 @@ class LinkedDocumentsPage extends StatefulWidget {
}
class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
final _pagingController =
PagingController<int, DocumentModel>(firstPageKey: 1);
@override
void initState() {
super.initState();
_pagingController.nextPageKey = null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -38,8 +28,6 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
if (!state.isLoaded) {
return const DocumentsListLoadingWidget();
}
_pagingController.itemList = state.documents!.results;
return Column(
children: [
Text(
@@ -48,42 +36,34 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
style: Theme.of(context).textTheme.bodySmall,
),
Expanded(
child: CustomScrollView(
slivers: [
PagedSliverList<int, DocumentModel>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
animateTransitions: true,
itemBuilder: (context, document, index) {
return DocumentListItem(
isLabelClickable: false,
document: document,
onTap: (doc) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentDetailsCubit(
context.read<PaperlessDocumentsApi>(),
document,
),
child: const DocumentDetailsPage(
isLabelClickable: false,
allowEdit: false,
),
),
),
);
},
isSelected: false,
isAtLeastOneSelected: false,
isTagSelectedPredicate: (_) => false,
onTagSelected: (int tag) {},
);
},
),
),
],
child: ListView.builder(
itemBuilder: (context, index) {
return DocumentListItem(
isLabelClickable: false,
document: state.documents!.results.elementAt(index),
onTap: (doc) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentDetailsCubit(
context.read<PaperlessDocumentsApi>(),
state.documents!.results.elementAt(index),
),
child: const DocumentDetailsPage(
isLabelClickable: false,
allowEdit: false,
),
),
),
);
},
isSelected: false,
isAtLeastOneSelected: false,
isTagSelectedPredicate: (_) => false,
onTagSelected: (int tag) {},
);
},
),
),
],

View File

@@ -1,7 +1,7 @@
import 'package:dio/dio.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
@@ -12,7 +12,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
with HydratedMixin<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi;
final AuthenticationAwareDioManager _dioWrapper;
final SessionManager _dioWrapper;
AuthenticationCubit(
this._localAuthService,

View File

@@ -1,8 +1,6 @@
import 'package:injectable/injectable.dart';
import 'package:local_auth/local_auth.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
@lazySingleton
class LocalAuthenticationService {
final LocalVault localStore;
final LocalAuthentication localAuthentication;

View File

@@ -1,11 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
part 'notification_state.dart';
class NotificationCubit extends Cubit<NotificationState> {
NotificationCubit() : super(NotificationInitialState());
void navigateTo(String route, dynamic args) {}
}

View File

@@ -1,16 +0,0 @@
part of 'notification_cubit.dart';
abstract class NotificationState extends Equatable {
const NotificationState();
@override
List<Object> get props => [];
}
class NotificationInitialState extends NotificationState {}
class NotificationOpenDocumentDetailsPageState extends NotificationState {
final int documentId;
const NotificationOpenDocumentDetailsPageState(this.documentId);
}

View File

@@ -11,9 +11,7 @@ class LocalNotificationService {
final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
LocalNotificationService._();
static final LocalNotificationService instance = LocalNotificationService._();
LocalNotificationService();
Future<void> initialize() async {
const AndroidInitializationSettings initializationSettingsAndroid =
@@ -71,7 +69,7 @@ class LocalNotificationService {
body = task.taskFileName;
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
payload = CreateDocumentSuccessNotificationResponsePayload(
task.relatedDocumentId!,
task.relatedDocument!,
);
break;
default:

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
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/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/selection/confirm_delete_saved_view_dialog.dart';
@@ -31,91 +32,97 @@ class SavedViewSelectionWidget extends StatelessWidget {
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectivityState) {
final hasInternetConnection = connectivityState.isConnected;
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
if (!state.hasLoaded) {
return _buildLoadingWidget(context);
}
if (state.value.isEmpty) {
return Text(S.of(context).savedViewsEmptyStateText);
}
return SizedBox(
height: height,
child: ListView.separated(
itemCount: state.value.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final view = state.value.values.elementAt(index);
return GestureDetector(
onLongPress: hasInternetConnection
? () => _onDelete(context, view)
: null,
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, docState) {
return FilterChip(
label: Text(
state.value.values.toList()[index].name,
),
selected: view.id == docState.selectedSavedViewId,
onSelected: enabled && hasInternetConnection
? (isSelected) =>
_onSelected(isSelected, context, view)
: null,
);
},
),
);
},
separatorBuilder: (context, index) => const SizedBox(
width: 4.0,
),
),
);
},
),
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.hasLoaded &&
hasInternetConnection)
? () => _onCreatePressed(context, docState.filter)
return SizedBox(
height: height,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
if (!state.hasLoaded) {
return _buildLoadingWidget(context);
}
if (state.value.isEmpty) {
return Text(S.of(context).savedViewsEmptyStateText);
}
return SizedBox(
height: 38,
child: ListView.separated(
itemCount: state.value.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final view = state.value.values.elementAt(index);
return GestureDetector(
onLongPress: hasInternetConnection
? () => _onDelete(context, view)
: null,
label: Text(S.of(context).savedViewCreateNewLabel),
child: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, docState) {
final view = state.value.values.toList()[index];
return FilterChip(
label: Text(
view.name,
),
selected:
view.id == docState.selectedSavedViewId,
onSelected: enabled && hasInternetConnection
? (isSelected) =>
_onSelected(isSelected, context, view)
: null,
);
},
),
);
},
separatorBuilder: (context, index) => const SizedBox(
width: 4.0,
),
),
],
);
},
),
],
);
},
),
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.hasLoaded &&
hasInternetConnection)
? () =>
_onCreatePressed(context, docState.filter)
: null,
label: Text(S.of(context).savedViewCreateNewLabel),
);
},
),
],
);
},
),
],
).padded(),
);
},
);
}
Widget _buildLoadingWidget(BuildContext context) {
final r = Random(123456789);
return SizedBox(
height: height,
width: double.infinity,
height: 38,
width: MediaQuery.of(context).size.width,
child: Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
@@ -123,14 +130,35 @@ class SavedViewSelectionWidget extends StatelessWidget {
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: ListView.separated(
child: ListView(
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) => const SizedBox(width: 4.0),
children: [
FilterChip(
label: const SizedBox(width: 32),
onSelected: (_) {},
),
const SizedBox(width: 4.0),
FilterChip(
label: const SizedBox(width: 64),
onSelected: (_) {},
),
const SizedBox(width: 4.0),
FilterChip(
label: const SizedBox(width: 100),
onSelected: (_) {},
),
const SizedBox(width: 4.0),
FilterChip(
label: const SizedBox(width: 32),
onSelected: (_) {},
),
const SizedBox(width: 4.0),
FilterChip(
label: const SizedBox(width: 48),
onSelected: (_) {},
),
],
),
),
);

View File

@@ -24,6 +24,7 @@ import 'package:paperless_mobile/features/documents/view/pages/document_view.dar
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/widgets/grid_image_item_widget.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:path/path.dart' as p;
@@ -140,7 +141,7 @@ class _ScannerPageState extends State<ScannerPage>
final file = await _assembleFileBytes(
context.read<DocumentScannerCubit>().state,
);
final uploaded = await Navigator.of(context).push(
final taskId = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
@@ -165,8 +166,10 @@ class _ScannerPageState extends State<ScannerPage>
),
) ??
false;
if (uploaded) {
if (taskId != null) {
// For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset();
context.read<TaskStatusCubit>().listenToTaskChanges(taskId);
}
}

View File

@@ -7,20 +7,30 @@ class TaskStatusCubit extends Cubit<TaskStatusState> {
final PaperlessTasksApi _api;
TaskStatusCubit(this._api) : super(const TaskStatusState());
void startListeningToTask(String taskId) {
void listenToTaskChanges(String taskId) {
_api
.listenForTaskChanges(taskId)
.forEach(
(element) => TaskStatusState(
isListening: true,
isAcknowledged: false,
task: element,
(element) => emit(
TaskStatusState(
isListening: true,
task: element,
),
),
)
.whenComplete(() => emit(state.copyWith(isListening: false)));
}
void acknowledgeCurrentTask() {
emit(state.copyWith(isListening: false, isAcknowledged: true));
Future<void> acknowledgeCurrentTask() async {
if (state.task == null) {
return;
}
final task = await _api.acknowledgeTask(state.task!);
emit(
state.copyWith(
task: task,
isListening: false,
),
);
}
}

View File

@@ -3,22 +3,20 @@ part of 'task_status_cubit.dart';
class TaskStatusState extends Equatable {
final Task? task;
final bool isListening;
final bool isAcknowledged;
const TaskStatusState({
this.task,
this.isListening = false,
this.isAcknowledged = false,
});
bool get isActive => isListening && !isAcknowledged;
bool get isSuccess => task?.status == TaskStatus.success;
bool get isAcknowledged => task?.acknowledged ?? false;
String? get taskId => task?.taskId;
@override
List<Object> get props => [];
List<Object?> get props => [task, isListening];
TaskStatusState copyWith({
Task? task,
@@ -28,7 +26,6 @@ class TaskStatusState extends Equatable {
return TaskStatusState(
task: task ?? this.task,
isListening: isListening ?? this.isListening,
isAcknowledged: isAcknowledged ?? this.isAcknowledged,
);
}
}

View File

@@ -194,6 +194,8 @@
"@editLabelPageConfirmDeletionDialogTitle": {},
"editLabelPageDeletionDialogText": "Dokumenty mají přiřazen tento štítek. Odstraněním štítku bude označení odstraněno. Pokračovat?",
"@editLabelPageDeletionDialogText": {},
"errorMessageAcknowledgeTasksError": "",
"@errorMessageAcknowledgeTasksError": {},
"errorMessageAuthenticationFailed": "Přihlášení selhalo, zkuste to znovu.",
"@errorMessageAuthenticationFailed": {},
"errorMessageAutocompleteQueryError": "Při automatickém doplnění požadavku došlo k chybě.",
@@ -250,6 +252,8 @@
"@errorMessageStoragePathCreateFailed": {},
"errorMessageStoragePathLoadFailed": "Nelze načíst cestu k úložišti.",
"@errorMessageStoragePathLoadFailed": {},
"errorMessageSuggestionsQueryError": "",
"@errorMessageSuggestionsQueryError": {},
"errorMessageTagCreateFailed": "Nelze vytvořit tag, zkuste to znovu.",
"@errorMessageTagCreateFailed": {},
"errorMessageTagLoadFailed": "Nelze načíst tagy.",
@@ -260,25 +264,25 @@
"@errorMessageUnsupportedFileFormat": {},
"errorReportLabel": "NAHLÁSIT",
"@errorReportLabel": {},
"extendedDateRangeDialogAbsoluteLabel": "Absolute",
"extendedDateRangeDialogAbsoluteLabel": "",
"@extendedDateRangeDialogAbsoluteLabel": {},
"extendedDateRangeDialogHintText": "Hint: Apart from concrete dates, you can also specify a time range relative to the current date.",
"extendedDateRangeDialogHintText": "",
"@extendedDateRangeDialogHintText": {},
"extendedDateRangeDialogRelativeAmountLabel": "Amount",
"extendedDateRangeDialogRelativeAmountLabel": "",
"@extendedDateRangeDialogRelativeAmountLabel": {},
"extendedDateRangeDialogRelativeLabel": "Relative",
"extendedDateRangeDialogRelativeLabel": "",
"@extendedDateRangeDialogRelativeLabel": {},
"extendedDateRangeDialogRelativeLastLabel": "Last",
"extendedDateRangeDialogRelativeLastLabel": "",
"@extendedDateRangeDialogRelativeLastLabel": {},
"extendedDateRangeDialogRelativeTimeUnitLabel": "Time unit",
"extendedDateRangeDialogRelativeTimeUnitLabel": "",
"@extendedDateRangeDialogRelativeTimeUnitLabel": {},
"extendedDateRangeDialogTitle": "Select date range",
"extendedDateRangeDialogTitle": "",
"@extendedDateRangeDialogTitle": {},
"extendedDateRangePickerAfterLabel": "After",
"extendedDateRangePickerAfterLabel": "",
"@extendedDateRangePickerAfterLabel": {},
"extendedDateRangePickerBeforeLabel": "Before",
"extendedDateRangePickerBeforeLabel": "",
"@extendedDateRangePickerBeforeLabel": {},
"extendedDateRangePickerDayText": "{count, plural, zero{} one{day} other{days}}",
"extendedDateRangePickerDayText": "{count, plural, other{}}",
"@extendedDateRangePickerDayText": {
"placeholders": {
"count": {}
@@ -298,9 +302,9 @@
"count": {}
}
},
"extendedDateRangePickerLastText": "Last",
"extendedDateRangePickerLastText": "",
"@extendedDateRangePickerLastText": {},
"extendedDateRangePickerLastWeeksLabel": "{count, plural, zero{} one{Last week} other{Last {count} weeks}}",
"extendedDateRangePickerLastWeeksLabel": "{count, plural, other{}}",
"@extendedDateRangePickerLastWeeksLabel": {
"placeholders": {
"count": {}
@@ -312,7 +316,7 @@
"count": {}
}
},
"extendedDateRangePickerMonthText": "{count, plural, zero{} one{month} other{months}}",
"extendedDateRangePickerMonthText": "{count, plural, other{}}",
"@extendedDateRangePickerMonthText": {
"placeholders": {
"count": {}
@@ -320,13 +324,13 @@
},
"extendedDateRangePickerToLabel": "Do",
"@extendedDateRangePickerToLabel": {},
"extendedDateRangePickerWeekText": "{count, plural, zero{} one{week} other{weeks}}",
"extendedDateRangePickerWeekText": "{count, plural, other{}}",
"@extendedDateRangePickerWeekText": {
"placeholders": {
"count": {}
}
},
"extendedDateRangePickerYearText": "{count, plural, zero{} one{year} other{years}}",
"extendedDateRangePickerYearText": "{count, plural, other{}}",
"@extendedDateRangePickerYearText": {
"placeholders": {
"count": {}
@@ -428,7 +432,7 @@
"@loginPageClientCertificateSettingLabel": {},
"loginPageClientCertificateSettingSelectFileText": "Vybrat soubor...",
"@loginPageClientCertificateSettingSelectFileText": {},
"loginPageContinueLabel": "Continue",
"loginPageContinueLabel": "",
"@loginPageContinueLabel": {},
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Chybná nebo chybějící heslová fráze certifikátu.",
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
@@ -438,27 +442,27 @@
"@loginPagePasswordFieldLabel": {},
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
"@loginPagePasswordValidatorMessageText": {},
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
"loginPageReachabilityInvalidClientCertificateConfigurationText": "",
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
"loginPageReachabilityMissingClientCertificateText": "",
"@loginPageReachabilityMissingClientCertificateText": {},
"loginPageReachabilityNotReachableText": "Could not establish a connection to the server.",
"loginPageReachabilityNotReachableText": "",
"@loginPageReachabilityNotReachableText": {},
"loginPageReachabilitySuccessText": "Connection successfully established.",
"loginPageReachabilitySuccessText": "",
"@loginPageReachabilitySuccessText": {},
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.",
"loginPageReachabilityUnresolvedHostText": "",
"@loginPageReachabilityUnresolvedHostText": {},
"loginPageServerUrlFieldLabel": "'Adresa serveru",
"@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
"loginPageServerUrlValidatorMessageInvalidAddressText": "",
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageSignInButtonLabel": "Sign In",
"loginPageSignInButtonLabel": "",
"@loginPageSignInButtonLabel": {},
"loginPageSignInTitle": "Sign In",
"loginPageSignInTitle": "",
"@loginPageSignInTitle": {},
"loginPageSignInToPrefixText": "Sign in to {serverAddress}",
"loginPageSignInToPrefixText": "",
"@loginPageSignInToPrefixText": {
"placeholders": {
"serverAddress": {}
@@ -476,7 +480,7 @@
"@onboardingDoneButtonLabel": {},
"onboardingNextButtonLabel": "Další",
"@onboardingNextButtonLabel": {},
"receiveSharedFilePermissionDeniedMessage": "Could not access the received file. Please try to open the app before sharing.",
"receiveSharedFilePermissionDeniedMessage": "",
"@receiveSharedFilePermissionDeniedMessage": {},
"referencedDocumentsReadOnlyHintText": "Tento náhled nelze upravovat! Nelze upravovat nebo odstraňovat dokumenty. Bude načteno maximálně 100 odkazovaných dokumentů.",
"@referencedDocumentsReadOnlyHintText": {},
@@ -540,12 +544,12 @@
"@tagInboxTagPropertyLabel": {},
"uploadPageAutomaticallInferredFieldsHintText": "Pokud specifikuješ hodnoty pro tato pole, paperless instance nebude automaticky přiřazovat naučené hodnoty. Pokud mají být tato pole automaticky vyplňována, nevyplňujte zde nic.",
"@uploadPageAutomaticallInferredFieldsHintText": {},
"verifyIdentityPageDescriptionText": "Use the configured biometric factor to authenticate and unlock your documents.",
"verifyIdentityPageDescriptionText": "",
"@verifyIdentityPageDescriptionText": {},
"verifyIdentityPageLogoutButtonLabel": "Disconnect",
"verifyIdentityPageLogoutButtonLabel": "",
"@verifyIdentityPageLogoutButtonLabel": {},
"verifyIdentityPageTitle": "Verify your identity",
"verifyIdentityPageTitle": "",
"@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity",
"verifyIdentityPageVerifyIdentityButtonLabel": "",
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
}

View File

@@ -194,6 +194,8 @@
"@editLabelPageConfirmDeletionDialogTitle": {},
"editLabelPageDeletionDialogText": "Dieser Kennzeichner wird von Dokumenten referenziert. Durch das Löschen dieses Kennzeichners werden alle Referenzen entfernt. Fortfahren?",
"@editLabelPageDeletionDialogText": {},
"errorMessageAcknowledgeTasksError": "Dateiaufgabe konnte nicht verworfen werden.",
"@errorMessageAcknowledgeTasksError": {},
"errorMessageAuthenticationFailed": "Authentifizierung fehlgeschlagen, bitte versuche es erneut.",
"@errorMessageAuthenticationFailed": {},
"errorMessageAutocompleteQueryError": "Beim automatischen Vervollständigen ist ein Fehler aufgetreten.",
@@ -250,6 +252,8 @@
"@errorMessageStoragePathCreateFailed": {},
"errorMessageStoragePathLoadFailed": "Speicherpfade konnten nicht geladen werden.",
"@errorMessageStoragePathLoadFailed": {},
"errorMessageSuggestionsQueryError": "Vorschläge konnten nicht geladen werden.",
"@errorMessageSuggestionsQueryError": {},
"errorMessageTagCreateFailed": "Tag konnte nicht erstellt werden, bitte versuche es erneut.",
"@errorMessageTagCreateFailed": {},
"errorMessageTagLoadFailed": "Tags konnten nicht geladen werden.",

View File

@@ -194,6 +194,8 @@
"@editLabelPageConfirmDeletionDialogTitle": {},
"editLabelPageDeletionDialogText": "This label contains references to other documents. By deleting this label, all references will be removed. Continue?",
"@editLabelPageDeletionDialogText": {},
"errorMessageAcknowledgeTasksError": "Could not acknowledge tasks.",
"@errorMessageAcknowledgeTasksError": {},
"errorMessageAuthenticationFailed": "Authentication failed, please try again.",
"@errorMessageAuthenticationFailed": {},
"errorMessageAutocompleteQueryError": "An error ocurred while trying to autocomplete your query.",
@@ -250,6 +252,8 @@
"@errorMessageStoragePathCreateFailed": {},
"errorMessageStoragePathLoadFailed": "Could not load storage paths.",
"@errorMessageStoragePathLoadFailed": {},
"errorMessageSuggestionsQueryError": "Could not load suggestions.",
"@errorMessageSuggestionsQueryError": {},
"errorMessageTagCreateFailed": "Could not create tag, please try again.",
"@errorMessageTagCreateFailed": {},
"errorMessageTagLoadFailed": "Could not load tags.",

View File

@@ -14,7 +14,6 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
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/interceptor/dio_http_error_interceptor.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
@@ -27,7 +26,7 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi
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/core/security/authentication_aware_dio_manager.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';
@@ -46,7 +45,6 @@ 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.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
@@ -55,7 +53,6 @@ void main() async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
await findSystemLocale();
await LocalNotificationService.instance.initialize();
// Initialize External dependencies
final connectivity = Connectivity();
@@ -78,27 +75,18 @@ void main() async {
final languageHeaderInterceptor = LanguageHeaderInterceptor(
appSettingsCubit.state.preferredLocaleSubtag,
);
// Required for self signed client certificates
final dioWrapper = AuthenticationAwareDioManager([
DioHttpErrorInterceptor(),
PrettyDioLogger(
compact: true,
responseBody: false,
responseHeader: false,
request: false,
requestBody: false,
requestHeader: false,
),
languageHeaderInterceptor,
]);
// Manages security context, required for self signed client certificates
final sessionManager = SessionManager([languageHeaderInterceptor]);
// Initialize Paperless APIs
final authApi = PaperlessAuthenticationApiImpl(dioWrapper.client);
final documentsApi = PaperlessDocumentsApiImpl(dioWrapper.client);
final labelsApi = PaperlessLabelApiImpl(dioWrapper.client);
final statsApi = PaperlessServerStatsApiImpl(dioWrapper.client);
final savedViewsApi = PaperlessSavedViewsApiImpl(dioWrapper.client);
final tasksApi = PaperlessTasksApiImpl(dioWrapper.client);
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
final documentsApi = PaperlessDocumentsApiImpl(sessionManager.client);
final labelsApi = PaperlessLabelApiImpl(sessionManager.client);
final statsApi = PaperlessServerStatsApiImpl(sessionManager.client);
final savedViewsApi = PaperlessSavedViewsApiImpl(sessionManager.client);
final tasksApi = PaperlessTasksApiImpl(
sessionManager.client,
);
// Initialize Blocs/Cubits
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
@@ -119,20 +107,23 @@ void main() async {
final authCubit = AuthenticationCubit(
localAuthService,
authApi,
dioWrapper,
sessionManager,
);
await authCubit
.restoreSessionState(appSettingsCubit.state.isLocalAuthenticationEnabled);
if (authCubit.state.isAuthenticated) {
final auth = authCubit.state.authentication!;
dioWrapper.updateSettings(
sessionManager.updateSettings(
baseUrl: auth.serverUrl,
authToken: auth.token,
clientCertificate: auth.clientCertificate,
);
}
final localNotificationService = LocalNotificationService();
await localNotificationService.initialize();
//Update language header in interceptor on language change.
appSettingsCubit.stream.listen((event) => languageHeaderInterceptor
.preferredLocaleSubtag = event.preferredLocaleSubtag);
@@ -149,7 +140,7 @@ void main() async {
create: (context) => cm.CacheManager(
cm.Config(
'cacheKey',
fileService: DioFileService(dioWrapper.client),
fileService: DioFileService(sessionManager.client),
),
),
),
@@ -157,6 +148,9 @@ void main() async {
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService,
),
Provider<LocalNotificationService>.value(
value: localNotificationService,
),
],
child: MultiRepositoryProvider(
providers: [
@@ -221,6 +215,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
vertical: 16.0,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
chipTheme: ChipThemeData(
backgroundColor: Colors.lightGreen[50],
),
@@ -242,6 +237,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
vertical: 16.0,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
chipTheme: ChipThemeData(
backgroundColor: Colors.green[900],
),
@@ -252,7 +248,9 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => PaperlessServerInformationCubit(context.read()),
create: (context) => PaperlessServerInformationCubit(
context.read<PaperlessServerStatsApi>(),
),
),
],
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
@@ -260,14 +258,8 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: _lightTheme.copyWith(
listTileTheme: _lightTheme.listTileTheme
.copyWith(tileColor: Colors.transparent),
),
darkTheme: _darkTheme.copyWith(
listTileTheme: _darkTheme.listTileTheme
.copyWith(tileColor: Colors.transparent),
),
theme: _lightTheme,
darkTheme: _darkTheme,
themeMode: settings.preferredThemeMode,
supportedLocales: S.delegate.supportedLocales,
locale: Locale.fromSubtags(