mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-17 20:12:32 -06:00
Refactored DI, serialization, added feedback to document download
This commit is contained in:
@@ -4,7 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
@prod
|
||||
@test
|
||||
@lazySingleton
|
||||
class ConnectivityCubit extends Cubit<ConnectivityState> {
|
||||
final ConnectivityStatusService connectivityStatusService;
|
||||
StreamSubscription<bool>? _sub;
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
@singleton
|
||||
@prod
|
||||
@test
|
||||
@lazySingleton
|
||||
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
||||
DocumentStatusCubit() : super(null);
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||
|
||||
@singleton
|
||||
@prod
|
||||
@test
|
||||
@lazySingleton
|
||||
class PaperlessServerInformationCubit
|
||||
extends Cubit<PaperlessServerInformationState> {
|
||||
final PaperlessServerStatsApi service;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
@injectable
|
||||
class AuthenticationInterceptor implements InterceptorContract {
|
||||
final LocalVault _localVault;
|
||||
AuthenticationInterceptor(this._localVault);
|
||||
@@ -20,15 +18,15 @@ class AuthenticationInterceptor implements InterceptorContract {
|
||||
if (kDebugMode) {
|
||||
log("Intercepted ${request.method} request to ${request.url.toString()}");
|
||||
}
|
||||
if (auth == null) {
|
||||
throw const PaperlessServerException(ErrorCode.notAuthenticated);
|
||||
}
|
||||
|
||||
return request.copyWith(
|
||||
//Append server Url
|
||||
url: Uri.parse(auth.serverUrl + request.url.toString()),
|
||||
headers: auth.token.isEmpty
|
||||
headers: auth?.token?.isEmpty ?? true
|
||||
? request.headers
|
||||
: {...request.headers, 'Authorization': 'Token ${auth.token}'},
|
||||
: {
|
||||
...request.headers,
|
||||
'Authorization': 'Token ${auth!.token}',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
28
lib/core/interceptor/base_url_interceptor.dart
Normal file
28
lib/core/interceptor/base_url_interceptor.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||
|
||||
@prod
|
||||
@injectable
|
||||
class BaseUrlInterceptor implements InterceptorContract {
|
||||
final LocalVault _localVault;
|
||||
|
||||
BaseUrlInterceptor(this._localVault);
|
||||
@override
|
||||
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
||||
final auth = await _localVault.loadAuthenticationInformation();
|
||||
if (auth == null) {
|
||||
throw Exception(
|
||||
"Authentication information not available, cannot perform request!",
|
||||
);
|
||||
}
|
||||
return request.copyWith(
|
||||
url: Uri.parse(auth.serverUrl + request.url.toString()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaseResponse> interceptResponse(
|
||||
{required BaseResponse response}) async =>
|
||||
response;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import 'package:injectable/injectable.dart';
|
||||
const interceptedRoutes = ['thumb/'];
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
class ResponseConversionInterceptor implements InterceptorContract {
|
||||
@override
|
||||
|
||||
@@ -12,10 +12,9 @@ import 'package:injectable/injectable.dart';
|
||||
///
|
||||
/// Convenience class which handles timeout errors.
|
||||
///
|
||||
@Injectable(as: BaseClient)
|
||||
@dev
|
||||
@prod
|
||||
@Named("timeoutClient")
|
||||
@Injectable(as: BaseClient)
|
||||
class TimeoutClient implements BaseClient {
|
||||
final ConnectivityStatusService connectivityStatusService;
|
||||
static const Duration requestTimeout = Duration(seconds: 25);
|
||||
|
||||
@@ -9,7 +9,8 @@ abstract class ConnectivityStatusService {
|
||||
Stream<bool> connectivityChanges();
|
||||
}
|
||||
|
||||
@Injectable(as: ConnectivityStatusService, env: ['prod', 'dev'])
|
||||
@prod
|
||||
@Injectable(as: ConnectivityStatusService)
|
||||
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
|
||||
final Connectivity connectivity;
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class FileService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Directory?> get downloadsDirectory async {
|
||||
static Future<Directory> get downloadsDirectory async {
|
||||
if (Platform.isAndroid) {
|
||||
return (await getExternalStorageDirectories(
|
||||
type: StorageDirectory.downloads))!
|
||||
|
||||
@@ -17,9 +17,8 @@ abstract class LocalVault {
|
||||
Future<void> clear();
|
||||
}
|
||||
|
||||
@Injectable(as: LocalVault)
|
||||
@prod
|
||||
@dev
|
||||
@Injectable(as: LocalVault)
|
||||
class LocalVaultImpl implements LocalVault {
|
||||
static const applicationSettingsKey = "applicationSettings";
|
||||
static const authenticationKey = "authentication";
|
||||
|
||||
@@ -2,18 +2,21 @@ import 'dart:io';
|
||||
|
||||
import 'package:paperless_mobile/di_initializer.config.dart';
|
||||
import 'package:paperless_mobile/di_modules.dart';
|
||||
import 'package:paperless_mobile/di_paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
final getIt = GetIt.instance..allowReassignment;
|
||||
|
||||
@InjectableInit(
|
||||
initializerName: r'$initGetIt', // default
|
||||
initializerName: 'init', // default
|
||||
preferRelativeImports: true, // default
|
||||
asExtension: false, // default
|
||||
includeMicroPackages: false,
|
||||
)
|
||||
void configureDependencies(String environment) =>
|
||||
$initGetIt(getIt, environment: environment);
|
||||
init(getIt, environment: environment);
|
||||
|
||||
///
|
||||
/// Registers new security context, which will be used by the HttpClient, see [RegisterModule].
|
||||
|
||||
@@ -2,10 +2,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/authentication.interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/base_url_interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart';
|
||||
import 'package:http/http.dart';
|
||||
@@ -16,34 +15,30 @@ import 'package:local_auth/local_auth.dart';
|
||||
|
||||
@module
|
||||
abstract class RegisterModule {
|
||||
@singleton
|
||||
@dev
|
||||
@prod
|
||||
@singleton
|
||||
LocalAuthentication get localAuthentication => LocalAuthentication();
|
||||
|
||||
@singleton
|
||||
@dev
|
||||
@prod
|
||||
@singleton
|
||||
EncryptedSharedPreferences get encryptedSharedPreferences =>
|
||||
EncryptedSharedPreferences();
|
||||
|
||||
@singleton
|
||||
@dev
|
||||
@prod
|
||||
@test
|
||||
@singleton
|
||||
@Order(-1)
|
||||
SecurityContext get securityContext => SecurityContext();
|
||||
|
||||
@singleton
|
||||
@dev
|
||||
@prod
|
||||
@singleton
|
||||
Connectivity get connectivity => Connectivity();
|
||||
|
||||
///
|
||||
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
|
||||
///
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
@Order(-1)
|
||||
HttpClient getHttpClient(SecurityContext securityContext) =>
|
||||
HttpClient(context: securityContext)
|
||||
..connectionTimeout = const Duration(seconds: 10);
|
||||
@@ -51,66 +46,26 @@ abstract class RegisterModule {
|
||||
///
|
||||
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
|
||||
///
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
@Order(-1)
|
||||
BaseClient getBaseClient(
|
||||
AuthenticationInterceptor authInterceptor,
|
||||
ResponseConversionInterceptor responseConversionInterceptor,
|
||||
LanguageHeaderInterceptor languageHeaderInterceptor,
|
||||
BaseUrlInterceptor baseUrlInterceptor,
|
||||
HttpClient client,
|
||||
) =>
|
||||
InterceptedClient.build(
|
||||
interceptors: [
|
||||
baseUrlInterceptor,
|
||||
authInterceptor,
|
||||
responseConversionInterceptor,
|
||||
languageHeaderInterceptor
|
||||
languageHeaderInterceptor,
|
||||
],
|
||||
client: IOClient(client),
|
||||
);
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
CacheManager getCacheManager(BaseClient client) => CacheManager(
|
||||
Config('cacheKey', fileService: HttpFileService(httpClient: client)));
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
PaperlessAuthenticationApi authenticationModule(BaseClient client) =>
|
||||
PaperlessAuthenticationApiImpl(client);
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
PaperlessLabelsApi labelsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
) =>
|
||||
PaperlessLabelApiImpl(timeoutClient);
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
PaperlessDocumentsApi documentsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
HttpClient httpClient,
|
||||
) =>
|
||||
PaperlessDocumentsApiImpl(timeoutClient, httpClient);
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
PaperlessSavedViewsApi savedViewsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
) =>
|
||||
PaperlessSavedViewsApiImpl(timeoutClient);
|
||||
|
||||
@injectable
|
||||
@dev
|
||||
@prod
|
||||
PaperlessServerStatsApi serverStatsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
) =>
|
||||
PaperlessServerStatsApiImpl(timeoutClient);
|
||||
}
|
||||
|
||||
47
lib/di_paperless_api.dart
Normal file
47
lib/di_paperless_api.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
@module
|
||||
abstract class PaperlessApiModule {
|
||||
@prod
|
||||
@Order(-1)
|
||||
@injectable
|
||||
PaperlessAuthenticationApi authenticationModule(BaseClient client) =>
|
||||
PaperlessAuthenticationApiImpl(client);
|
||||
|
||||
@prod
|
||||
@Order(-1)
|
||||
@injectable
|
||||
PaperlessLabelsApi labelsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
) =>
|
||||
PaperlessLabelApiImpl(timeoutClient);
|
||||
|
||||
@prod
|
||||
@Order(-1)
|
||||
@injectable
|
||||
PaperlessDocumentsApi documentsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
HttpClient httpClient,
|
||||
) =>
|
||||
PaperlessDocumentsApiImpl(timeoutClient, httpClient);
|
||||
|
||||
@prod
|
||||
@Order(-1)
|
||||
@injectable
|
||||
PaperlessSavedViewsApi savedViewsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
) =>
|
||||
PaperlessSavedViewsApiImpl(timeoutClient);
|
||||
|
||||
@prod
|
||||
@Order(-1)
|
||||
@injectable
|
||||
PaperlessServerStatsApi serverStatsModule(
|
||||
@Named('timeoutClient') BaseClient timeoutClient,
|
||||
) =>
|
||||
PaperlessServerStatsApiImpl(timeoutClient);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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!))
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 ??
|
||||
|
||||
@@ -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;
|
||||
@@ -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: {}));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"@documentDetailsPageTabOverviewLabel": {},
|
||||
"documentDocumentTypePropertyLabel": "Typ dokumentu",
|
||||
"@documentDocumentTypePropertyLabel": {},
|
||||
"documentDownloadSuccessMessage": "Document successfully downloaded.",
|
||||
"@documentDownloadSuccessMessage": {},
|
||||
"documentEditPageTitle": "Upravit dokument",
|
||||
"@documentEditPageTitle": {},
|
||||
"documentMetaDataChecksumLabel": "MD5 součet originálu",
|
||||
@@ -270,6 +272,10 @@
|
||||
"@inboxPageMarkAllAsSeenLabel": {},
|
||||
"inboxPageMarkAsSeenText": "Označit jako shlédnuté",
|
||||
"@inboxPageMarkAsSeenText": {},
|
||||
"inboxPageNoNewDocumentsRefreshLabel": "Refresh",
|
||||
"@inboxPageNoNewDocumentsRefreshLabel": {},
|
||||
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
|
||||
"@inboxPageNoNewDocumentsText": {},
|
||||
"inboxPageTodayText": "Dnes",
|
||||
"@inboxPageTodayText": {},
|
||||
"inboxPageUndoRemoveText": "VRÁTIT",
|
||||
@@ -415,7 +421,5 @@
|
||||
"tagInboxTagPropertyLabel": "Tag inboxu",
|
||||
"@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": {},
|
||||
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
|
||||
"inboxPageNoNewDocumentsRefreshLabel": "Refresh"
|
||||
"@uploadPageAutomaticallInferredFieldsHintText": {}
|
||||
}
|
||||
@@ -70,6 +70,8 @@
|
||||
"@documentDetailsPageTabOverviewLabel": {},
|
||||
"documentDocumentTypePropertyLabel": "Dokumenttyp",
|
||||
"@documentDocumentTypePropertyLabel": {},
|
||||
"documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.",
|
||||
"@documentDownloadSuccessMessage": {},
|
||||
"documentEditPageTitle": "Dokument Bearbeiten",
|
||||
"@documentEditPageTitle": {},
|
||||
"documentMetaDataChecksumLabel": "MD5-Prüfsumme Original",
|
||||
@@ -136,13 +138,13 @@
|
||||
"@documentsPageEmptyStateOopsText": {},
|
||||
"documentsPageOrderByLabel": "Sortiere nach",
|
||||
"@documentsPageOrderByLabel": {},
|
||||
"documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchten Sie trotzdem fortfahren?",
|
||||
"documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchtest Du trotzdem fortfahren?",
|
||||
"@documentsPageSelectionBulkDeleteDialogContinueText": {},
|
||||
"documentsPageSelectionBulkDeleteDialogTitle": "Löschen bestätigen",
|
||||
"@documentsPageSelectionBulkDeleteDialogTitle": {},
|
||||
"documentsPageSelectionBulkDeleteDialogWarningTextMany": "Sind Sie sicher, dass sie folgende Dokumente löschen wollen?",
|
||||
"documentsPageSelectionBulkDeleteDialogWarningTextMany": "Bist Du sicher, dass Du folgende Dokumente löschen möchtest?",
|
||||
"@documentsPageSelectionBulkDeleteDialogWarningTextMany": {},
|
||||
"documentsPageSelectionBulkDeleteDialogWarningTextOne": "Sind Sie sicher, dass sie folgendes Dokument löschen wollen?",
|
||||
"documentsPageSelectionBulkDeleteDialogWarningTextOne": "Bist Du sicher, dass Du folgendes Dokument löschen möchtest?",
|
||||
"@documentsPageSelectionBulkDeleteDialogWarningTextOne": {},
|
||||
"documentsPageTitle": "Dokumente",
|
||||
"@documentsPageTitle": {},
|
||||
@@ -190,7 +192,7 @@
|
||||
"@errorMessageCorrespondentLoadFailed": {},
|
||||
"errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.",
|
||||
"@errorMessageCreateSavedViewError": {},
|
||||
"errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht gelöscht werden, bitte versuche es erneut.",
|
||||
"errorMessageDeleteSavedViewError": "Gespeicherte Ansicht konnte nicht geklöscht werden, bitte versuche es erneut.",
|
||||
"@errorMessageDeleteSavedViewError": {},
|
||||
"errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.",
|
||||
"@errorMessageDeviceOffline": {},
|
||||
@@ -262,14 +264,18 @@
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.",
|
||||
"@inboxPageDocumentRemovedMessageText": {},
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogText": "Sind Sie sicher, dass Sie alle Dokumente als gesehen markieren möchten? Dadurch wird eine Massenbearbeitung durchgeführt, bei der alle Posteingangs-Tags von den Dokumenten entfernt werden.\nDiese Aktion kann nicht rückgängig gemacht werden! Möchten Sie trotzdem fortfahren?",
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogText": "Bist Du sicher, dass Du alle Dokumente als gesehen markieren möchtest? Dadurch wird eine Massenbearbeitung durchgeführt, bei der alle Posteingangs-Tags von den Dokumenten entfernt werden.\\nDiese Aktion kann nicht rückgängig gemacht werden! Möchtest Du trotzdem fortfahren?",
|
||||
"@inboxPageMarkAllAsSeenConfirmationDialogText": {},
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Alle als gesehen markieren?",
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Alle als gelesen markieren?",
|
||||
"@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {},
|
||||
"inboxPageMarkAllAsSeenLabel": "Alle als gesehen markieren",
|
||||
"inboxPageMarkAllAsSeenLabel": "Alle als gelesen markieren",
|
||||
"@inboxPageMarkAllAsSeenLabel": {},
|
||||
"inboxPageMarkAsSeenText": "Als gesehen markieren",
|
||||
"inboxPageMarkAsSeenText": "Als gelesen markieren",
|
||||
"@inboxPageMarkAsSeenText": {},
|
||||
"inboxPageNoNewDocumentsRefreshLabel": "Neu laden",
|
||||
"@inboxPageNoNewDocumentsRefreshLabel": {},
|
||||
"inboxPageNoNewDocumentsText": "Du hast keine ungesehenen Dokumente.",
|
||||
"@inboxPageNoNewDocumentsText": {},
|
||||
"inboxPageTodayText": "Heute",
|
||||
"@inboxPageTodayText": {},
|
||||
"inboxPageUndoRemoveText": "UNDO",
|
||||
@@ -415,7 +421,5 @@
|
||||
"tagInboxTagPropertyLabel": "Posteingangs-Tag",
|
||||
"@tagInboxTagPropertyLabel": {},
|
||||
"uploadPageAutomaticallInferredFieldsHintText": "Wenn Werte für diese Felder angegeben werden, wird Paperless nicht automatisch einen Wert zuweisen. Wenn diese Felder automatisch von Paperless erkannt werden sollen, sollten die Felder leer bleiben.",
|
||||
"@uploadPageAutomaticallInferredFieldsHintText": {},
|
||||
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
|
||||
"inboxPageNoNewDocumentsRefreshLabel": "Refresh"
|
||||
"@uploadPageAutomaticallInferredFieldsHintText": {}
|
||||
}
|
||||
@@ -70,6 +70,8 @@
|
||||
"@documentDetailsPageTabOverviewLabel": {},
|
||||
"documentDocumentTypePropertyLabel": "Document Type",
|
||||
"@documentDocumentTypePropertyLabel": {},
|
||||
"documentDownloadSuccessMessage": "Document successfully downloaded.",
|
||||
"@documentDownloadSuccessMessage": {},
|
||||
"documentEditPageTitle": "Edit Document",
|
||||
"@documentEditPageTitle": {},
|
||||
"documentMetaDataChecksumLabel": "Original MD5-Checksum",
|
||||
@@ -262,7 +264,7 @@
|
||||
"@genericMessageOfflineText": {},
|
||||
"inboxPageDocumentRemovedMessageText": "Document removed from inbox.",
|
||||
"@inboxPageDocumentRemovedMessageText": {},
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogText": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents.\nThis action is not reversible! Are you sure you want to continue?",
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogText": "Are you sure you want to mark all documents as seen? This will perform a bulk edit operation removing all inbox tags from the documents.\\nThis action is not reversible! Are you sure you want to continue?",
|
||||
"@inboxPageMarkAllAsSeenConfirmationDialogText": {},
|
||||
"inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Mark all as seen?",
|
||||
"@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {},
|
||||
@@ -270,6 +272,10 @@
|
||||
"@inboxPageMarkAllAsSeenLabel": {},
|
||||
"inboxPageMarkAsSeenText": "Mark as seen",
|
||||
"@inboxPageMarkAsSeenText": {},
|
||||
"inboxPageNoNewDocumentsRefreshLabel": "Refresh",
|
||||
"@inboxPageNoNewDocumentsRefreshLabel": {},
|
||||
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
|
||||
"@inboxPageNoNewDocumentsText": {},
|
||||
"inboxPageTodayText": "Today",
|
||||
"@inboxPageTodayText": {},
|
||||
"inboxPageUndoRemoveText": "UNDO",
|
||||
@@ -415,7 +421,5 @@
|
||||
"tagInboxTagPropertyLabel": "Inbox-Tag",
|
||||
"@tagInboxTagPropertyLabel": {},
|
||||
"uploadPageAutomaticallInferredFieldsHintText": "If you specify values for these fields, your paperless instance will not automatically derive a value. If you want these values to be automatically populated by your server, leave the fields blank.",
|
||||
"@uploadPageAutomaticallInferredFieldsHintText": {},
|
||||
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
|
||||
"inboxPageNoNewDocumentsRefreshLabel": "Refresh"
|
||||
"@uploadPageAutomaticallInferredFieldsHintText": {}
|
||||
}
|
||||
@@ -32,8 +32,9 @@ import 'package:paperless_mobile/features/settings/model/application_settings_st
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
|
||||
Future<void> startAppProd() async {
|
||||
void main() async {
|
||||
Bloc.observer = BlocChangesObserver();
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
@@ -54,10 +55,6 @@ Future<void> startAppProd() async {
|
||||
runApp(const PaperlessMobileEntrypoint());
|
||||
}
|
||||
|
||||
void main() async {
|
||||
await startAppProd();
|
||||
}
|
||||
|
||||
class PaperlessMobileEntrypoint extends StatefulWidget {
|
||||
const PaperlessMobileEntrypoint({Key? key}) : super(key: key);
|
||||
|
||||
@@ -71,10 +68,18 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ConnectivityCubit>()),
|
||||
BlocProvider.value(value: getIt<AuthenticationCubit>()),
|
||||
BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()),
|
||||
BlocProvider.value(value: getIt<ApplicationSettingsCubit>()),
|
||||
BlocProvider<ConnectivityCubit>.value(
|
||||
value: getIt<ConnectivityCubit>(),
|
||||
),
|
||||
BlocProvider<AuthenticationCubit>.value(
|
||||
value: getIt<AuthenticationCubit>(),
|
||||
),
|
||||
BlocProvider<PaperlessServerInformationCubit>.value(
|
||||
value: getIt<PaperlessServerInformationCubit>(),
|
||||
),
|
||||
BlocProvider<ApplicationSettingsCubit>.value(
|
||||
value: getIt<ApplicationSettingsCubit>(),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
builder: (context, settings) {
|
||||
@@ -234,6 +239,10 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
child: const HomePage(),
|
||||
);
|
||||
} else {
|
||||
if (authentication.wasLoginStored &&
|
||||
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
|
||||
return BiometricAuthenticationPage();
|
||||
}
|
||||
return const LoginPage();
|
||||
}
|
||||
},
|
||||
@@ -241,3 +250,43 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BiometricAuthenticationPage extends StatelessWidget {
|
||||
const BiometricAuthenticationPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"The app is locked!",
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
"You can now either try to authenticate again or disconnect from the current server.",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
).padded(),
|
||||
const SizedBox(height: 48),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
BlocProvider.of<AuthenticationCubit>(context).logout(),
|
||||
child: Text("Log out"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => BlocProvider.of<AuthenticationCubit>(context)
|
||||
.restoreSessionState(),
|
||||
child: Text("Authenticate"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,17 @@ void showSnackBar(
|
||||
String? details,
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
message + (details != null ? ' ($details)' : ''),
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
message + (details != null ? ' ($details)' : ''),
|
||||
),
|
||||
action: action,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
action: action,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
void showGenericError(
|
||||
|
||||
Reference in New Issue
Block a user