mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 06:07:48 -06:00
Refactored DI, serialization, added feedback to document download
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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/'];
|
const interceptedRoutes = ['thumb/'];
|
||||||
|
|
||||||
@injectable
|
@injectable
|
||||||
@dev
|
|
||||||
@prod
|
@prod
|
||||||
class ResponseConversionInterceptor implements InterceptorContract {
|
class ResponseConversionInterceptor implements InterceptorContract {
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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))!
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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].
|
||||||
|
|||||||
@@ -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
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/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';
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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: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;
|
||||||
|
|
||||||
|
|||||||
@@ -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!))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ class _EditLabelPageState<T extends Label> extends State<EditLabelPage<T>> {
|
|||||||
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
serverUrl: serverUrl,
|
||||||
token: "",
|
|
||||||
clientCertificate: clientCertificate,
|
clientCertificate: clientCertificate,
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
await _localVault.storeAuthenticationInformation(currentAuth);
|
||||||
|
|
||||||
final token = await _authApi.login(
|
final token = await _authApi.login(
|
||||||
username: credentials.username!,
|
username: credentials.username!,
|
||||||
password: credentials.password!,
|
password: credentials.password!,
|
||||||
serverUrl: serverUrl,
|
|
||||||
);
|
|
||||||
final auth = AuthenticationInformation(
|
|
||||||
username: credentials.username!,
|
|
||||||
password: credentials.password!,
|
|
||||||
token: token,
|
|
||||||
serverUrl: serverUrl,
|
|
||||||
clientCertificate: clientCertificate,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await localStore.storeAuthenticationInformation(auth);
|
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");
|
||||||
|
if (localAuthSuccess) {
|
||||||
registerSecurityContext(storedAuth.clientCertificate);
|
registerSecurityContext(storedAuth.clientCertificate);
|
||||||
emit(
|
return emit(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
wasLoginStored: true,
|
wasLoginStored: true,
|
||||||
authentication: storedAuth,
|
authentication: storedAuth,
|
||||||
|
wasLocalAuthenticationSuccessful: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
emit(AuthenticationState(isAuthenticated: false, wasLoginStored: true));
|
return emit(AuthenticationState(
|
||||||
|
isAuthenticated: false,
|
||||||
|
wasLoginStored: true,
|
||||||
|
wasLocalAuthenticationSuccessful: false,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
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 ??
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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: {}));
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
@@ -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"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ void showSnackBar(
|
|||||||
String? details,
|
String? details,
|
||||||
SnackBarAction? action,
|
SnackBarAction? action,
|
||||||
}) {
|
}) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context)
|
||||||
|
..hideCurrentSnackBar()
|
||||||
|
..showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
message + (details != null ? ' ($details)' : ''),
|
message + (details != null ? ' ($details)' : ''),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Taken from [FormBuilderColorPicker].
|
/// Taken from [FormBuilderColorPicker].
|
||||||
///
|
///
|
||||||
String? _toHex(Color? color) {
|
static String? _toHex(Color? color) {
|
||||||
if (color == null) {
|
if (color == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String val =
|
String val =
|
||||||
'#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
|
'#${(color.value & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toLowerCase()}';
|
||||||
log("Color in Tag#_toHex is $val");
|
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
int? _colorStringToInt(String? color) {
|
|
||||||
if (color == null) return null;
|
|
||||||
return int.tryParse(color.replaceAll("#", "ff"), radix: 16);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
11
pubspec.lock
11
pubspec.lock
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user