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

@@ -45,7 +45,6 @@ void main() async {
when(getIt<PaperlessAuthenticationApi>().login( when(getIt<PaperlessAuthenticationApi>().login(
username: testUsername, username: testUsername,
password: testPassword, password: testPassword,
serverUrl: testServerUrl,
)).thenAnswer((i) => Future.value("eyTestToken")); )).thenAnswer((i) => Future.value("eyTestToken"));
await getIt<ConnectivityCubit>().initialize(); await getIt<ConnectivityCubit>().initialize();
@@ -74,7 +73,6 @@ void main() async {
verify(getIt<PaperlessAuthenticationApi>().login( verify(getIt<PaperlessAuthenticationApi>().login(
username: testUsername, username: testUsername,
password: testPassword, password: testPassword,
serverUrl: testServerUrl,
)).called(1); )).called(1);
}); });
@@ -125,7 +123,6 @@ void main() async {
.login( .login(
username: testUsername, username: testUsername,
password: testPassword, password: testPassword,
serverUrl: testServerUrl,
)); ));
expect( expect(
find.textContaining(t.translations.loginPagePasswordValidatorMessageText), find.textContaining(t.translations.loginPagePasswordValidatorMessageText),
@@ -175,7 +172,6 @@ void main() async {
.login( .login(
username: testUsername, username: testUsername,
password: testPassword, password: testPassword,
serverUrl: testServerUrl,
)); ));
expect( expect(
find.textContaining(t.translations.loginPageUsernameValidatorMessageText), find.textContaining(t.translations.loginPageUsernameValidatorMessageText),
@@ -224,7 +220,6 @@ void main() async {
verifyNever(getIt<PaperlessAuthenticationApi>().login( verifyNever(getIt<PaperlessAuthenticationApi>().login(
username: testUsername, username: testUsername,
password: testPassword, password: testPassword,
serverUrl: testServerUrl,
)); ));
expect( expect(
find.textContaining( find.textContaining(

View File

@@ -4,7 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart'; import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
@singleton @prod
@test
@lazySingleton
class ConnectivityCubit extends Cubit<ConnectivityState> { class ConnectivityCubit extends Cubit<ConnectivityState> {
final ConnectivityStatusService connectivityStatusService; final ConnectivityStatusService connectivityStatusService;
StreamSubscription<bool>? _sub; StreamSubscription<bool>? _sub;

View File

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

View File

@@ -3,7 +3,9 @@ import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart'; import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
@singleton @prod
@test
@lazySingleton
class PaperlessServerInformationCubit class PaperlessServerInformationCubit
extends Cubit<PaperlessServerInformationState> { extends Cubit<PaperlessServerInformationState> {
final PaperlessServerStatsApi service; final PaperlessServerStatsApi service;

View File

@@ -1,14 +1,12 @@
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/foundation.dart'; 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:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
@injectable
@dev
@prod @prod
@injectable
class AuthenticationInterceptor implements InterceptorContract { class AuthenticationInterceptor implements InterceptorContract {
final LocalVault _localVault; final LocalVault _localVault;
AuthenticationInterceptor(this._localVault); AuthenticationInterceptor(this._localVault);
@@ -20,15 +18,15 @@ class AuthenticationInterceptor implements InterceptorContract {
if (kDebugMode) { if (kDebugMode) {
log("Intercepted ${request.method} request to ${request.url.toString()}"); log("Intercepted ${request.method} request to ${request.url.toString()}");
} }
if (auth == null) {
throw const PaperlessServerException(ErrorCode.notAuthenticated);
}
return request.copyWith( return request.copyWith(
//Append server Url //Append server Url
url: Uri.parse(auth.serverUrl + request.url.toString()), headers: auth?.token?.isEmpty ?? true
headers: auth.token.isEmpty
? request.headers ? request.headers
: {...request.headers, 'Authorization': 'Token ${auth.token}'}, : {
...request.headers,
'Authorization': 'Token ${auth!.token}',
},
); );
} }

View 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;
}

View File

@@ -5,7 +5,6 @@ import 'package:injectable/injectable.dart';
const interceptedRoutes = ['thumb/']; const interceptedRoutes = ['thumb/'];
@injectable @injectable
@dev
@prod @prod
class ResponseConversionInterceptor implements InterceptorContract { class ResponseConversionInterceptor implements InterceptorContract {
@override @override

View File

@@ -12,10 +12,9 @@ import 'package:injectable/injectable.dart';
/// ///
/// Convenience class which handles timeout errors. /// Convenience class which handles timeout errors.
/// ///
@Injectable(as: BaseClient)
@dev
@prod @prod
@Named("timeoutClient") @Named("timeoutClient")
@Injectable(as: BaseClient)
class TimeoutClient implements BaseClient { class TimeoutClient implements BaseClient {
final ConnectivityStatusService connectivityStatusService; final ConnectivityStatusService connectivityStatusService;
static const Duration requestTimeout = Duration(seconds: 25); static const Duration requestTimeout = Duration(seconds: 25);

View File

@@ -9,7 +9,8 @@ abstract class ConnectivityStatusService {
Stream<bool> connectivityChanges(); Stream<bool> connectivityChanges();
} }
@Injectable(as: ConnectivityStatusService, env: ['prod', 'dev']) @prod
@Injectable(as: ConnectivityStatusService)
class ConnectivityStatusServiceImpl implements ConnectivityStatusService { class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
final Connectivity connectivity; final Connectivity connectivity;

View File

@@ -56,7 +56,7 @@ class FileService {
} }
} }
static Future<Directory?> get downloadsDirectory async { static Future<Directory> get downloadsDirectory async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return (await getExternalStorageDirectories( return (await getExternalStorageDirectories(
type: StorageDirectory.downloads))! type: StorageDirectory.downloads))!

View File

@@ -17,9 +17,8 @@ abstract class LocalVault {
Future<void> clear(); Future<void> clear();
} }
@Injectable(as: LocalVault)
@prod @prod
@dev @Injectable(as: LocalVault)
class LocalVaultImpl implements LocalVault { class LocalVaultImpl implements LocalVault {
static const applicationSettingsKey = "applicationSettings"; static const applicationSettingsKey = "applicationSettings";
static const authenticationKey = "authentication"; static const authenticationKey = "authentication";

View File

@@ -2,18 +2,21 @@ import 'dart:io';
import 'package:paperless_mobile/di_initializer.config.dart'; import 'package:paperless_mobile/di_initializer.config.dart';
import 'package:paperless_mobile/di_modules.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:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
final getIt = GetIt.instance..allowReassignment; final getIt = GetIt.instance..allowReassignment;
@InjectableInit( @InjectableInit(
initializerName: r'$initGetIt', // default initializerName: 'init', // default
preferRelativeImports: true, // default preferRelativeImports: true, // default
asExtension: false, // default asExtension: false, // default
includeMicroPackages: false,
) )
void configureDependencies(String environment) => 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]. /// Registers new security context, which will be used by the HttpClient, see [RegisterModule].

View File

@@ -2,10 +2,9 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.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: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/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/language_header.interceptor.dart';
import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/response_conversion.interceptor.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@@ -16,34 +15,30 @@ import 'package:local_auth/local_auth.dart';
@module @module
abstract class RegisterModule { abstract class RegisterModule {
@singleton
@dev
@prod @prod
@singleton
LocalAuthentication get localAuthentication => LocalAuthentication(); LocalAuthentication get localAuthentication => LocalAuthentication();
@singleton
@dev
@prod @prod
@singleton
EncryptedSharedPreferences get encryptedSharedPreferences => EncryptedSharedPreferences get encryptedSharedPreferences =>
EncryptedSharedPreferences(); EncryptedSharedPreferences();
@singleton
@dev
@prod @prod
@test @test
@singleton
@Order(-1)
SecurityContext get securityContext => SecurityContext(); SecurityContext get securityContext => SecurityContext();
@singleton
@dev
@prod @prod
@singleton
Connectivity get connectivity => Connectivity(); Connectivity get connectivity => Connectivity();
/// ///
/// Factory method creating an [HttpClient] with the currently registered [SecurityContext]. /// Factory method creating an [HttpClient] with the currently registered [SecurityContext].
/// ///
@injectable
@dev
@prod @prod
@Order(-1)
HttpClient getHttpClient(SecurityContext securityContext) => HttpClient getHttpClient(SecurityContext securityContext) =>
HttpClient(context: securityContext) HttpClient(context: securityContext)
..connectionTimeout = const Duration(seconds: 10); ..connectionTimeout = const Duration(seconds: 10);
@@ -51,66 +46,26 @@ abstract class RegisterModule {
/// ///
/// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient]. /// Factory method creating a [InterceptedClient] on top of the currently registered [HttpClient].
/// ///
@injectable
@dev
@prod @prod
@Order(-1)
BaseClient getBaseClient( BaseClient getBaseClient(
AuthenticationInterceptor authInterceptor, AuthenticationInterceptor authInterceptor,
ResponseConversionInterceptor responseConversionInterceptor, ResponseConversionInterceptor responseConversionInterceptor,
LanguageHeaderInterceptor languageHeaderInterceptor, LanguageHeaderInterceptor languageHeaderInterceptor,
BaseUrlInterceptor baseUrlInterceptor,
HttpClient client, HttpClient client,
) => ) =>
InterceptedClient.build( InterceptedClient.build(
interceptors: [ interceptors: [
baseUrlInterceptor,
authInterceptor, authInterceptor,
responseConversionInterceptor, responseConversionInterceptor,
languageHeaderInterceptor languageHeaderInterceptor,
], ],
client: IOClient(client), client: IOClient(client),
); );
@injectable
@dev
@prod @prod
CacheManager getCacheManager(BaseClient client) => CacheManager( CacheManager getCacheManager(BaseClient client) => CacheManager(
Config('cacheKey', fileService: HttpFileService(httpClient: client))); 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
View 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);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.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/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/util.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/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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/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_edit_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.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'; 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!) ? () => _onDelete(state.document!)
: null, : null,
).padded(const EdgeInsets.symmetric(horizontal: 4)), ).padded(const EdgeInsets.symmetric(horizontal: 4)),
IconButton( DocumentDownloadButton(
icon: const Icon(Icons.download), document: state.document,
onPressed: Platform.isAndroid && state.document != null ),
? () => _onDownload(state.document!)
: null,
).padded(const EdgeInsets.only(right: 4)),
IconButton( IconButton(
icon: const Icon(Icons.open_in_new), icon: const Icon(Icons.open_in_new),
onPressed: state.document != null onPressed: state.document != null
@@ -404,25 +402,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return const SizedBox(height: 32.0); 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. /// 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:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart'; import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
@singleton @prod
@test
@lazySingleton
class DocumentsCubit extends Cubit<DocumentsState> { class DocumentsCubit extends Cubit<DocumentsState> {
final PaperlessDocumentsApi _api; final PaperlessDocumentsApi _api;

View File

@@ -64,7 +64,6 @@ class _SortFieldSelectionBottomSheetState
contentPadding: const EdgeInsets.symmetric(horizontal: 32), contentPadding: const EdgeInsets.symmetric(horizontal: 32),
title: Text( title: Text(
_localizedSortField(field), _localizedSortField(field),
style: Theme.of(context).textTheme.bodyText2,
), ),
trailing: isNextSelected trailing: isNextSelected
? (_buildOrderIcon(_selectedOrderLoading!)) ? (_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:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
@singleton @prod
@test
@lazySingleton
class CorrespondentCubit extends LabelCubit<Correspondent> { class CorrespondentCubit extends LabelCubit<Correspondent> {
CorrespondentCubit(super.metaDataService); 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:paperless_mobile/features/labels/bloc/label_cubit.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
@singleton @prod
@test
@lazySingleton
class DocumentTypeCubit extends LabelCubit<DocumentType> { class DocumentTypeCubit extends LabelCubit<DocumentType> {
DocumentTypeCubit(super.metaDataService); DocumentTypeCubit(super.metaDataService);

View File

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

View File

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

View File

@@ -120,11 +120,15 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
child: Text(S.of(context).genericActionCancelLabel), child: Text(S.of(context).genericActionCancelLabel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
widget.onDelete(widget.label); widget.onDelete(widget.label);
}, },
child: Text(S.of(context).genericActionDeleteLabel)), 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/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.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/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'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
const authenticationKey = "authentication"; @prod
@test
@singleton @singleton
class AuthenticationCubit extends Cubit<AuthenticationState> { class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService; final LocalAuthenticationService _localAuthService;
final PaperlessAuthenticationApi _authApi; final PaperlessAuthenticationApi _authApi;
final LocalVault localStore; final LocalVault _localVault;
AuthenticationCubit( AuthenticationCubit(
this.localStore, this._localVault,
this._localAuthService, this._localAuthService,
this._authApi, this._authApi,
) : super(AuthenticationState.initial); ) : super(AuthenticationState.initial);
@@ -37,33 +37,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
assert(credentials.username != null && credentials.password != null); assert(credentials.username != null && credentials.password != null);
try { try {
registerSecurityContext(clientCertificate); registerSecurityContext(clientCertificate);
emit( // Store information required to make requests
AuthenticationState( final currentAuth = AuthenticationInformation(
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,
serverUrl: serverUrl, serverUrl: serverUrl,
clientCertificate: clientCertificate, 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( emit(AuthenticationState(
isAuthenticated: true, isAuthenticated: true,
@@ -84,10 +72,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
Future<void> restoreSessionState() async { Future<void> restoreSessionState() async {
final storedAuth = await localStore.loadAuthenticationInformation(); final storedAuth = await _localVault.loadAuthenticationInformation();
late ApplicationSettingsState? appSettings; late ApplicationSettingsState? appSettings;
try { try {
appSettings = await localStore.loadApplicationSettings() ?? appSettings = await _localVault.loadApplicationSettings() ??
ApplicationSettingsState.defaultSettings; ApplicationSettingsState.defaultSettings;
} catch (err) { } catch (err) {
appSettings = ApplicationSettingsState.defaultSettings; appSettings = ApplicationSettingsState.defaultSettings;
@@ -95,31 +83,40 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
if (storedAuth == null || !storedAuth.isValid) { if (storedAuth == null || !storedAuth.isValid) {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false)); emit(AuthenticationState(isAuthenticated: false, wasLoginStored: false));
} else { } else {
if (!appSettings.isLocalAuthenticationEnabled || if (appSettings.isLocalAuthenticationEnabled) {
await _localAuthService final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in")) { .authenticateLocalUser("Authenticate to log back in");
registerSecurityContext(storedAuth.clientCertificate); if (localAuthSuccess) {
emit( registerSecurityContext(storedAuth.clientCertificate);
AuthenticationState( return emit(
isAuthenticated: true, AuthenticationState(
isAuthenticated: true,
wasLoginStored: true,
authentication: storedAuth,
wasLocalAuthenticationSuccessful: true,
),
);
} else {
return emit(AuthenticationState(
isAuthenticated: false,
wasLoginStored: true, wasLoginStored: true,
authentication: storedAuth, wasLocalAuthenticationSuccessful: false,
), ));
); }
} else {
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
} }
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
} }
} }
Future<void> logout() async { Future<void> logout() async {
await localStore.clear(); await _localVault.clear();
emit(AuthenticationState.initial); emit(AuthenticationState.initial);
} }
} }
class AuthenticationState { class AuthenticationState {
final bool wasLoginStored; final bool wasLoginStored;
final bool? wasLocalAuthenticationSuccessful;
final bool isAuthenticated; final bool isAuthenticated;
final AuthenticationInformation? authentication; final AuthenticationInformation? authentication;
@@ -131,6 +128,7 @@ class AuthenticationState {
AuthenticationState({ AuthenticationState({
required this.isAuthenticated, required this.isAuthenticated,
required this.wasLoginStored, required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
this.authentication, this.authentication,
}); });
@@ -138,11 +136,14 @@ class AuthenticationState {
bool? wasLoginStored, bool? wasLoginStored,
bool? isAuthenticated, bool? isAuthenticated,
AuthenticationInformation? authentication, AuthenticationInformation? authentication,
bool? wasLocalAuthenticationSuccessful,
}) { }) {
return AuthenticationState( return AuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated, isAuthenticated: isAuthenticated ?? this.isAuthenticated,
wasLoginStored: wasLoginStored ?? this.wasLoginStored, wasLoginStored: wasLoginStored ?? this.wasLoginStored,
authentication: authentication ?? this.authentication, 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'; import 'package:paperless_mobile/features/login/model/client_certificate.dart';
class AuthenticationInformation { class AuthenticationInformation {
static const usernameKey = 'username';
static const passwordKey = 'password';
static const tokenKey = 'token'; static const tokenKey = 'token';
static const serverUrlKey = 'serverUrl'; static const serverUrlKey = 'serverUrl';
static const clientCertificateKey = 'clientCertificate'; static const clientCertificateKey = 'clientCertificate';
final String username; final String? token;
final String password;
final String token;
final String serverUrl; final String serverUrl;
final ClientCertificate? clientCertificate; final ClientCertificate? clientCertificate;
AuthenticationInformation({ AuthenticationInformation({
required this.username, this.token,
required this.password,
required this.token,
required this.serverUrl, required this.serverUrl,
this.clientCertificate, this.clientCertificate,
}); });
AuthenticationInformation.fromJson(JSON json) AuthenticationInformation.fromJson(JSON json)
: username = json[usernameKey], : token = json[tokenKey],
password = json[passwordKey],
token = json[tokenKey],
serverUrl = json[serverUrlKey], serverUrl = json[serverUrlKey],
clientCertificate = json[clientCertificateKey] != null clientCertificate = json[clientCertificateKey] != null
? ClientCertificate.fromJson(json[clientCertificateKey]) ? ClientCertificate.fromJson(json[clientCertificateKey])
@@ -33,8 +25,6 @@ class AuthenticationInformation {
JSON toJson() { JSON toJson() {
return { return {
usernameKey: username,
passwordKey: password,
tokenKey: token, tokenKey: token,
serverUrlKey: serverUrl, serverUrlKey: serverUrl,
clientCertificateKey: clientCertificate?.toJson(), clientCertificateKey: clientCertificate?.toJson(),
@@ -42,21 +32,16 @@ class AuthenticationInformation {
} }
bool get isValid { bool get isValid {
return serverUrl.isNotEmpty && token.isNotEmpty; return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false);
} }
AuthenticationInformation copyWith({ AuthenticationInformation copyWith({
String? username,
String? password,
String? token, String? token,
String? serverUrl, String? serverUrl,
ClientCertificate? clientCertificate, ClientCertificate? clientCertificate,
bool removeClientCertificate = false, bool removeClientCertificate = false,
bool? isLocalAuthenticationEnabled,
}) { }) {
return AuthenticationInformation( return AuthenticationInformation(
username: username ?? this.username,
password: password ?? this.password,
token: token ?? this.token, token: token ?? this.token,
serverUrl: serverUrl ?? this.serverUrl, serverUrl: serverUrl ?? this.serverUrl,
clientCertificate: clientCertificate ?? clientCertificate: clientCertificate ??

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
correspondent: correspondent, correspondent: correspondent,
tags: tags, tags: tags,
createdAt: createdAt, createdAt: createdAt,
authToken: auth.token, authToken: auth.token!,
serverUrl: auth.serverUrl, serverUrl: auth.serverUrl,
); );
if (onConsumptionFinished != null) { 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:injectable/injectable.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart'; import 'package:paperless_mobile/features/settings/model/view_type.dart';
@singleton @prod
@test
@lazySingleton
class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> { class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
final LocalVault localVault; final LocalVault localVault;

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.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/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart'; import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart'; import 'package:paperless_mobile/generated/l10n.dart';

View File

@@ -70,6 +70,8 @@
"@documentDetailsPageTabOverviewLabel": {}, "@documentDetailsPageTabOverviewLabel": {},
"documentDocumentTypePropertyLabel": "Typ dokumentu", "documentDocumentTypePropertyLabel": "Typ dokumentu",
"@documentDocumentTypePropertyLabel": {}, "@documentDocumentTypePropertyLabel": {},
"documentDownloadSuccessMessage": "Document successfully downloaded.",
"@documentDownloadSuccessMessage": {},
"documentEditPageTitle": "Upravit dokument", "documentEditPageTitle": "Upravit dokument",
"@documentEditPageTitle": {}, "@documentEditPageTitle": {},
"documentMetaDataChecksumLabel": "MD5 součet originálu", "documentMetaDataChecksumLabel": "MD5 součet originálu",
@@ -270,6 +272,10 @@
"@inboxPageMarkAllAsSeenLabel": {}, "@inboxPageMarkAllAsSeenLabel": {},
"inboxPageMarkAsSeenText": "Označit jako shlédnuté", "inboxPageMarkAsSeenText": "Označit jako shlédnuté",
"@inboxPageMarkAsSeenText": {}, "@inboxPageMarkAsSeenText": {},
"inboxPageNoNewDocumentsRefreshLabel": "Refresh",
"@inboxPageNoNewDocumentsRefreshLabel": {},
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
"@inboxPageNoNewDocumentsText": {},
"inboxPageTodayText": "Dnes", "inboxPageTodayText": "Dnes",
"@inboxPageTodayText": {}, "@inboxPageTodayText": {},
"inboxPageUndoRemoveText": "VRÁTIT", "inboxPageUndoRemoveText": "VRÁTIT",
@@ -415,7 +421,5 @@
"tagInboxTagPropertyLabel": "Tag inboxu", "tagInboxTagPropertyLabel": "Tag inboxu",
"@tagInboxTagPropertyLabel": {}, "@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": "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": {}, "@uploadPageAutomaticallInferredFieldsHintText": {}
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
"inboxPageNoNewDocumentsRefreshLabel": "Refresh"
} }

View File

@@ -70,6 +70,8 @@
"@documentDetailsPageTabOverviewLabel": {}, "@documentDetailsPageTabOverviewLabel": {},
"documentDocumentTypePropertyLabel": "Dokumenttyp", "documentDocumentTypePropertyLabel": "Dokumenttyp",
"@documentDocumentTypePropertyLabel": {}, "@documentDocumentTypePropertyLabel": {},
"documentDownloadSuccessMessage": "Dokument erfolgreich heruntergeladen.",
"@documentDownloadSuccessMessage": {},
"documentEditPageTitle": "Dokument Bearbeiten", "documentEditPageTitle": "Dokument Bearbeiten",
"@documentEditPageTitle": {}, "@documentEditPageTitle": {},
"documentMetaDataChecksumLabel": "MD5-Prüfsumme Original", "documentMetaDataChecksumLabel": "MD5-Prüfsumme Original",
@@ -136,13 +138,13 @@
"@documentsPageEmptyStateOopsText": {}, "@documentsPageEmptyStateOopsText": {},
"documentsPageOrderByLabel": "Sortiere nach", "documentsPageOrderByLabel": "Sortiere nach",
"@documentsPageOrderByLabel": {}, "@documentsPageOrderByLabel": {},
"documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchten Sie trotzdem fortfahren?", "documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchtest Du trotzdem fortfahren?",
"@documentsPageSelectionBulkDeleteDialogContinueText": {}, "@documentsPageSelectionBulkDeleteDialogContinueText": {},
"documentsPageSelectionBulkDeleteDialogTitle": "Löschen bestätigen", "documentsPageSelectionBulkDeleteDialogTitle": "Löschen bestätigen",
"@documentsPageSelectionBulkDeleteDialogTitle": {}, "@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": {}, "@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": {}, "@documentsPageSelectionBulkDeleteDialogWarningTextOne": {},
"documentsPageTitle": "Dokumente", "documentsPageTitle": "Dokumente",
"@documentsPageTitle": {}, "@documentsPageTitle": {},
@@ -190,7 +192,7 @@
"@errorMessageCorrespondentLoadFailed": {}, "@errorMessageCorrespondentLoadFailed": {},
"errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.", "errorMessageCreateSavedViewError": "Gespeicherte Ansicht konnte nicht erstellt werden, bitte versuche es erneut.",
"@errorMessageCreateSavedViewError": {}, "@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": {}, "@errorMessageDeleteSavedViewError": {},
"errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.", "errorMessageDeviceOffline": "Daten konnten nicht geladen werden: Eine Verbindung zum Internet konnte nicht hergestellt werden.",
"@errorMessageDeviceOffline": {}, "@errorMessageDeviceOffline": {},
@@ -262,14 +264,18 @@
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.", "inboxPageDocumentRemovedMessageText": "Dokument aus Posteingang entfernt.",
"@inboxPageDocumentRemovedMessageText": {}, "@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": {}, "@inboxPageMarkAllAsSeenConfirmationDialogText": {},
"inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Alle als gesehen markieren?", "inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Alle als gelesen markieren?",
"@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {},
"inboxPageMarkAllAsSeenLabel": "Alle als gesehen markieren", "inboxPageMarkAllAsSeenLabel": "Alle als gelesen markieren",
"@inboxPageMarkAllAsSeenLabel": {}, "@inboxPageMarkAllAsSeenLabel": {},
"inboxPageMarkAsSeenText": "Als gesehen markieren", "inboxPageMarkAsSeenText": "Als gelesen markieren",
"@inboxPageMarkAsSeenText": {}, "@inboxPageMarkAsSeenText": {},
"inboxPageNoNewDocumentsRefreshLabel": "Neu laden",
"@inboxPageNoNewDocumentsRefreshLabel": {},
"inboxPageNoNewDocumentsText": "Du hast keine ungesehenen Dokumente.",
"@inboxPageNoNewDocumentsText": {},
"inboxPageTodayText": "Heute", "inboxPageTodayText": "Heute",
"@inboxPageTodayText": {}, "@inboxPageTodayText": {},
"inboxPageUndoRemoveText": "UNDO", "inboxPageUndoRemoveText": "UNDO",
@@ -415,7 +421,5 @@
"tagInboxTagPropertyLabel": "Posteingangs-Tag", "tagInboxTagPropertyLabel": "Posteingangs-Tag",
"@tagInboxTagPropertyLabel": {}, "@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": "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": {}, "@uploadPageAutomaticallInferredFieldsHintText": {}
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
"inboxPageNoNewDocumentsRefreshLabel": "Refresh"
} }

View File

@@ -70,6 +70,8 @@
"@documentDetailsPageTabOverviewLabel": {}, "@documentDetailsPageTabOverviewLabel": {},
"documentDocumentTypePropertyLabel": "Document Type", "documentDocumentTypePropertyLabel": "Document Type",
"@documentDocumentTypePropertyLabel": {}, "@documentDocumentTypePropertyLabel": {},
"documentDownloadSuccessMessage": "Document successfully downloaded.",
"@documentDownloadSuccessMessage": {},
"documentEditPageTitle": "Edit Document", "documentEditPageTitle": "Edit Document",
"@documentEditPageTitle": {}, "@documentEditPageTitle": {},
"documentMetaDataChecksumLabel": "Original MD5-Checksum", "documentMetaDataChecksumLabel": "Original MD5-Checksum",
@@ -262,7 +264,7 @@
"@genericMessageOfflineText": {}, "@genericMessageOfflineText": {},
"inboxPageDocumentRemovedMessageText": "Document removed from inbox.", "inboxPageDocumentRemovedMessageText": "Document removed from inbox.",
"@inboxPageDocumentRemovedMessageText": {}, "@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": {}, "@inboxPageMarkAllAsSeenConfirmationDialogText": {},
"inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Mark all as seen?", "inboxPageMarkAllAsSeenConfirmationDialogTitleText": "Mark all as seen?",
"@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {}, "@inboxPageMarkAllAsSeenConfirmationDialogTitleText": {},
@@ -270,6 +272,10 @@
"@inboxPageMarkAllAsSeenLabel": {}, "@inboxPageMarkAllAsSeenLabel": {},
"inboxPageMarkAsSeenText": "Mark as seen", "inboxPageMarkAsSeenText": "Mark as seen",
"@inboxPageMarkAsSeenText": {}, "@inboxPageMarkAsSeenText": {},
"inboxPageNoNewDocumentsRefreshLabel": "Refresh",
"@inboxPageNoNewDocumentsRefreshLabel": {},
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
"@inboxPageNoNewDocumentsText": {},
"inboxPageTodayText": "Today", "inboxPageTodayText": "Today",
"@inboxPageTodayText": {}, "@inboxPageTodayText": {},
"inboxPageUndoRemoveText": "UNDO", "inboxPageUndoRemoveText": "UNDO",
@@ -415,7 +421,5 @@
"tagInboxTagPropertyLabel": "Inbox-Tag", "tagInboxTagPropertyLabel": "Inbox-Tag",
"@tagInboxTagPropertyLabel": {}, "@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": "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": {}, "@uploadPageAutomaticallInferredFieldsHintText": {}
"inboxPageNoNewDocumentsText": "You do not have unseen documents.",
"inboxPageNoNewDocumentsRefreshLabel": "Refresh"
} }

View File

@@ -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/generated/l10n.dart';
import 'package:paperless_mobile/util.dart'; import 'package:paperless_mobile/util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.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(); Bloc.observer = BlocChangesObserver();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@@ -54,10 +55,6 @@ Future<void> startAppProd() async {
runApp(const PaperlessMobileEntrypoint()); runApp(const PaperlessMobileEntrypoint());
} }
void main() async {
await startAppProd();
}
class PaperlessMobileEntrypoint extends StatefulWidget { class PaperlessMobileEntrypoint extends StatefulWidget {
const PaperlessMobileEntrypoint({Key? key}) : super(key: key); const PaperlessMobileEntrypoint({Key? key}) : super(key: key);
@@ -71,10 +68,18 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider.value(value: getIt<ConnectivityCubit>()), BlocProvider<ConnectivityCubit>.value(
BlocProvider.value(value: getIt<AuthenticationCubit>()), value: getIt<ConnectivityCubit>(),
BlocProvider.value(value: getIt<PaperlessServerInformationCubit>()), ),
BlocProvider.value(value: getIt<ApplicationSettingsCubit>()), BlocProvider<AuthenticationCubit>.value(
value: getIt<AuthenticationCubit>(),
),
BlocProvider<PaperlessServerInformationCubit>.value(
value: getIt<PaperlessServerInformationCubit>(),
),
BlocProvider<ApplicationSettingsCubit>.value(
value: getIt<ApplicationSettingsCubit>(),
),
], ],
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>( child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) { builder: (context, settings) {
@@ -234,6 +239,10 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
child: const HomePage(), child: const HomePage(),
); );
} else { } else {
if (authentication.wasLoginStored &&
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
return BiometricAuthenticationPage();
}
return const LoginPage(); 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"),
),
],
),
],
),
);
}
}

View File

@@ -22,15 +22,17 @@ void showSnackBar(
String? details, String? details,
SnackBarAction? action, SnackBarAction? action,
}) { }) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context)
SnackBar( ..hideCurrentSnackBar()
content: Text( ..showSnackBar(
message + (details != null ? ' ($details)' : ''), SnackBar(
content: Text(
message + (details != null ? ' ($details)' : ''),
),
action: action,
duration: const Duration(seconds: 5),
), ),
action: action, );
duration: const Duration(seconds: 5),
),
);
} }
void showGenericError( void showGenericError(

View File

@@ -1,12 +1,14 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'correspondent_model.g.dart';
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class Correspondent extends Label { class Correspondent extends Label {
static const lastCorrespondenceKey = 'last_correspondence'; final DateTime? lastCorrespondence;
late DateTime? lastCorrespondence; const Correspondent({
Correspondent({
required super.id, required super.id,
required super.name, required super.name,
super.slug, super.slug,
@@ -17,24 +19,16 @@ class Correspondent extends Label {
this.lastCorrespondence, this.lastCorrespondence,
}); });
Correspondent.fromJson(Map<String, dynamic> json) factory Correspondent.fromJson(Map<String, dynamic> json) =>
: lastCorrespondence = _$CorrespondentFromJson(json);
DateTime.tryParse(json[lastCorrespondenceKey] ?? ''),
super.fromJson(json); Map<String, dynamic> toJson() => _$CorrespondentToJson(this);
@override @override
String toString() { String toString() {
return name; return name;
} }
@override
void addSpecificFieldsToJson(Map<String, dynamic> json) {
if (lastCorrespondence != null) {
json.putIfAbsent(
lastCorrespondenceKey, () => lastCorrespondence!.toIso8601String());
}
}
@override @override
Correspondent copyWith({ Correspondent copyWith({
int? id, int? id,
@@ -51,13 +45,25 @@ class Correspondent extends Label {
name: name ?? this.name, name: name ?? this.name,
documentCount: documentCount ?? documentCount, documentCount: documentCount ?? documentCount,
isInsensitive: isInsensitive ?? isInsensitive, isInsensitive: isInsensitive ?? isInsensitive,
lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence,
match: match ?? this.match, match: match ?? this.match,
matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm, matchingAlgorithm: matchingAlgorithm ?? this.matchingAlgorithm,
slug: slug ?? this.slug, slug: slug ?? this.slug,
lastCorrespondence: lastCorrespondence ?? this.lastCorrespondence,
); );
} }
@override @override
String get queryEndpoint => 'correspondents'; String get queryEndpoint => 'correspondents';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
lastCorrespondence,
matchingAlgorithm,
match,
];
} }

View File

@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'correspondent_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Correspondent _$CorrespondentFromJson(Map<String, dynamic> json) =>
Correspondent(
id: json['id'] as int?,
name: json['name'] as String,
slug: json['slug'] as String?,
match: json['match'] as String?,
matchingAlgorithm: $enumDecodeNullable(
_$MatchingAlgorithmEnumMap, json['matching_algorithm']),
isInsensitive: json['is_insensitive'] as bool?,
documentCount: json['document_count'] as int?,
lastCorrespondence: json['last_correspondence'] == null
? null
: DateTime.parse(json['last_correspondence'] as String),
);
Map<String, dynamic> _$CorrespondentToJson(Correspondent instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['name'] = instance.name;
writeNotNull('slug', instance.slug);
writeNotNull('match', instance.match);
writeNotNull('matching_algorithm',
_$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]);
writeNotNull('is_insensitive', instance.isInsensitive);
writeNotNull('document_count', instance.documentCount);
writeNotNull(
'last_correspondence', instance.lastCorrespondence?.toIso8601String());
return val;
}
const _$MatchingAlgorithmEnumMap = {
MatchingAlgorithm.anyWord: 1,
MatchingAlgorithm.allWords: 2,
MatchingAlgorithm.exactMatch: 3,
MatchingAlgorithm.regex: 4,
MatchingAlgorithm.similarWord: 5,
MatchingAlgorithm.auto: 6,
};

View File

@@ -1,8 +1,11 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'document_type_model.g.dart';
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class DocumentType extends Label { class DocumentType extends Label {
DocumentType({ const DocumentType({
required super.id, required super.id,
required super.name, required super.name,
super.slug, super.slug,
@@ -12,10 +15,8 @@ class DocumentType extends Label {
super.documentCount, super.documentCount,
}); });
DocumentType.fromJson(Map<String, dynamic> json) : super.fromJson(json); factory DocumentType.fromJson(Map<String, dynamic> json) =>
_$DocumentTypeFromJson(json);
@override
void addSpecificFieldsToJson(Map<String, dynamic> json) {}
@override @override
String get queryEndpoint => 'document_types'; String get queryEndpoint => 'document_types';
@@ -40,4 +41,18 @@ class DocumentType extends Label {
slug: slug ?? this.slug, slug: slug ?? this.slug,
); );
} }
@override
Map<String, dynamic> toJson() => _$DocumentTypeToJson(this);
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
matchingAlgorithm,
match,
];
} }

View File

@@ -0,0 +1,47 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'document_type_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DocumentType _$DocumentTypeFromJson(Map<String, dynamic> json) => DocumentType(
id: json['id'] as int?,
name: json['name'] as String,
slug: json['slug'] as String?,
match: json['match'] as String?,
matchingAlgorithm: $enumDecodeNullable(
_$MatchingAlgorithmEnumMap, json['matching_algorithm']),
isInsensitive: json['is_insensitive'] as bool?,
documentCount: json['document_count'] as int?,
);
Map<String, dynamic> _$DocumentTypeToJson(DocumentType instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['name'] = instance.name;
writeNotNull('slug', instance.slug);
writeNotNull('match', instance.match);
writeNotNull('matching_algorithm',
_$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]);
writeNotNull('is_insensitive', instance.isInsensitive);
writeNotNull('document_count', instance.documentCount);
return val;
}
const _$MatchingAlgorithmEnumMap = {
MatchingAlgorithm.anyWord: 1,
MatchingAlgorithm.allWords: 2,
MatchingAlgorithm.exactMatch: 3,
MatchingAlgorithm.regex: 4,
MatchingAlgorithm.similarWord: 5,
MatchingAlgorithm.auto: 6,
};

View File

@@ -1,7 +1,8 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
abstract class Label with EquatableMixin implements Comparable { abstract class Label extends Equatable implements Comparable {
static const idKey = "id"; static const idKey = "id";
static const nameKey = "name"; static const nameKey = "name";
static const slugKey = "slug"; static const slugKey = "slug";
@@ -11,13 +12,19 @@ abstract class Label with EquatableMixin implements Comparable {
static const documentCountKey = "document_count"; static const documentCountKey = "document_count";
String get queryEndpoint; String get queryEndpoint;
@JsonKey()
final int? id; final int? id;
@JsonKey()
final String name; final String name;
@JsonKey()
final String? slug; final String? slug;
@JsonKey()
final String? match; final String? match;
@JsonKey()
final MatchingAlgorithm? matchingAlgorithm; final MatchingAlgorithm? matchingAlgorithm;
@JsonKey()
final bool? isInsensitive; final bool? isInsensitive;
@JsonKey()
final int? documentCount; final int? documentCount;
const Label({ const Label({
@@ -30,31 +37,6 @@ abstract class Label with EquatableMixin implements Comparable {
this.slug, this.slug,
}); });
Label.fromJson(Map<String, dynamic> json)
: id = json[idKey],
name = json[nameKey],
slug = json[slugKey],
match = json[matchKey],
matchingAlgorithm =
MatchingAlgorithm.fromInt(json[matchingAlgorithmKey]),
isInsensitive = json[isInsensitiveKey],
documentCount = json[documentCountKey];
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json.putIfAbsent(idKey, () => id);
json.putIfAbsent(nameKey, () => name);
json.putIfAbsent(slugKey, () => slug);
json.putIfAbsent(matchKey, () => match);
json.putIfAbsent(matchingAlgorithmKey, () => matchingAlgorithm?.value);
json.putIfAbsent(isInsensitiveKey, () => isInsensitive);
json.putIfAbsent(documentCountKey, () => documentCount);
addSpecificFieldsToJson(json);
return json;
}
void addSpecificFieldsToJson(Map<String, dynamic> json);
Label copyWith({ Label copyWith({
int? id, int? id,
String? name, String? name,
@@ -75,6 +57,5 @@ abstract class Label with EquatableMixin implements Comparable {
return toString().toLowerCase().compareTo(other.toString().toLowerCase()); return toString().toLowerCase().compareTo(other.toString().toLowerCase());
} }
@override Map<String, dynamic> toJson();
List<Object?> get props => [id];
} }

View File

@@ -1,3 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
@JsonEnum(valueField: 'value')
enum MatchingAlgorithm { enum MatchingAlgorithm {
anyWord(1, "Any: Match one of the following words"), anyWord(1, "Any: Match one of the following words"),
allWords(2, "All: Match all of the following words"), allWords(2, "All: Match all of the following words"),

View File

@@ -1,9 +1,11 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
part 'storage_path_model.g.dart';
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.snake)
class StoragePath extends Label { class StoragePath extends Label {
static const pathKey = 'path'; static const pathKey = 'path';
late String? path; late String? path;
StoragePath({ StoragePath({
@@ -17,23 +19,14 @@ class StoragePath extends Label {
required this.path, required this.path,
}); });
StoragePath.fromJson(Map<String, dynamic> json) factory StoragePath.fromJson(Map<String, dynamic> json) =>
: path = json[pathKey], _$StoragePathFromJson(json);
super.fromJson(json);
@override @override
String toString() { String toString() {
return name; return name;
} }
@override
void addSpecificFieldsToJson(Map<String, dynamic> json) {
json.putIfAbsent(
pathKey,
() => path,
);
}
@override @override
StoragePath copyWith({ StoragePath copyWith({
int? id, int? id,
@@ -59,4 +52,19 @@ class StoragePath extends Label {
@override @override
String get queryEndpoint => 'storage_paths'; String get queryEndpoint => 'storage_paths';
@override
List<Object?> get props => [
id,
name,
slug,
isInsensitive,
documentCount,
path,
matchingAlgorithm,
match,
];
@override
Map<String, dynamic> toJson() => _$StoragePathToJson(this);
} }

View File

@@ -0,0 +1,49 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'storage_path_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StoragePath _$StoragePathFromJson(Map<String, dynamic> json) => StoragePath(
id: json['id'] as int?,
name: json['name'] as String,
slug: json['slug'] as String?,
match: json['match'] as String?,
matchingAlgorithm: $enumDecodeNullable(
_$MatchingAlgorithmEnumMap, json['matching_algorithm']),
isInsensitive: json['is_insensitive'] as bool?,
documentCount: json['document_count'] as int?,
path: json['path'] as String?,
);
Map<String, dynamic> _$StoragePathToJson(StoragePath instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', instance.id);
val['name'] = instance.name;
writeNotNull('slug', instance.slug);
writeNotNull('match', instance.match);
writeNotNull('matching_algorithm',
_$MatchingAlgorithmEnumMap[instance.matchingAlgorithm]);
writeNotNull('is_insensitive', instance.isInsensitive);
writeNotNull('document_count', instance.documentCount);
writeNotNull('path', instance.path);
return val;
}
const _$MatchingAlgorithmEnumMap = {
MatchingAlgorithm.anyWord: 1,
MatchingAlgorithm.allWords: 2,
MatchingAlgorithm.exactMatch: 3,
MatchingAlgorithm.regex: 4,
MatchingAlgorithm.similarWord: 5,
MatchingAlgorithm.auto: 6,
};

View File

@@ -1,6 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:ui'; import 'dart:ui';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/labels/label_model.dart'; import 'package:paperless_api/src/models/labels/label_model.dart';
import 'package:paperless_api/src/models/labels/matching_algorithm.dart'; import 'package:paperless_api/src/models/labels/matching_algorithm.dart';
@@ -10,11 +11,17 @@ class Tag extends Label {
static const textColorKey = 'text_color'; static const textColorKey = 'text_color';
static const legacyColourKey = 'colour'; static const legacyColourKey = 'colour';
final Color? color; final Color? _apiV2color;
final Color? _apiV1color;
final Color? textColor; final Color? textColor;
final bool? isInboxTag; final bool? isInboxTag;
Tag({ Color? get color => _apiV2color ?? _apiV1color;
const Tag({
required super.id, required super.id,
required super.name, required super.name,
super.documentCount, super.documentCount,
@@ -22,42 +29,17 @@ class Tag extends Label {
super.match, super.match,
super.matchingAlgorithm, super.matchingAlgorithm,
super.slug, super.slug,
this.color, Color? color,
this.textColor, this.textColor,
this.isInboxTag, this.isInboxTag,
}); }) : _apiV1color = color,
_apiV2color = color;
Tag.fromJson(Map<String, dynamic> json)
: isInboxTag = json[isInboxTagKey],
textColor = Color(_colorStringToInt(json[textColorKey]) ?? 0),
color = _parseColorFromJson(json),
super.fromJson(json);
///
/// The `color` field of the json object can either be of type [Color] or a hex [String].
/// Since API version 2, the old attribute `colour` has been replaced with `color`.
///
static Color _parseColorFromJson(Map<String, dynamic> json) {
if (json.containsKey(legacyColourKey)) {
return Color(_colorStringToInt(json[legacyColourKey]) ?? 0);
}
if (json[colorKey] is Color) {
return json[colorKey];
}
return Color(_colorStringToInt(json[colorKey]) ?? 0);
}
@override @override
String toString() { String toString() {
return name; return name;
} }
@override
void addSpecificFieldsToJson(Map<String, dynamic> json) {
json.putIfAbsent(colorKey, () => _toHex(color));
json.putIfAbsent(isInboxTagKey, () => isInboxTag);
}
@override @override
Tag copyWith({ Tag copyWith({
int? id, int? id,
@@ -87,22 +69,103 @@ class Tag extends Label {
@override @override
String get queryEndpoint => 'tags'; String get queryEndpoint => 'tags';
}
/// @override
/// Taken from [FormBuilderColorPicker]. List<Object?> get props => [
/// id,
String? _toHex(Color? color) { name,
if (color == null) { slug,
isInsensitive,
documentCount,
matchingAlgorithm,
color,
textColor,
isInboxTag,
match,
];
factory Tag.fromJson(Map<String, dynamic> json) {
const $MatchingAlgorithmEnumMap = {
MatchingAlgorithm.anyWord: 1,
MatchingAlgorithm.allWords: 2,
MatchingAlgorithm.exactMatch: 3,
MatchingAlgorithm.regex: 4,
MatchingAlgorithm.similarWord: 5,
MatchingAlgorithm.auto: 6,
};
return Tag(
id: json['id'] as int?,
name: json['name'] as String,
documentCount: json['document_count'] as int?,
isInsensitive: json['is_insensitive'] as bool?,
match: json['match'] as String?,
matchingAlgorithm: $enumDecodeNullable(
$MatchingAlgorithmEnumMap, json['matching_algorithm']),
slug: json['slug'] as String?,
textColor: _colorFromJson(json['text_color']),
isInboxTag: json['is_inbox_tag'] as bool?,
color: _colorFromJson(json['color']) ?? _colorFromJson(json['colour']),
);
}
@override
Map<String, dynamic> toJson() {
final val = <String, dynamic>{};
const $MatchingAlgorithmEnumMap = {
MatchingAlgorithm.anyWord: 1,
MatchingAlgorithm.allWords: 2,
MatchingAlgorithm.exactMatch: 3,
MatchingAlgorithm.regex: 4,
MatchingAlgorithm.similarWord: 5,
MatchingAlgorithm.auto: 6,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('id', id);
val['name'] = name;
writeNotNull('slug', slug);
writeNotNull('match', match);
writeNotNull(
'matching_algorithm', $MatchingAlgorithmEnumMap[matchingAlgorithm]);
writeNotNull('is_insensitive', isInsensitive);
writeNotNull('document_count', documentCount);
writeNotNull('color', _toHex(_apiV2color));
writeNotNull('colour', _toHex(_apiV1color));
writeNotNull('text_color', _toHex(textColor));
writeNotNull('is_inbox_tag', isInboxTag);
return val;
}
static Color? _colorFromJson(dynamic color) {
if (color is Color) {
return color;
}
if (color is String) {
final decoded = int.tryParse(color.replaceAll("#", "ff"), radix: 16);
if (decoded == null) {
return null;
}
return Color(decoded);
}
return null; return null;
} }
String val =
'#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
log("Color in Tag#_toHex is $val");
return val;
}
int? _colorStringToInt(String? color) { ///
if (color == null) return null; /// Taken from [FormBuilderColorPicker].
return int.tryParse(color.replaceAll("#", "ff"), radix: 16); ///
static String? _toHex(Color? color) {
if (color == null) {
return null;
}
String val =
'#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
return val;
}
} }

View File

@@ -2,6 +2,5 @@ abstract class PaperlessAuthenticationApi {
Future<String> login({ Future<String> login({
required String username, required String username,
required String password, required String password,
required String serverUrl,
}); });
} }

View File

@@ -14,13 +14,15 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
Future<String> login({ Future<String> login({
required String username, required String username,
required String password, required String password,
required String serverUrl,
}) async { }) async {
late Response response; late Response response;
try { try {
response = await client.post( response = await client.post(
Uri.parse("/api/token/"), Uri.parse("/api/token/"),
body: {"username": username, "password": password}, body: {
"username": username,
"password": password,
},
); );
} on FormatException catch (e) { } on FormatException catch (e) {
final source = e.source; final source = e.source;

View File

@@ -644,6 +644,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
hive:
dependency: "direct main"
description:
name: hive
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.3"
html: html:
dependency: transitive dependency: transitive
description: description:
@@ -699,14 +706,14 @@ packages:
name: injectable name: injectable
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.3" version: "2.1.0"
injectable_generator: injectable_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: injectable_generator name: injectable_generator
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.4" version: "2.1.2"
integration_test: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View File

@@ -33,7 +33,7 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
get_it: ^7.2.0 get_it: ^7.2.0
injectable: ^1.5.3 injectable: ^2.1.0
encrypted_shared_preferences: ^3.0.0 encrypted_shared_preferences: ^3.0.0
permission_handler: ^9.2.0 permission_handler: ^9.2.0
pdf: ^3.8.1 pdf: ^3.8.1
@@ -80,6 +80,7 @@ dependencies:
fluttertoast: ^8.1.1 fluttertoast: ^8.1.1
paperless_api: paperless_api:
path: packages/paperless_api path: packages/paperless_api
hive: ^2.2.3
dev_dependencies: dev_dependencies:
integration_test: integration_test:
@@ -87,7 +88,7 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.1.11
injectable_generator: ^1.5.3 injectable_generator: ^2.1.0
mockito: ^5.3.2 mockito: ^5.3.2
bloc_test: ^9.1.0 bloc_test: ^9.1.0
dependency_validator: ^3.0.0 dependency_validator: ^3.0.0