Refactored DI, serialization, added feedback to document download

This commit is contained in:
Anton Stubenbord
2022-12-06 00:39:18 +01:00
parent d79682a011
commit 75fa2f7713
51 changed files with 711 additions and 366 deletions

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/services/authentication.service.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.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/util.dart';

View File

@@ -12,6 +12,7 @@ import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/di_initializer.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/widgets/document_download_button.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_edit_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
@@ -82,12 +83,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
? () => _onDelete(state.document!)
: null,
).padded(const EdgeInsets.symmetric(horizontal: 4)),
IconButton(
icon: const Icon(Icons.download),
onPressed: Platform.isAndroid && state.document != null
? () => _onDownload(state.document!)
: null,
).padded(const EdgeInsets.only(right: 4)),
DocumentDownloadButton(
document: state.document,
),
IconButton(
icon: const Icon(Icons.open_in_new),
onPressed: state.document != null
@@ -404,25 +402,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return const SizedBox(height: 32.0);
}
Future<void> _onDownload(DocumentModel document) async {
if (!Platform.isAndroid) {
showSnackBar(
context, "This feature is currently only supported on Android!");
return;
}
setState(() => _isDownloadPending = true);
getIt<PaperlessDocumentsApi>().download(document).then((bytes) async {
final Directory dir = (await getExternalStorageDirectories(
type: StorageDirectory.downloads))!
.first;
String filePath = "${dir.path}/${document.originalFileName}";
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
await File(filePath).writeAsBytes(bytes);
setState(() => _isDownloadPending = false);
dev.log("File downloaded to $filePath");
});
}
///
/// Downloads file to temporary directory, from which it can then be shared.
///

View File

@@ -0,0 +1,61 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
class DocumentDownloadButton extends StatefulWidget {
final DocumentModel? document;
const DocumentDownloadButton({super.key, required this.document});
@override
State<DocumentDownloadButton> createState() => _DocumentDownloadButtonState();
}
class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
bool _isDownloadPending = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: _isDownloadPending
? const SizedBox(
child: CircularProgressIndicator(),
height: 16,
width: 16,
)
: const Icon(Icons.download),
onPressed: Platform.isAndroid && widget.document != null
? () => _onDownload(widget.document!)
: null,
).padded(const EdgeInsets.only(right: 4));
}
Future<void> _onDownload(DocumentModel document) async {
if (!Platform.isAndroid) {
showSnackBar(
context, "This feature is currently only supported on Android!");
return;
}
setState(() => _isDownloadPending = true);
try {
final bytes = await getIt<PaperlessDocumentsApi>().download(document);
final Directory dir = await FileService.downloadsDirectory;
String filePath = "${dir.path}/${document.originalFileName}";
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
await File(filePath).writeAsBytes(bytes);
showSnackBar(context, S.of(context).documentDownloadSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} catch (error) {
showGenericError(context, error);
} finally {
setState(() => _isDownloadPending = false);
}
}
}

View File

@@ -1,11 +1,11 @@
import 'dart:typed_data';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
@singleton
@prod
@test
@lazySingleton
class DocumentsCubit extends Cubit<DocumentsState> {
final PaperlessDocumentsApi _api;

View File

@@ -64,7 +64,6 @@ class _SortFieldSelectionBottomSheetState
contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text(
_localizedSortField(field),
style: Theme.of(context).textTheme.bodyText2,
),
trailing: isNextSelected
? (_buildOrderIcon(_selectedOrderLoading!))

View File

@@ -2,7 +2,9 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart';
@singleton
@prod
@test
@lazySingleton
class CorrespondentCubit extends LabelCubit<Correspondent> {
CorrespondentCubit(super.metaDataService);

View File

@@ -2,7 +2,9 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart';
@singleton
@prod
@test
@lazySingleton
class DocumentTypeCubit extends LabelCubit<DocumentType> {
DocumentTypeCubit(super.metaDataService);

View File

@@ -2,7 +2,9 @@ import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
@singleton
@prod
@test
@lazySingleton
class StoragePathCubit extends LabelCubit<StoragePath> {
StoragePathCubit(super.metaDataService);

View File

@@ -2,7 +2,9 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart';
@singleton
@prod
@test
@lazySingleton
class TagCubit extends LabelCubit<Tag> {
TagCubit(super.metaDataService);

View File

@@ -159,7 +159,7 @@ class _TagFormFieldState extends State<TagFormField> {
(query) => _buildTag(
field,
query,
tagState.getLabel(query.id)!,
tagState.getLabel(query.id),
),
)
.toList(),
@@ -235,11 +235,13 @@ class _TagFormFieldState extends State<TagFormField> {
Widget _buildTag(
FormFieldState<TagsQuery> field,
TagIdQuery query,
Tag tag,
Tag? tag,
) {
final currentQuery = field.value as IdsTagsQuery;
final isIncludedTag = currentQuery.includedIds.contains(query.id);
if (tag == null) {
return Container();
}
return InputChip(
label: Text(
tag.name,

View File

@@ -120,11 +120,15 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
child: Text(S.of(context).genericActionCancelLabel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
widget.onDelete(widget.label);
},
child: Text(S.of(context).genericActionDeleteLabel)),
onPressed: () {
Navigator.pop(context);
widget.onDelete(widget.label);
},
child: Text(
S.of(context).genericActionDeleteLabel,
style: TextStyle(color: Theme.of(context).errorColor),
),
),
],
),
);

View File

@@ -8,19 +8,19 @@ import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
import 'package:paperless_mobile/features/login/services/authentication.service.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
const authenticationKey = "authentication";
@prod
@test
@singleton
class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi;
final LocalVault localStore;
final LocalVault _localVault;
AuthenticationCubit(
this.localStore,
this._localVault,
this._localAuthService,
this._authApi,
) : super(AuthenticationState.initial);
@@ -37,33 +37,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
assert(credentials.username != null && credentials.password != null);
try {
registerSecurityContext(clientCertificate);
emit(
AuthenticationState(
isAuthenticated: false,
wasLoginStored: false,
authentication: AuthenticationInformation(
username: credentials.username!,
password: credentials.password!,
serverUrl: serverUrl,
token: "",
clientCertificate: clientCertificate,
),
),
);
final token = await _authApi.login(
username: credentials.username!,
password: credentials.password!,
serverUrl: serverUrl,
);
final auth = AuthenticationInformation(
username: credentials.username!,
password: credentials.password!,
token: token,
// Store information required to make requests
final currentAuth = AuthenticationInformation(
serverUrl: serverUrl,
clientCertificate: clientCertificate,
);
await _localVault.storeAuthenticationInformation(currentAuth);
await localStore.storeAuthenticationInformation(auth);
final token = await _authApi.login(
username: credentials.username!,
password: credentials.password!,
);
final auth = currentAuth.copyWith(token: token);
await _localVault.storeAuthenticationInformation(auth);
emit(AuthenticationState(
isAuthenticated: true,
@@ -84,10 +72,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
Future<void> restoreSessionState() async {
final storedAuth = await localStore.loadAuthenticationInformation();
final storedAuth = await _localVault.loadAuthenticationInformation();
late ApplicationSettingsState? appSettings;
try {
appSettings = await localStore.loadApplicationSettings() ??
appSettings = await _localVault.loadApplicationSettings() ??
ApplicationSettingsState.defaultSettings;
} catch (err) {
appSettings = ApplicationSettingsState.defaultSettings;
@@ -95,31 +83,40 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
if (storedAuth == null || !storedAuth.isValid) {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false));
} else {
if (!appSettings.isLocalAuthenticationEnabled ||
await _localAuthService
.authenticateLocalUser("Authenticate to log back in")) {
registerSecurityContext(storedAuth.clientCertificate);
emit(
AuthenticationState(
isAuthenticated: true,
if (appSettings.isLocalAuthenticationEnabled) {
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in");
if (localAuthSuccess) {
registerSecurityContext(storedAuth.clientCertificate);
return emit(
AuthenticationState(
isAuthenticated: true,
wasLoginStored: true,
authentication: storedAuth,
wasLocalAuthenticationSuccessful: true,
),
);
} else {
return emit(AuthenticationState(
isAuthenticated: false,
wasLoginStored: true,
authentication: storedAuth,
),
);
} else {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
wasLocalAuthenticationSuccessful: false,
));
}
}
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
}
}
Future<void> logout() async {
await localStore.clear();
await _localVault.clear();
emit(AuthenticationState.initial);
}
}
class AuthenticationState {
final bool wasLoginStored;
final bool? wasLocalAuthenticationSuccessful;
final bool isAuthenticated;
final AuthenticationInformation? authentication;
@@ -131,6 +128,7 @@ class AuthenticationState {
AuthenticationState({
required this.isAuthenticated,
required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
this.authentication,
});
@@ -138,11 +136,14 @@ class AuthenticationState {
bool? wasLoginStored,
bool? isAuthenticated,
AuthenticationInformation? authentication,
bool? wasLocalAuthenticationSuccessful,
}) {
return AuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
wasLoginStored: wasLoginStored ?? this.wasLoginStored,
authentication: authentication ?? this.authentication,
wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ??
this.wasLocalAuthenticationSuccessful,
);
}
}

View File

@@ -1,25 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:local_auth/local_auth.dart';
class LocalAuthenticationCubit extends Cubit<LocalAuthenticationState> {
LocalAuthenticationCubit() : super(LocalAuthenticationState(false));
Future<void> authorize(String localizedMessage) async {
final isAuthenticationSuccessful = await getIt<LocalAuthentication>()
.authenticate(localizedReason: localizedMessage);
if (isAuthenticationSuccessful) {
emit(LocalAuthenticationState(true));
} else {
throw const PaperlessServerException(
ErrorCode.biometricAuthenticationFailed);
}
}
}
class LocalAuthenticationState {
final bool isAuthorized;
LocalAuthenticationState(this.isAuthorized);
}

View File

@@ -2,30 +2,22 @@ import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
class AuthenticationInformation {
static const usernameKey = 'username';
static const passwordKey = 'password';
static const tokenKey = 'token';
static const serverUrlKey = 'serverUrl';
static const clientCertificateKey = 'clientCertificate';
final String username;
final String password;
final String token;
final String? token;
final String serverUrl;
final ClientCertificate? clientCertificate;
AuthenticationInformation({
required this.username,
required this.password,
required this.token,
this.token,
required this.serverUrl,
this.clientCertificate,
});
AuthenticationInformation.fromJson(JSON json)
: username = json[usernameKey],
password = json[passwordKey],
token = json[tokenKey],
: token = json[tokenKey],
serverUrl = json[serverUrlKey],
clientCertificate = json[clientCertificateKey] != null
? ClientCertificate.fromJson(json[clientCertificateKey])
@@ -33,8 +25,6 @@ class AuthenticationInformation {
JSON toJson() {
return {
usernameKey: username,
passwordKey: password,
tokenKey: token,
serverUrlKey: serverUrl,
clientCertificateKey: clientCertificate?.toJson(),
@@ -42,21 +32,16 @@ class AuthenticationInformation {
}
bool get isValid {
return serverUrl.isNotEmpty && token.isNotEmpty;
return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false);
}
AuthenticationInformation copyWith({
String? username,
String? password,
String? token,
String? serverUrl,
ClientCertificate? clientCertificate,
bool removeClientCertificate = false,
bool? isLocalAuthenticationEnabled,
}) {
return AuthenticationInformation(
username: username ?? this.username,
password: password ?? this.password,
token: token ?? this.token,
serverUrl: serverUrl ?? this.serverUrl,
clientCertificate: clientCertificate ??

View File

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

View File

@@ -3,7 +3,9 @@ import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/saved_view/bloc/saved_view_state.dart';
@singleton
@prod
@test
@lazySingleton
class SavedViewCubit extends Cubit<SavedViewState> {
final PaperlessSavedViewsApi _api;
SavedViewCubit(this._api) : super(SavedViewState(value: {}));

View File

@@ -67,7 +67,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
correspondent: correspondent,
tags: tags,
createdAt: createdAt,
authToken: auth.token,
authToken: auth.token!,
serverUrl: auth.serverUrl,
);
if (onConsumptionFinished != null) {

View File

@@ -5,7 +5,9 @@ import 'package:paperless_mobile/features/settings/model/application_settings_st
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@singleton
@prod
@test
@lazySingleton
class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
final LocalVault localVault;

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/services/authentication.service.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.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/generated/l10n.dart';