WIP - Replaced get_it + injectable with Provider

This commit is contained in:
Anton Stubenbord
2022-12-21 01:14:06 +01:00
parent 10149fb7c1
commit 60aecb549d
59 changed files with 1099 additions and 1362 deletions

View File

@@ -1,225 +1,224 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/di_test_mocks.mocks.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:mockito/mockito.dart';
// import 'package:paperless_api/paperless_api.dart';
// import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
// import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
// import 'package:paperless_mobile/core/store/local_vault.dart';
// import 'package:paperless_mobile/di_test_mocks.mocks.dart';
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
// import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
// import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'src/framework.dart';
// import 'src/framework.dart';
void main() async {
final t = await initializeTestingFramework(languageCode: 'de');
// void main() async {
// final t = await initializeTestingFramework(languageCode: 'de');
const testServerUrl = 'https://example.com';
const testUsername = 'user';
const testPassword = 'pass';
// const testServerUrl = 'https://example.com';
// const testUsername = 'user';
// const testPassword = 'pass';
final serverAddressField = find.byKey(const ValueKey('login-server-address'));
final usernameField = find.byKey(const ValueKey('login-username'));
final passwordField = find.byKey(const ValueKey('login-password'));
final loginBtn = find.byKey(const ValueKey('login-login-button'));
// final serverAddressField = find.byKey(const ValueKey('login-server-address'));
// final usernameField = find.byKey(const ValueKey('login-username'));
// final passwordField = find.byKey(const ValueKey('login-password'));
// final loginBtn = find.byKey(const ValueKey('login-login-button'));
testWidgets('Test successful login flow', (WidgetTester tester) async {
await initAndLaunchTestApp(tester, () async {
// Initialize dat for mocked classes
when((getIt<ConnectivityStatusService>()).connectivityChanges())
.thenAnswer((i) => Stream.value(true));
when((getIt<LocalVault>() as MockLocalVault)
.loadAuthenticationInformation())
.thenAnswer((realInvocation) async => null);
when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
.thenAnswer((realInvocation) async => ApplicationSettingsState(
preferredLocaleSubtag: 'en',
preferredThemeMode: ThemeMode.light,
isLocalAuthenticationEnabled: false,
preferredViewType: ViewType.list,
showInboxOnStartup: false,
));
when(getIt<PaperlessAuthenticationApi>().login(
username: testUsername,
password: testPassword,
)).thenAnswer((i) => Future.value("eyTestToken"));
// testWidgets('Test successful login flow', (WidgetTester tester) async {
// await initAndLaunchTestApp(tester, () async {
// // Initialize dat for mocked classes
// when((getIt<ConnectivityStatusService>()).connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>() as MockLocalVault)
// .loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
// when(getIt<PaperlessAuthenticationApi>().login(
// username: testUsername,
// password: testPassword,
// )).thenAnswer((i) => Future.value("eyTestToken"));
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
});
// await getIt<ConnectivityCubit>().initialize();
// await getIt<ApplicationSettingsCubit>().initialize();
// });
// Mocked classes
// // Mocked classes
await t.binding.waitUntilFirstFrameRasterized;
await tester.pumpAndSettle();
// await t.binding.waitUntilFirstFrameRasterized;
// await tester.pumpAndSettle();
await tester.enterText(serverAddressField, testServerUrl);
await tester.pumpAndSettle();
// await tester.enterText(serverAddressField, testServerUrl);
// await tester.pumpAndSettle();
await tester.enterText(usernameField, testUsername);
await tester.pumpAndSettle();
// await tester.enterText(usernameField, testUsername);
// await tester.pumpAndSettle();
await tester.enterText(passwordField, testPassword);
// await tester.enterText(passwordField, testPassword);
FocusManager.instance.primaryFocus?.unfocus();
await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
await tester.tap(loginBtn);
// await tester.tap(loginBtn);
verify(getIt<PaperlessAuthenticationApi>().login(
username: testUsername,
password: testPassword,
)).called(1);
});
// verify(getIt<PaperlessAuthenticationApi>().login(
// username: testUsername,
// password: testPassword,
// )).called(1);
// });
testWidgets('Test login validation missing password',
(WidgetTester tester) async {
await initAndLaunchTestApp(tester, () async {
when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
.connectivityChanges())
.thenAnswer((i) => Stream.value(true));
when((getIt<LocalVault>() as MockLocalVault)
.loadAuthenticationInformation())
.thenAnswer((realInvocation) async => null);
// testWidgets('Test login validation missing password',
// (WidgetTester tester) async {
// await initAndLaunchTestApp(tester, () async {
// when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
// .connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>() as MockLocalVault)
// .loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
.thenAnswer((realInvocation) async => ApplicationSettingsState(
preferredLocaleSubtag: 'en',
preferredThemeMode: ThemeMode.light,
isLocalAuthenticationEnabled: false,
preferredViewType: ViewType.list,
showInboxOnStartup: false,
));
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
});
// Mocked classes
// await getIt<ConnectivityCubit>().initialize();
// await getIt<ApplicationSettingsCubit>().initialize();
// });
// // Mocked classes
// Initialize dat for mocked classes
// // Initialize dat for mocked classes
await t.binding.waitUntilFirstFrameRasterized;
await tester.pumpAndSettle();
// await t.binding.waitUntilFirstFrameRasterized;
// await tester.pumpAndSettle();
await tester.enterText(serverAddressField, testServerUrl);
await tester.pumpAndSettle();
// await tester.enterText(serverAddressField, testServerUrl);
// await tester.pumpAndSettle();
await tester.enterText(usernameField, testUsername);
await tester.pumpAndSettle();
// await tester.enterText(usernameField, testUsername);
// await tester.pumpAndSettle();
FocusManager.instance.primaryFocus?.unfocus();
await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
await tester.tap(loginBtn);
await tester.pumpAndSettle();
// await tester.tap(loginBtn);
// await tester.pumpAndSettle();
verifyNever(
(getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
.login(
username: testUsername,
password: testPassword,
));
expect(
find.textContaining(t.translations.loginPagePasswordValidatorMessageText),
findsOneWidget,
);
});
// verifyNever(
// (getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
// .login(
// username: testUsername,
// password: testPassword,
// ));
// expect(
// find.textContaining(t.translations.loginPagePasswordValidatorMessageText),
// findsOneWidget,
// );
// });
testWidgets('Test login validation missing username',
(WidgetTester tester) async {
await initAndLaunchTestApp(tester, () async {
when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
.connectivityChanges())
.thenAnswer((i) => Stream.value(true));
when((getIt<LocalVault>() as MockLocalVault)
.loadAuthenticationInformation())
.thenAnswer((realInvocation) async => null);
when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
.thenAnswer((realInvocation) async => ApplicationSettingsState(
preferredLocaleSubtag: 'en',
preferredThemeMode: ThemeMode.light,
isLocalAuthenticationEnabled: false,
preferredViewType: ViewType.list,
showInboxOnStartup: false,
));
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
});
// testWidgets('Test login validation missing username',
// (WidgetTester tester) async {
// await initAndLaunchTestApp(tester, () async {
// when((getIt<ConnectivityStatusService>() as MockConnectivityStatusService)
// .connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
// when((getIt<LocalVault>() as MockLocalVault)
// .loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>() as MockLocalVault).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
// await getIt<ConnectivityCubit>().initialize();
// await getIt<ApplicationSettingsCubit>().initialize();
// });
await t.binding.waitUntilFirstFrameRasterized;
await tester.pumpAndSettle();
// await t.binding.waitUntilFirstFrameRasterized;
// await tester.pumpAndSettle();
await tester.enterText(serverAddressField, testServerUrl);
await tester.pumpAndSettle();
// await tester.enterText(serverAddressField, testServerUrl);
// await tester.pumpAndSettle();
await tester.enterText(passwordField, testPassword);
await tester.pumpAndSettle();
// await tester.enterText(passwordField, testPassword);
// await tester.pumpAndSettle();
FocusManager.instance.primaryFocus?.unfocus();
await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
await tester.tap(loginBtn);
await tester.pumpAndSettle();
// await tester.tap(loginBtn);
// await tester.pumpAndSettle();
verifyNever(
(getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
.login(
username: testUsername,
password: testPassword,
));
expect(
find.textContaining(t.translations.loginPageUsernameValidatorMessageText),
findsOneWidget,
);
});
// verifyNever(
// (getIt<PaperlessAuthenticationApi>() as MockPaperlessAuthenticationApi)
// .login(
// username: testUsername,
// password: testPassword,
// ));
// expect(
// find.textContaining(t.translations.loginPageUsernameValidatorMessageText),
// findsOneWidget,
// );
// });
testWidgets('Test login validation missing server address',
(WidgetTester tester) async {
initAndLaunchTestApp(tester, () async {
when((getIt<ConnectivityStatusService>()).connectivityChanges())
.thenAnswer((i) => Stream.value(true));
// testWidgets('Test login validation missing server address',
// (WidgetTester tester) async {
// initAndLaunchTestApp(tester, () async {
// when((getIt<ConnectivityStatusService>()).connectivityChanges())
// .thenAnswer((i) => Stream.value(true));
when((getIt<LocalVault>()).loadAuthenticationInformation())
.thenAnswer((realInvocation) async => null);
// when((getIt<LocalVault>()).loadAuthenticationInformation())
// .thenAnswer((realInvocation) async => null);
when((getIt<LocalVault>()).loadApplicationSettings())
.thenAnswer((realInvocation) async => ApplicationSettingsState(
preferredLocaleSubtag: 'en',
preferredThemeMode: ThemeMode.light,
isLocalAuthenticationEnabled: false,
preferredViewType: ViewType.list,
showInboxOnStartup: false,
));
// when((getIt<LocalVault>()).loadApplicationSettings())
// .thenAnswer((realInvocation) async => ApplicationSettingsState(
// preferredLocaleSubtag: 'en',
// preferredThemeMode: ThemeMode.light,
// isLocalAuthenticationEnabled: false,
// preferredViewType: ViewType.list,
// showInboxOnStartup: false,
// ));
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
});
// await getIt<ConnectivityCubit>().initialize();
// await getIt<ApplicationSettingsCubit>().initialize();
// });
await t.binding.waitUntilFirstFrameRasterized;
await tester.pumpAndSettle();
// await t.binding.waitUntilFirstFrameRasterized;
// await tester.pumpAndSettle();
await tester.enterText(usernameField, testUsername);
await tester.pumpAndSettle();
// await tester.enterText(usernameField, testUsername);
// await tester.pumpAndSettle();
await tester.enterText(passwordField, testPassword);
await tester.pumpAndSettle();
// await tester.enterText(passwordField, testPassword);
// await tester.pumpAndSettle();
FocusManager.instance.primaryFocus?.unfocus();
await tester.pumpAndSettle();
// FocusManager.instance.primaryFocus?.unfocus();
// await tester.pumpAndSettle();
await tester.tap(loginBtn);
await tester.pumpAndSettle();
// await tester.tap(loginBtn);
// await tester.pumpAndSettle();
verifyNever(getIt<PaperlessAuthenticationApi>().login(
username: testUsername,
password: testPassword,
));
expect(
find.textContaining(
t.translations.loginPageServerUrlValidatorMessageText),
findsOneWidget,
);
});
}
// verifyNever(getIt<PaperlessAuthenticationApi>().login(
// username: testUsername,
// password: testPassword,
// ));
// expect(
// find.textContaining(
// t.translations.loginPageServerUrlValidatorMessageText),
// findsOneWidget,
// );
// });
// }

View File

@@ -1,13 +1,11 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/generated/l10n.dart';
Future<TestingFrameworkVariables> initializeTestingFramework(
{String languageCode = 'en'}) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
configureDependencies('test');
final translations = await S.load(
Locale.fromSubtags(
languageCode: languageCode,

View File

@@ -1,11 +0,0 @@
// Fix for accepting self signed certificates.
import 'dart:io';
class X509HttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}

View File

@@ -2,11 +2,8 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
@prod
@injectable
class AuthenticationInterceptor implements InterceptorContract {
final LocalVault _localVault;
AuthenticationInterceptor(this._localVault);

View File

@@ -1,9 +1,6 @@
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;

View File

@@ -1,8 +1,6 @@
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
@injectable
class LanguageHeaderInterceptor implements InterceptorContract {
final ApplicationSettingsCubit appSettingsCubit;

View File

@@ -1,10 +1,7 @@
import 'package:http_interceptor/http_interceptor.dart';
import 'package:injectable/injectable.dart';
const interceptedRoutes = ['thumb/'];
@injectable
@prod
class ResponseConversionInterceptor implements InterceptorContract {
@override
Future<BaseRequest> interceptRequest({required BaseRequest request}) async =>

View File

@@ -1,195 +0,0 @@
import 'dart:typed_data';
import 'dart:convert';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:http/http.dart';
import 'package:injectable/injectable.dart';
///
/// Convenience class which handles timeout errors.
///
@prod
@Named("timeoutClient")
@Injectable(as: BaseClient)
class TimeoutClient implements BaseClient {
final ConnectivityStatusService connectivityStatusService;
static const Duration requestTimeout = Duration(seconds: 25);
TimeoutClient(this.connectivityStatusService);
@override
Future<StreamedResponse> send(BaseRequest request) async {
return getIt<BaseClient>().send(request).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
);
}
@override
void close() {
getIt<BaseClient>().close();
}
@override
Future<Response> delete(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.delete(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> get(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>().get(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> head(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>().head(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> patch(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.patch(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> post(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.post(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<Response> put(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) async {
await _handleOfflineState();
return _handle400Error(
await getIt<BaseClient>()
.put(url, headers: headers, body: body, encoding: encoding)
.timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
),
);
}
@override
Future<String> read(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return getIt<BaseClient>().read(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
);
}
@override
Future<Uint8List> readBytes(
Uri url, {
Map<String, String>? headers,
}) async {
await _handleOfflineState();
return getIt<BaseClient>().readBytes(url, headers: headers).timeout(
requestTimeout,
onTimeout: () => Future.error(
const PaperlessServerException(ErrorCode.requestTimedOut)),
);
}
Response _handle400Error(Response response) {
if (response.statusCode == 400) {
// try to parse contained error message, otherwise return response
final JSON json = jsonDecode(utf8.decode(response.bodyBytes));
final PaperlessValidationErrors errorMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
errorMessages.putIfAbsent(
entry.key, () => (entry.value as List).cast<String>().first);
} else if (entry.value is String) {
errorMessages.putIfAbsent(entry.key, () => entry.value);
} else {
errorMessages.putIfAbsent(entry.key, () => entry.value.toString());
}
}
throw errorMessages;
}
return response;
}
Future<void> _handleOfflineState() async {
if (!(await connectivityStatusService.isConnectedToInternet())) {
throw const PaperlessServerException(ErrorCode.deviceOffline);
}
}
}

View File

@@ -11,17 +11,21 @@ class LabelRepositoriesProvider extends StatelessWidget {
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<Correspondent>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<DocumentType>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<StoragePath>>(context),
),
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Tag>>(context),
RepositoryProvider(
create: (context) =>
RepositoryProvider.of<LabelRepository<Tag>>(context),
),
],
child: child,

View File

@@ -0,0 +1,100 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
///
/// Convenience http client handling timeouts.
///
class SecurityContextAwareDioManager {
final Dio _dio;
// Some dependencies require an [HttpClient], therefore this is also maintained here.
final HttpClient _httpClient;
SecurityContextAwareDioManager()
: _dio = _initDio(),
_httpClient = HttpClient();
Dio get client => _dio;
Stream<SecurityContext> get securityContextChanges =>
_securityContextStreamController.stream.asBroadcastStream();
final StreamController<SecurityContext> _securityContextStreamController =
StreamController.broadcast();
static Dio _initDio() {
//en- and decoded by utf8 by default
final Dio dio = Dio(BaseOptions());
dio.options.receiveTimeout = const Duration(seconds: 25).inMilliseconds;
dio.options.responseType = ResponseType.json;
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) => client
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
dio.interceptors.add(
//TODO: Refactor, create own class...
InterceptorsWrapper(
onError: (e, handler) {
//TODO: Implement and debug how error handling works, or if request has to be resolved.
if (e.response?.statusCode == 400) {
// try to parse contained error message, otherwise return response
final JSON json = jsonDecode(e.response?.data);
final PaperlessValidationErrors errorMessages = {};
for (final entry in json.entries) {
if (entry.value is List) {
errorMessages.putIfAbsent(entry.key,
() => (entry.value as List).cast<String>().first);
} else if (entry.value is String) {
errorMessages.putIfAbsent(entry.key, () => entry.value);
} else {
errorMessages.putIfAbsent(
entry.key, () => entry.value.toString());
}
}
throw errorMessages;
}
handler.next(e);
},
),
);
return dio;
}
void updateSettings({
String? baseUrl,
String? authToken,
ClientCertificate? clientCertificate,
}) {
if (clientCertificate != null) {
final sc = SecurityContext()
..usePrivateKeyBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..useCertificateChainBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..setTrustedCertificatesBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
);
(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) => HttpClient(
context: sc,
)..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
_securityContextStreamController.add(sc);
}
if (baseUrl != null) {
_dio.options.baseUrl = baseUrl;
}
if (authToken != null) {
_dio.options.headers.addAll({'Authorization': 'Token $authToken'});
}
}
}

View File

@@ -0,0 +1,26 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:http/io_client.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:rxdart/rxdart.dart';
extension SecurityContextAwareBaseClientSubjectExtension
on BehaviorSubject<BaseClient> {
///
/// Registers new security context in a new [HttpClient].
///
BaseClient _createSecurityContextAwareHttpClient(
SecurityContext context, {
List<InterceptorContract> interceptors = const [],
}) {
Dio(BaseOptions());
return InterceptedClient.build(
client: IOClient(HttpClient(context: context)),
interceptors: interceptors,
);
}
}

View File

View File

@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:injectable/injectable.dart';
abstract class ConnectivityStatusService {
Future<bool> isConnectedToInternet();
@@ -9,8 +8,6 @@ abstract class ConnectivityStatusService {
Stream<bool> connectivityChanges();
}
@prod
@Injectable(as: ConnectivityStatusService)
class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
final Connectivity connectivity;

View File

@@ -82,8 +82,8 @@ class FileService {
static Future<void> clearUserData() async {
final scanDir = await scanDirectory;
final tempDir = await temporaryDirectory;
scanDir?.delete(recursive: true);
tempDir.delete(recursive: true);
await scanDir?.delete(recursive: true);
await tempDir.delete(recursive: true);
}
}

View File

@@ -3,11 +3,9 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/util.dart';
import 'package:web_socket_channel/io.dart';
@@ -17,8 +15,6 @@ abstract class StatusService {
AuthenticationInformation credentials, String documentFileName);
}
@Singleton(as: StatusService)
@Named("webSocketStatusService")
class WebSocketStatusService implements StatusService {
late WebSocket? socket;
late IOWebSocketChannel? _channel;
@@ -31,35 +27,33 @@ class WebSocketStatusService implements StatusService {
AuthenticationInformation credentials,
String documentFileName,
) async {
socket = await WebSocket.connect(
httpUrl.replaceFirst("http", "ws") + "/ws/status/",
customClient: getIt<HttpClient>(),
headers: {
'Authorization': 'Token ${credentials.token}',
},
).catchError((_) {
// Use long polling if connection could not be established
});
// socket = await WebSocket.connect(
// httpUrl.replaceFirst("http", "ws") + "/ws/status/",
// customClient: getIt<HttpClient>(),
// headers: {
// 'Authorization': 'Token ${credentials.token}',
// },
// ).catchError((_) {
// // Use long polling if connection could not be established
// });
if (socket != null) {
socket!.where(isNotNull).listen((event) {
final status = DocumentProcessingStatus.fromJson(event);
getIt<DocumentStatusCubit>().updateStatus(status);
if (status.currentProgress == 100) {
socket!.close();
}
});
}
// if (socket != null) {
// socket!.where(isNotNull).listen((event) {
// final status = DocumentProcessingStatus.fromJson(event);
// getIt<DocumentStatusCubit>().updateStatus(status);
// if (status.currentProgress == 100) {
// socket!.close();
// }
// });
// }
}
}
@Injectable(as: StatusService)
@Named("longPollingStatusService")
class LongPollingStatusService implements StatusService {
static const maxRetries = 60;
final BaseClient httpClient;
LongPollingStatusService(@Named("timeoutClient") this.httpClient);
LongPollingStatusService(this.httpClient);
@override
Future<void> startListeningBeforeDocumentUpload(
@@ -67,51 +61,51 @@ class LongPollingStatusService implements StatusService {
AuthenticationInformation credentials,
String documentFileName,
) async {
final today = DateTime.now();
bool consumptionFinished = false;
int retryCount = 0;
// final today = DateTime.now();
// bool consumptionFinished = false;
// int retryCount = 0;
getIt<DocumentStatusCubit>().updateStatus(
DocumentProcessingStatus(
currentProgress: 0,
filename: documentFileName,
maxProgress: 100,
message: ProcessingMessage.new_file,
status: ProcessingStatus.working,
taskId: DocumentProcessingStatus.unknownTaskId,
documentId: null,
isApproximated: true,
),
);
// getIt<DocumentStatusCubit>().updateStatus(
// DocumentProcessingStatus(
// currentProgress: 0,
// filename: documentFileName,
// maxProgress: 100,
// message: ProcessingMessage.new_file,
// status: ProcessingStatus.working,
// taskId: DocumentProcessingStatus.unknownTaskId,
// documentId: null,
// isApproximated: true,
// ),
// );
do {
final response = await httpClient.get(
Uri.parse(
'$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
);
final data = await compute(
PagedSearchResult.fromJson,
PagedSearchResultJsonSerializer(
jsonDecode(response.body), DocumentModel.fromJson),
);
if (data.count > 0) {
consumptionFinished = true;
final docId = data.results[0].id;
getIt<DocumentStatusCubit>().updateStatus(
DocumentProcessingStatus(
currentProgress: 100,
filename: documentFileName,
maxProgress: 100,
message: ProcessingMessage.finished,
status: ProcessingStatus.success,
taskId: DocumentProcessingStatus.unknownTaskId,
documentId: docId,
isApproximated: true,
),
);
return;
}
sleep(const Duration(seconds: 1));
} while (!consumptionFinished && retryCount < maxRetries);
// do {
// final response = await httpClient.get(
// Uri.parse(
// '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
// );
// final data = await compute(
// PagedSearchResult.fromJson,
// PagedSearchResultJsonSerializer(
// jsonDecode(response.body), DocumentModel.fromJson),
// );
// if (data.count > 0) {
// consumptionFinished = true;
// final docId = data.results[0].id;
// getIt<DocumentStatusCubit>().updateStatus(
// DocumentProcessingStatus(
// currentProgress: 100,
// filename: documentFileName,
// maxProgress: 100,
// message: ProcessingMessage.finished,
// status: ProcessingStatus.success,
// taskId: DocumentProcessingStatus.unknownTaskId,
// documentId: docId,
// isApproximated: true,
// ),
// );
// return;
// }
// sleep(const Duration(seconds: 1));
// } while (!consumptionFinished && retryCount < maxRetries);
}
}

View File

@@ -6,7 +6,6 @@ import 'package:paperless_mobile/core/type/types.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/settings/model/application_settings_state.dart';
import 'package:injectable/injectable.dart';
abstract class LocalVault {
Future<void> storeAuthenticationInformation(AuthenticationInformation auth);
@@ -17,8 +16,6 @@ abstract class LocalVault {
Future<void> clear();
}
@prod
@Injectable(as: LocalVault)
class LocalVaultImpl implements LocalVault {
static const applicationSettingsKey = "applicationSettings";
static const authenticationKey = "authentication";

View File

@@ -1,33 +0,0 @@
import 'dart:io';
import 'package:paperless_mobile/di_initializer.config.dart';
import 'package:paperless_mobile/di_modules.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final getIt = GetIt.instance..allowReassignment;
@InjectableInit(
initializerName: 'init', // default
preferRelativeImports: true, // default
asExtension: false, // default
includeMicroPackages: false,
)
void configureDependencies(String environment) =>
init(getIt, environment: environment);
///
/// Registers new security context, which will be used by the HttpClient, see [RegisterModule].
///
Future<void> registerSecurityContext(ClientCertificate? cert) async {
var context = SecurityContext();
if (cert != null) {
context = context
..usePrivateKeyBytes(cert.bytes, password: cert.passphrase)
..useCertificateChainBytes(cert.bytes, password: cert.passphrase)
..setTrustedCertificatesBytes(cert.bytes, password: cert.passphrase);
}
await getIt.unregister<SecurityContext>();
getIt.registerSingleton<SecurityContext>(context);
}

View File

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

View File

@@ -1,47 +0,0 @@
import 'dart:io';
import 'package:http/http.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
@module
abstract class PaperlessApiModule {
@prod
@Order(-1)
@injectable
PaperlessAuthenticationApi authenticationModule(BaseClient client) =>
PaperlessAuthenticationApiImpl(client);
@prod
@Order(-1)
@injectable
PaperlessLabelsApi labelsModule(
@Named('timeoutClient') BaseClient timeoutClient,
) =>
PaperlessLabelApiImpl(timeoutClient);
@prod
@Order(-1)
@injectable
PaperlessDocumentsApi documentsModule(
@Named('timeoutClient') BaseClient timeoutClient,
HttpClient httpClient,
) =>
PaperlessDocumentsApiImpl(timeoutClient, httpClient);
@prod
@Order(-1)
@injectable
PaperlessSavedViewsApi savedViewsModule(
@Named('timeoutClient') BaseClient timeoutClient,
) =>
PaperlessSavedViewsApiImpl(timeoutClient);
@prod
@Order(-1)
@injectable
PaperlessServerStatsApi serverStatsModule(
@Named('timeoutClient') BaseClient timeoutClient,
) =>
PaperlessServerStatsApiImpl(timeoutClient);
}

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:paperless_mobile/core/global/asset_images.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/view/widgets/biometric_authentication_setting.dart';
import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart';
@@ -26,84 +25,81 @@ class _ApplicationIntroSlideshowState extends State<ApplicationIntroSlideshow> {
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: BlocProvider.value(
value: getIt<ApplicationSettingsCubit>(),
child: IntroductionScreen(
globalBackgroundColor: Theme.of(context).canvasColor,
showDoneButton: true,
next: Text(S.of(context).onboardingNextButtonLabel),
done: Text(S.of(context).onboardingDoneButtonLabel),
onDone: () => Navigator.pop(context),
dotsDecorator: DotsDecorator(
color: Theme.of(context).colorScheme.onBackground,
activeColor: Theme.of(context).colorScheme.primary,
activeSize: const Size(16.0, 8.0),
activeShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
child: IntroductionScreen(
globalBackgroundColor: Theme.of(context).canvasColor,
showDoneButton: true,
next: Text(S.of(context).onboardingNextButtonLabel),
done: Text(S.of(context).onboardingDoneButtonLabel),
onDone: () => Navigator.pop(context),
dotsDecorator: DotsDecorator(
color: Theme.of(context).colorScheme.onBackground,
activeColor: Theme.of(context).colorScheme.primary,
activeSize: const Size(16.0, 8.0),
activeShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(25.0)),
),
),
pages: [
PageViewModel(
titleWidget: Text(
"Always right at your fingertip",
style: Theme.of(context).textTheme.titleLarge,
),
image: Padding(
padding: const EdgeInsets.all(8.0),
child: Image(
image: AssetImages.organizeDocuments.image,
),
),
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Organizing documents was never this easy",
textAlign: TextAlign.center,
),
],
),
),
pages: [
PageViewModel(
titleWidget: Text(
"Always right at your fingertip",
style: Theme.of(context).textTheme.titleLarge,
),
image: Padding(
padding: const EdgeInsets.all(8.0),
child: Image(
image: AssetImages.organizeDocuments.image,
PageViewModel(
titleWidget: Text(
"Accessible only by you",
style: Theme.of(context).textTheme.titleLarge,
),
image: Padding(
padding: const EdgeInsets.all(8.0),
child: Image(image: AssetImages.secureDocuments.image),
),
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Secure your documents with biometric authentication and client certificates",
textAlign: TextAlign.center,
),
),
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Organizing documents was never this easy",
textAlign: TextAlign.center,
),
],
),
],
),
PageViewModel(
titleWidget: Text(
"Accessible only by you",
style: Theme.of(context).textTheme.titleLarge,
),
image: Padding(
padding: const EdgeInsets.all(8.0),
child: Image(image: AssetImages.secureDocuments.image),
),
bodyWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Secure your documents with biometric authentication and client certificates",
textAlign: TextAlign.center,
),
],
),
),
PageViewModel(
titleWidget: Text(
"You're almost done",
style: Theme.of(context).textTheme.titleLarge,
),
PageViewModel(
titleWidget: Text(
"You're almost done",
style: Theme.of(context).textTheme.titleLarge,
),
image: Padding(
padding: const EdgeInsets.all(8.0),
child: Image(image: AssetImages.success.image),
),
bodyWidget: Column(
children: const [
BiometricAuthenticationSetting(),
LanguageSelectionSetting(),
ThemeModeSetting(),
],
),
image: Padding(
padding: const EdgeInsets.all(8.0),
child: Image(image: AssetImages.success.image),
),
],
),
bodyWidget: Column(
children: const [
BiometricAuthenticationSetting(),
LanguageSelectionSetting(),
ThemeModeSetting(),
],
),
),
],
),
);
}

View File

@@ -1,87 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/util.dart';
class BiometricAuthenticationIntroSlide extends StatefulWidget {
const BiometricAuthenticationIntroSlide({
Key? key,
}) : super(key: key);
@override
State<BiometricAuthenticationIntroSlide> createState() =>
_BiometricAuthenticationIntroSlideState();
}
class _BiometricAuthenticationIntroSlideState
extends State<BiometricAuthenticationIntroSlide> {
@override
Widget build(BuildContext context) {
//TODO: INTL
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"Configure Biometric Authentication",
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
Text(
"It is highly recommended to additionally secure your local data. Do you want to enable biometric authentication?",
textAlign: TextAlign.center,
),
Column(
children: [
const Icon(
Icons.fingerprint,
size: 48,
),
const SizedBox(
height: 32,
),
Builder(builder: (context) {
if (settings.isLocalAuthenticationEnabled) {
return ElevatedButton.icon(
icon: Icon(
Icons.done,
color: Colors.green,
),
label: Text("Enabled"),
onPressed: null,
);
}
return ElevatedButton(
child: Text("Enable"),
onPressed: () {
final settings =
BlocProvider.of<ApplicationSettingsCubit>(context)
.state;
getIt<LocalAuthenticationService>()
.authenticateLocalUser(
"Please authenticate to secure Paperless Mobile")
.then((isEnabled) {
if (!isEnabled) {
showSnackBar(context,
"Could not set up biometric authentication. Please try again or skip for now.");
return;
}
BlocProvider.of<ApplicationSettingsCubit>(context)
.setIsBiometricAuthenticationEnabled(true);
});
},
);
}),
],
),
],
);
},
);
}
}

View File

@@ -10,7 +10,6 @@ import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
@@ -26,6 +25,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
class DocumentDetailsPage extends StatefulWidget {
@@ -50,7 +50,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
return WillPopScope(
onWillPop: () async {
Navigator.of(context)
.pop(BlocProvider.of<DocumentDetailsCubit>(context).state.document);
.pop(context.read<DocumentDetailsCubit>().state.document);
return false;
},
child: DefaultTabController(
@@ -106,9 +106,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
.black, //TODO: check if there is a way to dynamically determine color...
),
onPressed: () => Navigator.of(context).pop(
BlocProvider.of<DocumentDetailsCubit>(context)
.state
.document,
context.read<DocumentDetailsCubit>().state.document,
),
),
floating: true,
@@ -185,29 +183,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Future<void> _onEdit(DocumentModel document) async {
{
final cubit = BlocProvider.of<DocumentDetailsCubit>(context);
final cubit = context.read<DocumentDetailsCubit>();
Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => EditDocumentCubit(
document,
documentsApi: getIt<PaperlessDocumentsApi>(),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
storagePathRepository:
RepositoryProvider.of<LabelRepository<StoragePath>>(
context,
),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
documentsApi: context.watch(),
correspondentRepository: context.watch(),
documentTypeRepository: context.watch(),
storagePathRepository: context.watch(),
tagRepository: context.watch(),
),
child: BlocListener<EditDocumentCubit, EditDocumentState>(
listenWhen: (previous, current) =>
@@ -226,7 +213,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Widget _buildDocumentMetaDataView(DocumentModel document) {
return FutureBuilder<DocumentMetaData>(
future: getIt<PaperlessDocumentsApi>().getMetaData(document),
future: context.read<PaperlessDocumentsApi>().getMetaData(document),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
@@ -281,7 +268,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Future<void> _assignAsn(DocumentModel document) async {
try {
await BlocProvider.of<DocumentDetailsCubit>(context).assignAsn(document);
await context.read<DocumentDetailsCubit>().assignAsn(document);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
@@ -393,7 +380,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
///
Future<void> _onShare(DocumentModel document) async {
Uint8List documentBytes =
await getIt<PaperlessDocumentsApi>().download(document);
await context.read<PaperlessDocumentsApi>().download(document);
final dir = await getTemporaryDirectory();
final String path = "${dir.path}/${document.originalFileName}";
await File(path).writeAsBytes(documentBytes);
@@ -419,7 +406,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
false;
if (delete) {
try {
await BlocProvider.of<DocumentDetailsCubit>(context).delete(document);
await context.read<DocumentDetailsCubit>().delete(document);
showSnackBar(context, S.of(context).documentDeleteSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
@@ -434,7 +421,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes: getIt<PaperlessDocumentsApi>().getPreview(document.id),
documentBytes:
context.read<PaperlessDocumentsApi>().getPreview(document.id),
),
),
);

View File

@@ -3,10 +3,10 @@ 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';
import 'package:provider/provider.dart';
class DocumentDownloadButton extends StatefulWidget {
final DocumentModel? document;
@@ -43,7 +43,8 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
}
setState(() => _isDownloadPending = true);
try {
final bytes = await getIt<PaperlessDocumentsApi>().download(document);
final bytes =
await context.read<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)

View File

@@ -70,8 +70,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
documentType: documentType,
tags: tags,
createdAt: createdAt,
authToken: auth.token!,
serverUrl: auth.serverUrl,
);
if (onConsumptionFinished != null) {
_documentApi

View File

@@ -166,10 +166,8 @@ class _DocumentUploadPreparationPageState
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialName) =>
RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
RepositoryProvider<LabelRepository<DocumentType>>(
create: (context) => context.watch(),
child: AddDocumentTypePage(initialName: initialName),
),
textFieldLabel:
@@ -182,11 +180,8 @@ class _DocumentUploadPreparationPageState
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialName) =>
RepositoryProvider.value(
value:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
RepositoryProvider<LabelRepository<Correspondent>>(
create: (context) => context.watch(),
child: AddCorrespondentPage(initialName: initialName),
),
textFieldLabel:
@@ -220,7 +215,7 @@ class _DocumentUploadPreparationPageState
void _onSubmit() async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final cubit = BlocProvider.of<DocumentUploadCubit>(context);
final cubit = context.read<DocumentUploadCubit>();
try {
setState(() => _isUploadLoading = true);

View File

@@ -99,8 +99,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
return LabelFormField<StoragePath>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<StoragePath>>(context),
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider<LabelRepository<StoragePath>>(
create: (context) => context.watch(),
child: AddStoragePathPage(initalValue: initialValue),
),
textFieldLabel: S.of(context).documentStoragePathPropertyLabel,
@@ -116,10 +117,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
return LabelFormField<Correspondent>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (initialValue) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
labelCreationWidgetBuilder: (initialValue) =>
RepositoryProvider<LabelRepository<Correspondent>>(
create: context.watch(),
child: AddCorrespondentPage(initialName: initialValue),
),
textFieldLabel: S.of(context).documentCorrespondentPropertyLabel,
@@ -135,10 +135,9 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
return LabelFormField<DocumentType>(
notAssignedSelectable: false,
formBuilderState: _formKey.currentState,
labelCreationWidgetBuilder: (currentInput) => RepositoryProvider.value(
value: RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
labelCreationWidgetBuilder: (currentInput) =>
RepositoryProvider<LabelRepository<DocumentType>>(
create: (context) => context.watch(),
child: AddDocumentTypePage(
initialName: currentInput,
),
@@ -170,8 +169,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
_isSubmitLoading = true;
});
try {
await BlocProvider.of<EditDocumentCubit>(context)
.updateDocument(mergedDocument);
await context.read<EditDocumentCubit>().updateDocument(mergedDocument);
showSnackBar(context, S.of(context).documentUpdateSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);

View File

@@ -4,7 +4,6 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
@@ -23,6 +22,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/util.dart';
import 'package:provider/provider.dart';
class DocumentsPage extends StatefulWidget {
const DocumentsPage({Key? key}) : super(key: key);
@@ -42,8 +42,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
@override
void initState() {
super.initState();
_documentsCubit = BlocProvider.of<DocumentsCubit>(context);
_savedViewCubit = BlocProvider.of<SavedViewCubit>(context);
_documentsCubit = context.watch();
_savedViewCubit = context.watch();
try {
_documentsCubit.load();
} on PaperlessServerException catch (error, stackTrace) {
@@ -249,8 +249,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
MaterialPageRoute<DocumentModel?> _buildDetailsPageRoute(
DocumentModel document) {
return MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: DocumentDetailsCubit(getIt<PaperlessDocumentsApi>(), document),
builder: (_) => BlocProvider(
create: (context) => DocumentDetailsCubit(
context.watch(),
document,
),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(),
),

View File

@@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:provider/provider.dart';
import 'package:shimmer/shimmer.dart';
class DocumentPreview extends StatelessWidget {
@@ -30,14 +30,15 @@ class DocumentPreview extends StatelessWidget {
fit: fit,
alignment: Alignment.topCenter,
cacheKey: "thumb_$id",
imageUrl: getIt<PaperlessDocumentsApi>().getThumbnailUrl(id),
imageUrl:
Provider.of<PaperlessDocumentsApi>(context).getThumbnailUrl(id),
errorWidget: (ctxt, msg, __) => Text(msg),
placeholder: (context, value) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: const SizedBox(height: 100, width: 100),
),
cacheManager: getIt<CacheManager>(),
cacheManager: context.watch(),
),
// ),
);

View File

@@ -28,7 +28,7 @@ class SortDocumentsButton extends StatelessWidget {
topRight: Radius.circular(16),
),
),
builder: (_) => BlocProvider.value(
builder: (_) => BlocProvider<DocumentsCubit>.value(
value: BlocProvider.of<DocumentsCubit>(context),
child: FractionallySizedBox(
heightFactor: .6,

View File

@@ -5,7 +5,6 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart';
@@ -15,6 +14,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/util.dart';
import 'package:provider/provider.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@@ -56,7 +56,8 @@ class _HomePageState extends State<HomePage> {
MultiBlocProvider(
providers: [
BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
value:
DocumentsCubit(Provider.of<PaperlessDocumentsApi>(context)),
),
BlocProvider(
create: (context) => SavedViewCubit(
@@ -70,8 +71,10 @@ class _HomePageState extends State<HomePage> {
value: _scannerCubit,
child: const ScannerPage(),
),
BlocProvider.value(
value: DocumentsCubit(getIt<PaperlessDocumentsApi>()),
BlocProvider(
create: (context) => DocumentsCubit(
Provider.of<PaperlessDocumentsApi>(context),
),
child: const LabelsPage(),
),
][_currentIndex],
@@ -81,13 +84,12 @@ class _HomePageState extends State<HomePage> {
void _initializeData(BuildContext context) {
try {
RepositoryProvider.of<LabelRepository<Tag>>(context).findAll();
RepositoryProvider.of<LabelRepository<Correspondent>>(context).findAll();
RepositoryProvider.of<LabelRepository<DocumentType>>(context).findAll();
RepositoryProvider.of<LabelRepository<StoragePath>>(context).findAll();
RepositoryProvider.of<SavedViewRepository>(context).findAll();
BlocProvider.of<PaperlessServerInformationCubit>(context)
.updateInformtion();
context.read<LabelRepository<Tag>>().findAll();
context.read<LabelRepository<Correspondent>>().findAll();
context.read<LabelRepository<DocumentType>>().findAll();
context.read<LabelRepository<StoragePath>>().findAll();
context.read<SavedViewRepository>().findAll();
context.read<PaperlessServerInformationCubit>().updateInformtion();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
@@ -7,7 +8,6 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
@@ -16,14 +16,29 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:collection/collection.dart';
class InfoDrawer extends StatelessWidget {
class InfoDrawer extends StatefulWidget {
final VoidCallback? afterInboxClosed;
const InfoDrawer({Key? key, this.afterInboxClosed}) : super(key: key);
@override
State<InfoDrawer> createState() => _InfoDrawerState();
}
class _InfoDrawerState extends State<InfoDrawer> {
late final Future<PackageInfo> _packageInfo;
@override
void initState() {
super.initState();
_packageInfo = PackageInfo.fromPlatform();
}
@override
Widget build(BuildContext context) {
return ClipRRect(
@@ -140,8 +155,9 @@ class InfoDrawer extends StatelessWidget {
),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: getIt<ApplicationSettingsCubit>(),
builder: (context) => BlocProvider(
create: (context) =>
Provider.of<ApplicationSettingsCubit>(context),
child: const SettingsPage(),
),
),
@@ -155,44 +171,51 @@ class InfoDrawer extends StatelessWidget {
'https://github.com/astubenbord/paperless-mobile/issues/new');
},
),
AboutListTile(
icon: const Icon(Icons.info),
applicationIcon: const ImageIcon(
AssetImage('assets/logos/paperless_logo_green.png')),
applicationName: 'Paperless Mobile',
applicationVersion:
kPackageInfo.version + '+' + kPackageInfo.buildNumber,
aboutBoxChildren: [
Text(
'${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'),
Link(
uri: Uri.parse(
'https://github.com/astubenbord/paperless-mobile'),
builder: (context, followLink) => GestureDetector(
onTap: followLink,
child: Text(
'https://github.com/astubenbord/paperless-mobile',
style: TextStyle(
color: Theme.of(context).colorScheme.tertiary),
FutureBuilder<PackageInfo>(
future: _packageInfo,
builder: (context, snapshot) {
return AboutListTile(
icon: const Icon(Icons.info),
applicationIcon: const ImageIcon(
AssetImage('assets/logos/paperless_logo_green.png'),
),
),
),
const SizedBox(height: 16),
Text(
'Credits',
style: Theme.of(context).textTheme.titleMedium,
),
_buildOnboardingImageCredits(),
],
child: Text(S.of(context).appDrawerAboutLabel),
),
applicationName: 'Paperless Mobile',
applicationVersion: (snapshot.data?.version ?? '') +
'+' +
(snapshot.data?.buildNumber ?? ''),
aboutBoxChildren: [
Text(
'${S.of(context).aboutDialogDevelopedByText} Anton Stubenbord'),
Link(
uri: Uri.parse(
'https://github.com/astubenbord/paperless-mobile'),
builder: (context, followLink) => GestureDetector(
onTap: followLink,
child: Text(
'https://github.com/astubenbord/paperless-mobile',
style: TextStyle(
color:
Theme.of(context).colorScheme.tertiary),
),
),
),
const SizedBox(height: 16),
Text(
'Credits',
style: Theme.of(context).textTheme.titleMedium,
),
_buildOnboardingImageCredits(),
],
child: Text(S.of(context).appDrawerAboutLabel),
);
}),
ListTile(
leading: const Icon(Icons.logout),
title: Text(S.of(context).appDrawerLogoutLabel),
onTap: () {
try {
BlocProvider.of<AuthenticationCubit>(context).logout();
getIt<LocalVault>().clear();
Provider.of<LocalVault>(context).clear();
BlocProvider.of<ApplicationSettingsCubit>(context).clear();
RepositoryProvider.of<LabelRepository<Tag>>(context)
.clear();
@@ -224,7 +247,7 @@ class InfoDrawer extends StatelessWidget {
child: BlocProvider(
create: (context) => InboxCubit(
RepositoryProvider.of<LabelRepository<Tag>>(context),
getIt<PaperlessDocumentsApi>(),
Provider.of<PaperlessDocumentsApi>(context),
)..loadInbox(),
child: const InboxPage(),
),
@@ -232,7 +255,7 @@ class InfoDrawer extends StatelessWidget {
maintainState: false,
),
);
afterInboxClosed?.call();
widget.afterInboxClosed?.call();
}
Link _buildOnboardingImageCredits() {

View File

@@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
import 'package:provider/provider.dart';
class InboxItem extends StatelessWidget {
static const _a4AspectRatio = 1 / 1.4142;
@@ -48,9 +48,9 @@ class InboxItem extends StatelessWidget {
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: DocumentDetailsCubit(
getIt<PaperlessDocumentsApi>(),
builder: (_) => BlocProvider(
create: (context) => DocumentDetailsCubit(
Provider.of<PaperlessDocumentsApi>(context),
document,
),
child: const LabelRepositoriesProvider(

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents_preview/view/pages/linked_documents_page.dart';
import 'package:provider/provider.dart';
class LabelItem<T extends Label> extends StatelessWidget {
final T label;
@@ -45,9 +45,9 @@ class LabelItem<T extends Label> extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: LinkedDocumentsCubit(
getIt<PaperlessDocumentsApi>(),
builder: (context) => BlocProvider(
create: (context) => LinkedDocumentsCubit(
Provider.of<PaperlessDocumentsApi>(context),
filter,
),
child: const LinkedDocumentsPage(),

View File

@@ -3,13 +3,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/linked_documents_cubit.dart';
import 'package:paperless_mobile/features/linked_documents_preview/bloc/state/linked_documents_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';
class LinkedDocumentsPage extends StatefulWidget {
const LinkedDocumentsPage({super.key});
@@ -63,10 +63,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
BlocProvider<DocumentDetailsCubit>.value(
value: DocumentDetailsCubit(
getIt<PaperlessDocumentsApi>(),
builder: (context) => BlocProvider(
create: (context) => DocumentDetailsCubit(
Provider.of<PaperlessDocumentsApi>(
context),
document,
),
child: const DocumentDetailsPage(

View File

@@ -1,24 +1,28 @@
import 'dart:io';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/security/security_context_aware_dio_manager.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
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/settings/model/application_settings_state.dart';
class AuthenticationCubit extends Cubit<AuthenticationState> {
class AuthenticationCubit extends HydratedCubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
PaperlessAuthenticationApi _authApi;
final PaperlessAuthenticationApi _authApi;
final LocalVault _localVault;
final SecurityContextAwareDioManager _dioWrapper;
AuthenticationCubit(
this._localVault,
this._localAuthService,
this._authApi,
this._dioWrapper,
) : super(AuthenticationState.initial);
Future<void> login({
@@ -28,22 +32,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}) async {
assert(credentials.username != null && credentials.password != null);
try {
await registerSecurityContext(clientCertificate);
//TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive.
_authApi = getIt<PaperlessAuthenticationApi>();
// Store information required to make requests
final currentAuth = AuthenticationInformation(
serverUrl: serverUrl,
_dioWrapper.updateSettings(
baseUrl: serverUrl,
clientCertificate: clientCertificate,
);
await _localVault.storeAuthenticationInformation(currentAuth);
final token = await _authApi.login(
username: credentials.username!,
password: credentials.password!,
);
final auth = currentAuth.copyWith(token: token);
final auth = AuthenticationInformation(
serverUrl: serverUrl,
clientCertificate: clientCertificate,
token: token,
);
await _localVault.storeAuthenticationInformation(auth);
@@ -83,9 +86,9 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in");
if (localAuthSuccess) {
await registerSecurityContext(storedAuth.clientCertificate);
//TODO: Workaround for new architecture, listen for security context changes in timeout_client, possibly persisted in hive.
_authApi = getIt<PaperlessAuthenticationApi>();
_dioWrapper.updateSettings(
clientCertificate: storedAuth.clientCertificate,
);
return emit(
AuthenticationState(
isAuthenticated: true,
@@ -102,7 +105,9 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
));
}
} else {
await registerSecurityContext(storedAuth.clientCertificate);
_dioWrapper.updateSettings(
clientCertificate: storedAuth.clientCertificate,
);
final authState = AuthenticationState(
isAuthenticated: true,
authentication: storedAuth,
@@ -115,40 +120,14 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Future<void> logout() async {
await _localVault.clear();
await super.clear();
emit(AuthenticationState.initial);
}
}
class AuthenticationState {
final bool wasLoginStored;
final bool? wasLocalAuthenticationSuccessful;
final bool isAuthenticated;
final AuthenticationInformation? authentication;
static final AuthenticationState initial = AuthenticationState(
wasLoginStored: false,
isAuthenticated: false,
);
AuthenticationState({
required this.isAuthenticated,
required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
this.authentication,
});
AuthenticationState copyWith({
bool? wasLoginStored,
bool? isAuthenticated,
AuthenticationInformation? authentication,
bool? wasLocalAuthenticationSuccessful,
}) {
return AuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
wasLoginStored: wasLoginStored ?? this.wasLoginStored,
authentication: authentication ?? this.authentication,
wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ??
this.wasLocalAuthenticationSuccessful,
);
}
@override
AuthenticationState? fromJson(Map<String, dynamic> json) =>
AuthenticationState.fromJson(json);
@override
Map<String, dynamic>? toJson(AuthenticationState state) => state.toJson();
}

View File

@@ -0,0 +1,44 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
part 'authentication_state.g.dart';
@JsonSerializable()
class AuthenticationState {
final bool wasLoginStored;
final bool? wasLocalAuthenticationSuccessful;
final bool isAuthenticated;
final AuthenticationInformation? authentication;
static final AuthenticationState initial = AuthenticationState(
wasLoginStored: false,
isAuthenticated: false,
);
AuthenticationState({
required this.isAuthenticated,
required this.wasLoginStored,
this.wasLocalAuthenticationSuccessful,
this.authentication,
});
AuthenticationState copyWith({
bool? wasLoginStored,
bool? isAuthenticated,
AuthenticationInformation? authentication,
bool? wasLocalAuthenticationSuccessful,
}) {
return AuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
wasLoginStored: wasLoginStored ?? this.wasLoginStored,
authentication: authentication ?? this.authentication,
wasLocalAuthenticationSuccessful: wasLocalAuthenticationSuccessful ??
this.wasLocalAuthenticationSuccessful,
);
}
factory AuthenticationState.fromJson(Map<String, dynamic> json) =>
_$AuthenticationStateFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticationStateToJson(this);
}

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authentication_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuthenticationState _$AuthenticationStateFromJson(Map<String, dynamic> json) =>
AuthenticationState(
isAuthenticated: json['isAuthenticated'] as bool,
wasLoginStored: json['wasLoginStored'] as bool,
wasLocalAuthenticationSuccessful:
json['wasLocalAuthenticationSuccessful'] as bool?,
authentication: json['authentication'] == null
? null
: AuthenticationInformation.fromJson(
json['authentication'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AuthenticationStateToJson(
AuthenticationState instance) =>
<String, dynamic>{
'wasLoginStored': instance.wasLoginStored,
'wasLocalAuthenticationSuccessful':
instance.wasLocalAuthenticationSuccessful,
'isAuthenticated': instance.isAuthenticated,
'authentication': instance.authentication,
};

View File

@@ -1,11 +1,10 @@
import 'package:paperless_mobile/core/type/types.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
class AuthenticationInformation {
static const tokenKey = 'token';
static const serverUrlKey = 'serverUrl';
static const clientCertificateKey = 'clientCertificate';
part 'authentication_information.g.dart';
@JsonSerializable()
class AuthenticationInformation {
final String? token;
final String serverUrl;
final ClientCertificate? clientCertificate;
@@ -16,21 +15,6 @@ class AuthenticationInformation {
this.clientCertificate,
});
AuthenticationInformation.fromJson(JSON json)
: token = json[tokenKey],
serverUrl = json[serverUrlKey],
clientCertificate = json[clientCertificateKey] != null
? ClientCertificate.fromJson(json[clientCertificateKey])
: null;
JSON toJson() {
return {
tokenKey: token,
serverUrlKey: serverUrl,
clientCertificateKey: clientCertificate?.toJson(),
};
}
bool get isValid {
return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false);
}
@@ -48,4 +32,9 @@ class AuthenticationInformation {
(removeClientCertificate ? null : this.clientCertificate),
);
}
factory AuthenticationInformation.fromJson(Map<String, dynamic> json) =>
_$AuthenticationInformationFromJson(json);
Map<String, dynamic> toJson() => _$AuthenticationInformationToJson(this);
}

View File

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authentication_information.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuthenticationInformation _$AuthenticationInformationFromJson(
Map<String, dynamic> json) =>
AuthenticationInformation(
token: json['token'] as String?,
serverUrl: json['serverUrl'] as String,
clientCertificate: json['clientCertificate'] == null
? null
: ClientCertificate.fromJson(
json['clientCertificate'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AuthenticationInformationToJson(
AuthenticationInformation instance) =>
<String, dynamic>{
'token': instance.token,
'serverUrl': instance.serverUrl,
'clientCertificate': instance.clientCertificate,
};

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:provider/provider.dart';
class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress";
@@ -64,7 +64,8 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
//https://stackoverflow.com/questions/49648022/check-whether-there-is-an-internet-connection-available-on-flutter-app
setState(() => _reachabilityStatus = ReachabilityStatus.testing);
final isReachable =
await getIt<ConnectivityStatusService>().isServerReachable(address);
await Provider.of<ConnectivityStatusService>(context, listen: false)
.isServerReachable(address);
if (isReachable) {
setState(() => _reachabilityStatus = ReachabilityStatus.reachable);
} else {

View File

@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
class DocumentScannerCubit extends Cubit<List<File>> {

View File

@@ -16,7 +16,6 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
@@ -28,6 +27,7 @@ import 'package:paperless_mobile/util.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class ScannerPage extends StatefulWidget {
const ScannerPage({Key? key}) : super(key: key);
@@ -139,8 +139,8 @@ class _ScannerPageState extends State<ScannerPage>
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
localVault: Provider.of<LocalVault>(context),
documentApi: Provider.of<PaperlessDocumentsApi>(context),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
@@ -274,8 +274,8 @@ class _ScannerPageState extends State<ScannerPage>
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
localVault: Provider.of<LocalVault>(context),
documentApi: Provider.of<PaperlessDocumentsApi>(context),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,

View File

@@ -1,24 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:injectable/injectable.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
@prod
@test
@lazySingleton
class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
final LocalVault localVault;
ApplicationSettingsCubit(this.localVault)
: super(ApplicationSettingsState.defaultSettings);
Future<void> initialize() async {
final settings = (await localVault.loadApplicationSettings()) ??
ApplicationSettingsState.defaultSettings;
emit(settings);
}
class ApplicationSettingsCubit extends HydratedCubit<ApplicationSettingsState> {
ApplicationSettingsCubit() : super(ApplicationSettingsState.defaultSettings);
Future<void> setLocale(String? localeSubtag) async {
final updatedSettings = state.copyWith(preferredLocaleSubtag: localeSubtag);
@@ -42,11 +28,20 @@ class ApplicationSettingsCubit extends Cubit<ApplicationSettingsState> {
}
Future<void> _updateSettings(ApplicationSettingsState settings) async {
await localVault.storeApplicationSettings(settings);
emit(settings);
}
void clear() {
@override
Future<void> clear() async {
await super.clear();
emit(ApplicationSettingsState.defaultSettings);
}
@override
ApplicationSettingsState? fromJson(Map<String, dynamic> json) =>
ApplicationSettingsState.fromJson(json);
@override
Map<String, dynamic>? toJson(ApplicationSettingsState state) =>
state.toJson();
}

View File

@@ -1,71 +1,45 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'application_settings_state.g.dart';
///
/// State holding the current application settings such as selected language, theme mode and more.
///
///
@JsonSerializable()
class ApplicationSettingsState {
static final defaultSettings = ApplicationSettingsState(
isLocalAuthenticationEnabled: false,
preferredLocaleSubtag: Platform.localeName.split('_').first,
preferredThemeMode: ThemeMode.system,
preferredViewType: ViewType.list,
showInboxOnStartup: true,
);
static const isLocalAuthenticationEnabledKey = "isLocalAuthenticationEnabled";
static const preferredLocaleSubtagKey = "localeSubtag";
static const preferredThemeModeKey = "preferredThemeModeKey";
static const preferredViewTypeKey = 'preferredViewType';
static const showInboxOnStartupKey = 'showinboxOnStartup';
final bool isLocalAuthenticationEnabled;
final String preferredLocaleSubtag;
final ThemeMode preferredThemeMode;
final ViewType preferredViewType;
final bool showInboxOnStartup;
ApplicationSettingsState({
required this.preferredLocaleSubtag,
required this.preferredThemeMode,
required this.isLocalAuthenticationEnabled,
required this.preferredViewType,
required this.showInboxOnStartup,
});
JSON toJson() {
return {
isLocalAuthenticationEnabledKey: isLocalAuthenticationEnabled,
preferredLocaleSubtagKey: preferredLocaleSubtag,
preferredThemeModeKey: preferredThemeMode.name,
preferredViewTypeKey: preferredViewType.name,
};
}
ApplicationSettingsState.fromJson(JSON json)
: isLocalAuthenticationEnabled = json[isLocalAuthenticationEnabledKey] ??
defaultSettings.isLocalAuthenticationEnabled,
preferredLocaleSubtag = json[preferredLocaleSubtagKey] ??
defaultSettings.preferredLocaleSubtag,
preferredThemeMode = json.containsKey(preferredThemeModeKey)
? ThemeMode.values.byName(json[preferredThemeModeKey])
: defaultSettings.preferredThemeMode,
preferredViewType = json.containsKey(preferredViewTypeKey)
? ViewType.values.byName(json[preferredViewTypeKey])
: defaultSettings.preferredViewType,
showInboxOnStartup =
json[showInboxOnStartupKey] ?? defaultSettings.showInboxOnStartup;
Map<String, dynamic> toJson() => _$ApplicationSettingsStateToJson(this);
factory ApplicationSettingsState.fromJson(Map<String, dynamic> json) =>
_$ApplicationSettingsStateFromJson(json);
ApplicationSettingsState copyWith({
bool? isLocalAuthenticationEnabled,
String? preferredLocaleSubtag,
ThemeMode? preferredThemeMode,
ViewType? preferredViewType,
bool? showInboxOnStartup,
}) {
return ApplicationSettingsState(
isLocalAuthenticationEnabled:
@@ -74,7 +48,6 @@ class ApplicationSettingsState {
preferredLocaleSubtag ?? this.preferredLocaleSubtag,
preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode,
preferredViewType: preferredViewType ?? this.preferredViewType,
showInboxOnStartup: showInboxOnStartup ?? this.showInboxOnStartup,
);
}
}

View File

@@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'application_settings_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ApplicationSettingsState _$ApplicationSettingsStateFromJson(
Map<String, dynamic> json) =>
ApplicationSettingsState(
preferredLocaleSubtag: json['preferredLocaleSubtag'] as String,
preferredThemeMode:
$enumDecode(_$ThemeModeEnumMap, json['preferredThemeMode']),
isLocalAuthenticationEnabled:
json['isLocalAuthenticationEnabled'] as bool,
preferredViewType:
$enumDecode(_$ViewTypeEnumMap, json['preferredViewType']),
);
Map<String, dynamic> _$ApplicationSettingsStateToJson(
ApplicationSettingsState instance) =>
<String, dynamic>{
'isLocalAuthenticationEnabled': instance.isLocalAuthenticationEnabled,
'preferredLocaleSubtag': instance.preferredLocaleSubtag,
'preferredThemeMode': _$ThemeModeEnumMap[instance.preferredThemeMode]!,
'preferredViewType': _$ViewTypeEnumMap[instance.preferredViewType]!,
};
const _$ThemeModeEnumMap = {
ThemeMode.system: 'system',
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};
const _$ViewTypeEnumMap = {
ViewType.grid: 'grid',
ViewType.list: 'list',
};

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';
class BiometricAuthenticationSetting extends StatelessWidget {
const BiometricAuthenticationSetting({super.key});
@@ -28,8 +28,9 @@ class BiometricAuthenticationSetting extends StatelessWidget {
: S
.of(context)
.appSettingsDisableBiometricAuthenticationReasonText;
final changeValue = await getIt<LocalAuthenticationService>()
.authenticateLocalUser(localizedReason);
final changeValue =
await Provider.of<LocalAuthenticationService>(context)
.authenticateLocalUser(localizedReason);
if (changeValue) {
settingsBloc.setIsBiometricAuthenticationEnabled(val);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:provider/provider.dart';
class ClearStorageSetting extends StatelessWidget {
const ClearStorageSetting({super.key});
@@ -12,12 +12,10 @@ class ClearStorageSetting extends StatelessWidget {
title: Text("Clear data"),
subtitle:
Text("Remove downloaded files, scans and clear the cache's content"),
onTap: _clearCache,
onTap: () {
Provider.of<cm.CacheManager>(context).emptyCache();
FileService.clearUserData();
},
);
}
void _clearCache() async {
getIt<cm.CacheManager>().emptyCache();
FileService.clearUserData();
}
}

View File

@@ -1,25 +1,28 @@
import 'dart:developer';
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:http/io_client.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl_standalone.dart';
import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/global/constants.dart';
import 'package:paperless_mobile/core/global/http_self_signed_certificate_override.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/model/paperless_statistics_state.dart';
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
import 'package:paperless_mobile/core/repository/impl/saved_view_repository_impl.dart';
@@ -27,78 +30,146 @@ import 'package:paperless_mobile/core/repository/impl/storage_path_repository_im
import 'package:paperless_mobile/core/repository/impl/tag_repository_impl.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
import 'package:paperless_mobile/core/security/security_context_aware_dio_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status.service.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/di_initializer.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
void main() async {
Bloc.observer = BlocChangesObserver();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
Intl.systemLocale = await findSystemLocale();
await findSystemLocale();
// Required for self signed client certificates
HttpOverrides.global = X509HttpOverrides();
final dioWrapper = SecurityContextAwareDioManager();
IOClient httpClient = IOClient();
configureDependencies('prod');
dioWrapper.securityContextChanges.listen(
(context) => httpClient = IOClient(HttpClient(context: context)),
);
// Initialize External dependencies
final connectivity = Connectivity();
final encryptedSharedPreferences = EncryptedSharedPreferences();
final localAuthentication = LocalAuthentication();
final cacheManager = cm.CacheManager(cm.Config('cacheKey',
fileService: cm.HttpFileService(httpClient: httpClient)));
// Initialize Paperless APIs
final authApi = PaperlessAuthenticationApiImpl(dioWrapper.client);
final documentsApi = PaperlessDocumentsApiImpl(dioWrapper.client);
final labelsApi = PaperlessLabelApiImpl(dioWrapper.client);
final statsApi = PaperlessServerStatsApiImpl(dioWrapper.client);
final savedViewsApi = PaperlessSavedViewsApiImpl(dioWrapper.client);
// Initialize other utility classes
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
final localVault = LocalVaultImpl(encryptedSharedPreferences);
final localAuthService =
LocalAuthenticationService(localVault, localAuthentication);
// Initialize Repositories
// Initialize Blocs/Cubits
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
// Remove temporarily downloaded files.
(await FileService.temporaryDirectory).deleteSync(recursive: true);
kPackageInfo = await PackageInfo.fromPlatform();
// Load application settings and stored authentication data
await getIt<ConnectivityCubit>().initialize();
await getIt<ApplicationSettingsCubit>().initialize();
await connectivityCubit.initialize();
final authCubit = AuthenticationCubit(
getIt<LocalVault>(),
getIt<LocalAuthenticationService>(),
getIt<PaperlessAuthenticationApi>(),
localVault,
localAuthService,
authApi,
dioWrapper,
);
await authCubit.restoreSessionState();
//TODO: Check if hydrated cubit restores state.
//await authCubit.restoreSessionState();
// Create repositories
final LabelRepository<Tag> tagRepository =
TagRepositoryImpl(getIt<PaperlessLabelsApi>());
final LabelRepository<Correspondent> correspondentRepository =
CorrespondentRepositoryImpl(getIt<PaperlessLabelsApi>());
final LabelRepository<DocumentType> documentTypeRepository =
DocumentTypeRepositoryImpl(getIt<PaperlessLabelsApi>());
final LabelRepository<StoragePath> storagePathRepository =
StoragePathRepositoryImpl(getIt<PaperlessLabelsApi>());
final SavedViewRepository savedViewRepository =
SavedViewRepositoryImpl(getIt<PaperlessSavedViewsApi>());
final tagRepository = TagRepositoryImpl(labelsApi);
final correspondentRepository = CorrespondentRepositoryImpl(labelsApi);
final documentTypeRepository = DocumentTypeRepositoryImpl(labelsApi);
final storagePathRepository = StoragePathRepositoryImpl(labelsApi);
final savedViewRepository = SavedViewRepositoryImpl(savedViewsApi);
runApp(
MultiRepositoryProvider(
MultiProvider(
providers: [
RepositoryProvider.value(value: tagRepository),
RepositoryProvider.value(value: correspondentRepository),
RepositoryProvider.value(value: documentTypeRepository),
RepositoryProvider.value(value: storagePathRepository),
RepositoryProvider.value(value: savedViewRepository),
Provider<PaperlessAuthenticationApi>.value(value: authApi),
Provider<PaperlessDocumentsApi>.value(value: documentsApi),
Provider<PaperlessLabelsApi>.value(value: labelsApi),
Provider<PaperlessServerStatsApi>.value(value: statsApi),
Provider<PaperlessSavedViewsApi>.value(value: savedViewsApi),
Provider<cm.CacheManager>.value(value: cacheManager),
Provider<LocalVault>.value(value: localVault),
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService,
),
],
child: PaperlessMobileEntrypoint(authenticationCubit: authCubit),
child: MultiRepositoryProvider(
providers: [
RepositoryProvider<LabelRepository<Tag>>.value(
value: tagRepository,
),
RepositoryProvider<LabelRepository<Correspondent>>.value(
value: correspondentRepository,
),
RepositoryProvider<LabelRepository<DocumentType>>.value(
value: documentTypeRepository,
),
RepositoryProvider<LabelRepository<StoragePath>>.value(
value: storagePathRepository,
),
RepositoryProvider<SavedViewRepository>.value(
value: savedViewRepository,
),
],
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => AuthenticationCubit(
localVault,
localAuthService,
authApi,
dioWrapper,
),
),
BlocProvider<ConnectivityCubit>.value(
value: connectivityCubit,
),
],
child: const PaperlessMobileEntrypoint(),
),
),
),
);
}
class PaperlessMobileEntrypoint extends StatefulWidget {
final AuthenticationCubit authenticationCubit;
const PaperlessMobileEntrypoint({
Key? key,
required this.authenticationCubit,
}) : super(key: key);
@override
@@ -153,14 +224,14 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ConnectivityCubit>.value(
value: getIt<ConnectivityCubit>(),
BlocProvider(
create: (context) => ConnectivityCubit(context.watch()),
),
BlocProvider<PaperlessServerInformationCubit>.value(
value: getIt<PaperlessServerInformationCubit>(),
BlocProvider(
create: (context) => PaperlessServerInformationCubit(context.watch()),
),
BlocProvider<ApplicationSettingsCubit>.value(
value: getIt<ApplicationSettingsCubit>(),
BlocProvider(
create: (context) => ApplicationSettingsCubit(),
),
],
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
@@ -188,10 +259,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
],
home: BlocProvider.value(
value: widget.authenticationCubit,
child: const AuthenticationWrapper(),
),
home: const AuthenticationWrapper(),
);
},
),
@@ -247,19 +315,11 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (BuildContext context) => DocumentUploadCubit(
localVault: getIt<LocalVault>(),
documentApi: getIt<PaperlessDocumentsApi>(),
tagRepository: RepositoryProvider.of<LabelRepository<Tag>>(
context,
),
correspondentRepository:
RepositoryProvider.of<LabelRepository<Correspondent>>(
context,
),
documentTypeRepository:
RepositoryProvider.of<LabelRepository<DocumentType>>(
context,
),
localVault: context.watch(),
documentApi: context.watch(),
tagRepository: context.watch(),
correspondentRepository: context.watch(),
documentTypeRepository: context.watch(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
@@ -349,13 +409,12 @@ class BiometricAuthenticationPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () =>
BlocProvider.of<AuthenticationCubit>(context).logout(),
onPressed: () => context.read<AuthenticationCubit>().logout(),
child: const Text("Log out"),
),
ElevatedButton(
onPressed: () => BlocProvider.of<AuthenticationCubit>(context)
.restoreSessionState(),
onPressed: () =>
context.read<AuthenticationCubit>().restoreSessionState(),
child: const Text("Authenticate"),
),
],

View File

@@ -14,7 +14,6 @@ import 'package:paperless_mobile/generated/l10n.dart';
final dateFormat = DateFormat("yyyy-MM-dd");
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
late PackageInfo kPackageInfo;
class SnackBarActionConfig {
final String label;

View File

@@ -1,12 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart';
class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
final BaseClient client;
final Dio client;
PaperlessAuthenticationApiImpl(this.client);
@@ -18,8 +15,8 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
late Response response;
try {
response = await client.post(
Uri.parse("/api/token/"),
body: {
"/api/token/",
data: {
"username": username,
"password": password,
},
@@ -34,11 +31,11 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
);
}
}
if (response.statusCode == HttpStatus.ok) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
return data['token'];
} else if (response.statusCode == HttpStatus.badRequest &&
response.body
if (response.statusCode == 200) {
return response.data['token'];
} else if (response.statusCode == 400 &&
response
.data //TODO: Check if text is included in statusMessage instead of body
.toLowerCase()
.contains("no required certificate was sent")) {
throw PaperlessServerException(

View File

@@ -12,12 +12,10 @@ abstract class PaperlessDocumentsApi {
Uint8List documentBytes, {
required String filename,
required String title,
required String authToken,
required String serverUrl,
DateTime? createdAt,
int? documentType,
int? correspondent,
Iterable<int> tags = const [],
DateTime? createdAt,
});
Future<DocumentModel> update(DocumentModel doc);
Future<int> findNextAsn();

View File

@@ -1,94 +1,42 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:http/src/boundary_characters.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/models/bulk_edit_model.dart';
import 'package:paperless_api/src/models/document_filter.dart';
import 'package:paperless_api/src/models/document_meta_data_model.dart';
import 'package:paperless_api/src/models/document_model.dart';
import 'package:paperless_api/src/models/paged_search_result.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/models/query_parameters/sort_field.dart';
import 'package:paperless_api/src/models/query_parameters/sort_order.dart';
import 'package:paperless_api/src/models/similar_document_model.dart';
import 'paperless_documents_api.dart';
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final BaseClient baseClient;
final HttpClient httpClient;
final Dio client;
PaperlessDocumentsApiImpl(this.baseClient, this.httpClient);
PaperlessDocumentsApiImpl(this.client);
@override
Future<void> create(
Uint8List documentBytes, {
required String filename,
required String title,
required String authToken,
required String serverUrl,
DateTime? createdAt,
int? documentType,
int? correspondent,
Iterable<int> tags = const [],
DateTime? createdAt,
}) async {
// The multipart request has to be generated from scratch as the http library does
// not allow the same key (tags) to be added multiple times. However, this is what the
// paperless api expects, i.e. one block for each tag.
final request = await httpClient.postUrl(
Uri.parse("$serverUrl/api/documents/post_document/"),
);
final formData = FormData();
final boundary = _boundaryString();
StringBuffer bodyBuffer = StringBuffer();
var fields = <String, String>{};
fields.putIfAbsent('title', () => title);
formData.fields.add(MapEntry('title', title));
if (createdAt != null) {
fields.putIfAbsent('created', () => apiDateFormat.format(createdAt));
formData.fields.add(MapEntry('created', apiDateFormat.format(createdAt)));
}
if (correspondent != null) {
fields.putIfAbsent('correspondent', () => jsonEncode(correspondent));
formData.fields.add(MapEntry('correspondent', jsonEncode(correspondent)));
}
if (documentType != null) {
fields.putIfAbsent('document_type', () => jsonEncode(documentType));
formData.fields.add(MapEntry('document_type', jsonEncode(documentType)));
}
for (final key in fields.keys) {
bodyBuffer.write(_buildMultipartField(key, fields[key]!, boundary));
}
for (final tag in tags) {
bodyBuffer.write(_buildMultipartField('tags', tag.toString(), boundary));
formData.fields.add(MapEntry('tags', tag.toString()));
}
bodyBuffer.write("--$boundary"
'\r\nContent-Disposition: form-data; name="document"; filename="$filename"'
"\r\nContent-type: application/octet-stream"
"\r\n\r\n");
final closing = "\r\n--$boundary--\r\n";
// Set headers
request.headers.set(HttpHeaders.contentTypeHeader,
"multipart/form-data; boundary=$boundary");
request.headers.set(HttpHeaders.contentLengthHeader,
"${bodyBuffer.length + closing.length + documentBytes.lengthInBytes}");
request.headers.set(HttpHeaders.authorizationHeader, "Token $authToken");
//Write fields to request
request.write(bodyBuffer.toString());
//Stream file
await request.addStream(Stream.fromIterable(documentBytes.map((e) => [e])));
// Write closing boundary to request
request.write(closing);
final response = await request.close();
final response =
await client.post('/api/documents/post_document/', data: formData);
if (response.statusCode != 200) {
throw PaperlessServerException(
ErrorCode.documentUploadFailed,
@@ -97,38 +45,14 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
}
}
String _buildMultipartField(String fieldName, String value, String boundary) {
// ignore: prefer_interpolation_to_compose_strings
return '--$boundary'
'\r\nContent-Disposition: form-data; name="$fieldName"'
'\r\nContent-type: text/plain'
'\r\n\r\n' +
value +
'\r\n';
}
String _boundaryString() {
Random _random = Random();
var prefix = 'dart-http-boundary-';
var list = List<int>.generate(
70 - prefix.length,
(index) => boundaryCharacters[_random.nextInt(boundaryCharacters.length)],
growable: false,
);
return '$prefix${String.fromCharCodes(list)}';
}
@override
Future<DocumentModel> update(DocumentModel doc) async {
final response = await baseClient.put(
Uri.parse("/api/documents/${doc.id}/"),
body: json.encode(doc.toJson()),
headers: {"Content-Type": "application/json"},
final response = await client.put(
"/api/documents/${doc.id}/",
data: doc.toJson(),
);
if (response.statusCode == 200) {
return DocumentModel.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
);
return DocumentModel.fromJson(response.data);
} else {
throw const PaperlessServerException(ErrorCode.documentUpdateFailed);
}
@@ -137,17 +61,15 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override
Future<PagedSearchResult<DocumentModel>> find(DocumentFilter filter) async {
final filterParams = filter.toQueryParameters();
final response = await baseClient.get(
Uri(
path: "/api/documents/",
queryParameters: filterParams,
),
final response = await client.get(
"/api/documents/",
queryParameters: filterParams,
);
if (response.statusCode == 200) {
return compute(
PagedSearchResult.fromJson,
PagedSearchResultJsonSerializer<DocumentModel>(
jsonDecode(utf8.decode(response.bodyBytes)),
response.data,
DocumentModel.fromJson,
),
);
@@ -158,8 +80,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override
Future<int> delete(DocumentModel doc) async {
final response =
await baseClient.delete(Uri.parse("/api/documents/${doc.id}/"));
final response = await client.delete("/api/documents/${doc.id}/");
if (response.statusCode == 204) {
return Future.value(doc.id);
@@ -178,9 +99,14 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override
Future<Uint8List> getPreview(int documentId) async {
final response = await baseClient.get(Uri.parse(getPreviewUrl(documentId)));
final response = await client.get(
getPreviewUrl(documentId),
options: Options(
responseType:
ResponseType.bytes), //TODO: Check if bytes or stream is required
);
if (response.statusCode == 200) {
return response.bodyBytes;
return response.data;
}
throw const PaperlessServerException(ErrorCode.documentPreviewFailed);
}
@@ -207,10 +133,9 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override
Future<Iterable<int>> bulkAction(BulkAction action) async {
final response = await baseClient.post(
Uri.parse("/api/documents/bulk_edit/"),
body: json.encode(action.toJson()),
headers: {'Content-Type': 'application/json'},
final response = await client.post(
"/api/documents/bulk_edit/",
data: action.toJson(),
);
if (response.statusCode == 200) {
return action.documentIds;
@@ -241,40 +166,48 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
@override
Future<Uint8List> download(DocumentModel document) async {
final response = await baseClient
.get(Uri.parse("/api/documents/${document.id}/download/"));
return response.bodyBytes;
//TODO: Add missing error handling
final response = await client.get(
"/api/documents/${document.id}/download/",
options: Options(responseType: ResponseType.bytes),
);
return response.data;
}
@override
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
final response = await baseClient
.get(Uri.parse("/api/documents/${document.id}/metadata/"));
final response =
await client.get("/api/documents/${document.id}/metadata/");
return compute(
DocumentMetaData.fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
response.data as Map<String, dynamic>,
);
}
@override
Future<List<String>> autocomplete(String query, [int limit = 10]) async {
final response = await baseClient
.get(Uri.parse("/api/search/autocomplete/?query=$query&limit=$limit}"));
final response = await client.get(
'/api/search/autocomplete/',
queryParameters: {
'query': query,
'limit': limit,
},
);
if (response.statusCode == 200) {
return jsonDecode(utf8.decode(response.bodyBytes)) as List<String>;
return response.data as List<String>;
}
throw const PaperlessServerException(ErrorCode.autocompleteQueryError);
}
@override
Future<List<SimilarDocumentModel>> findSimilar(int docId) async {
final response = await baseClient
.get(Uri.parse("/api/documents/?more_like=$docId&pageSize=10"));
final response =
await client.get("/api/documents/?more_like=$docId&pageSize=10");
if (response.statusCode == 200) {
return (await compute(
PagedSearchResult<SimilarDocumentModel>.fromJson,
PagedSearchResultJsonSerializer(
jsonDecode(utf8.decode(response.bodyBytes)),
response.data,
SimilarDocumentModel.fromJson,
),
))

View File

@@ -1,17 +1,19 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/labels/correspondent_model.dart';
import 'package:paperless_api/src/models/labels/document_type_model.dart';
import 'package:paperless_api/src/models/labels/storage_path_model.dart';
import 'package:paperless_api/src/models/labels/tag_model.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/modules/labels_api/paperless_labels_api.dart';
import 'package:paperless_api/src/utils.dart';
import 'package:paperless_api/src/request_utils.dart';
//Notes:
// Removed content type json header
class PaperlessLabelApiImpl implements PaperlessLabelsApi {
final BaseClient client;
final Dio client;
PaperlessLabelApiImpl(this.client);
@override
@@ -89,15 +91,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<Correspondent> saveCorrespondent(Correspondent correspondent) async {
final response = await client.post(
Uri.parse('/api/correspondents/'),
body: jsonEncode(correspondent.toJson()),
headers: {"Content-Type": "application/json"},
encoding: Encoding.getByName("utf-8"),
'/api/correspondents/',
data: correspondent.toJson(),
);
if (response.statusCode == HttpStatus.created) {
return Correspondent.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)),
);
return Correspondent.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.correspondentCreateFailed,
@@ -108,15 +106,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<DocumentType> saveDocumentType(DocumentType type) async {
final response = await client.post(
Uri.parse('/api/document_types/'),
body: json.encode(type.toJson()),
headers: {"Content-Type": "application/json"},
encoding: Encoding.getByName("utf-8"),
'/api/document_types/',
data: type.toJson(),
);
if (response.statusCode == HttpStatus.created) {
return DocumentType.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)),
);
return DocumentType.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.documentTypeCreateFailed,
@@ -126,18 +120,13 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<Tag> saveTag(Tag tag) async {
final body = json.encode(tag.toJson());
final response = await client.post(
Uri.parse('/api/tags/'),
body: body,
headers: {
"Content-Type": "application/json",
"Accept": "application/json; version=2",
},
encoding: Encoding.getByName("utf-8"),
'/api/tags/',
data: tag.toJson(),
options: Options(headers: {"Accept": "application/json; version=2"}),
);
if (response.statusCode == HttpStatus.created) {
return Tag.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
return Tag.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.tagCreateFailed,
@@ -148,8 +137,8 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<int> deleteCorrespondent(Correspondent correspondent) async {
assert(correspondent.id != null);
final response = await client
.delete(Uri.parse('/api/correspondents/${correspondent.id}/'));
final response =
await client.delete('/api/correspondents/${correspondent.id}/');
if (response.statusCode == HttpStatus.noContent) {
return correspondent.id!;
}
@@ -162,8 +151,8 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<int> deleteDocumentType(DocumentType documentType) async {
assert(documentType.id != null);
final response = await client
.delete(Uri.parse('/api/document_types/${documentType.id}/'));
final response =
await client.delete('/api/document_types/${documentType.id}/');
if (response.statusCode == HttpStatus.noContent) {
return documentType.id!;
}
@@ -176,7 +165,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<int> deleteTag(Tag tag) async {
assert(tag.id != null);
final response = await client.delete(Uri.parse('/api/tags/${tag.id}/'));
final response = await client.delete('/api/tags/${tag.id}/');
if (response.statusCode == HttpStatus.noContent) {
return tag.id!;
}
@@ -190,17 +179,14 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<Correspondent> updateCorrespondent(Correspondent correspondent) async {
assert(correspondent.id != null);
final response = await client.put(
Uri.parse('/api/correspondents/${correspondent.id}/'),
headers: {"Content-Type": "application/json"},
body: json.encode(correspondent.toJson()),
encoding: Encoding.getByName("utf-8"),
'/api/correspondents/${correspondent.id}/',
data: json.encode(correspondent.toJson()),
);
if (response.statusCode == HttpStatus.ok) {
return Correspondent.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)));
return Correspondent.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
ErrorCode.unknown, //TODO: Add correct error code mapping.
httpStatusCode: response.statusCode,
);
}
@@ -209,13 +195,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<DocumentType> updateDocumentType(DocumentType documentType) async {
assert(documentType.id != null);
final response = await client.put(
Uri.parse('/api/document_types/${documentType.id}/'),
headers: {"Content-Type": "application/json"},
body: json.encode(documentType.toJson()),
encoding: Encoding.getByName("utf-8"),
'/api/document_types/${documentType.id}/',
data: documentType.toJson(),
);
if (response.statusCode == HttpStatus.ok) {
return DocumentType.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
return DocumentType.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
@@ -227,16 +211,12 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<Tag> updateTag(Tag tag) async {
assert(tag.id != null);
final response = await client.put(
Uri.parse('/api/tags/${tag.id}/'),
headers: {
"Accept": "application/json; version=2",
"Content-Type": "application/json",
},
body: json.encode(tag.toJson()),
encoding: Encoding.getByName("utf-8"),
'/api/tags/${tag.id}/',
options: Options(headers: {"Accept": "application/json; version=2"}),
data: tag.toJson(),
);
if (response.statusCode == HttpStatus.ok) {
return Tag.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
return Tag.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.unknown,
@@ -247,8 +227,7 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<int> deleteStoragePath(StoragePath path) async {
assert(path.id != null);
final response =
await client.delete(Uri.parse('/api/storage_paths/${path.id}/'));
final response = await client.delete('/api/storage_paths/${path.id}/');
if (response.statusCode == HttpStatus.noContent) {
return path.id!;
}
@@ -285,12 +264,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
@override
Future<StoragePath> saveStoragePath(StoragePath path) async {
final response = await client.post(
Uri.parse('/api/storage_paths/'),
body: json.encode(path.toJson()),
headers: {"Content-Type": "application/json"},
'/api/storage_paths/',
data: path.toJson(),
);
if (response.statusCode == HttpStatus.created) {
return StoragePath.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
return StoragePath.fromJson(jsonDecode(response.data));
}
throw PaperlessServerException(ErrorCode.storagePathCreateFailed,
httpStatusCode: response.statusCode);
@@ -300,12 +278,11 @@ class PaperlessLabelApiImpl implements PaperlessLabelsApi {
Future<StoragePath> updateStoragePath(StoragePath path) async {
assert(path.id != null);
final response = await client.put(
Uri.parse('/api/storage_paths/${path.id}/'),
headers: {"Content-Type": "application/json"},
body: json.encode(path.toJson()),
'/api/storage_paths/${path.id}/',
data: path.toJson(),
);
if (response.statusCode == HttpStatus.ok) {
return StoragePath.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
return StoragePath.fromJson(jsonDecode(response.data));
}
throw const PaperlessServerException(ErrorCode.unknown);
}

View File

@@ -1,15 +1,15 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/models/saved_view_model.dart';
import 'package:paperless_api/src/utils.dart';
import 'package:paperless_api/src/request_utils.dart';
import 'paperless_saved_views_api.dart';
class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
final BaseClient client;
final Dio client;
PaperlessSavedViewsApiImpl(this.client);
@@ -28,12 +28,11 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
@override
Future<SavedView> save(SavedView view) async {
final response = await client.post(
Uri.parse("/api/saved_views/"),
body: jsonEncode(view.toJson()),
headers: {'Content-Type': 'application/json'},
"/api/saved_views/",
data: view.toJson(),
);
if (response.statusCode == HttpStatus.created) {
return SavedView.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
return SavedView.fromJson(response.data);
}
throw PaperlessServerException(
ErrorCode.createSavedViewError,
@@ -43,8 +42,7 @@ class PaperlessSavedViewsApiImpl implements PaperlessSavedViewsApi {
@override
Future<int> delete(SavedView view) async {
final response =
await client.delete(Uri.parse("/api/saved_views/${view.id}/"));
final response = await client.delete("/api/saved_views/${view.id}/");
if (response.statusCode == HttpStatus.noContent) {
return view.id!;
}

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:http/http.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/models/paperless_server_information_model.dart';
@@ -13,25 +14,24 @@ import 'paperless_server_stats_api.dart';
/// inbox and total number of documents.
///
class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
final BaseClient client;
final Dio client;
PaperlessServerStatsApiImpl(this.client);
@override
Future<PaperlessServerInformationModel> getServerInformation() async {
final response = await client.get(Uri.parse("/api/ui_settings/"));
final version =
response.headers[PaperlessServerInformationModel.versionHeader] ??
'unknown';
final apiVersion = int.tryParse(
response.headers[PaperlessServerInformationModel.apiVersionHeader] ??
'1');
final String username =
jsonDecode(utf8.decode(response.bodyBytes))['username'];
final response = await client.get("/api/ui_settings/");
final version = response
.headers[PaperlessServerInformationModel.versionHeader]?.first ??
'unknown';
final apiVersion = int.tryParse(response
.headers[PaperlessServerInformationModel.apiVersionHeader]?.first ??
'1');
final String username = response.data['username'];
final String host = response
.headers[PaperlessServerInformationModel.hostHeader] ??
response.request?.headers[PaperlessServerInformationModel.hostHeader] ??
('${response.request?.url.host}:${response.request?.url.port}');
.headers[PaperlessServerInformationModel.hostHeader]?.first ??
response.headers[PaperlessServerInformationModel.hostHeader]?.first ??
('${response.requestOptions.uri.host}:${response.requestOptions.uri.port}');
return PaperlessServerInformationModel(
username: username,
version: version,
@@ -42,11 +42,9 @@ class PaperlessServerStatsApiImpl implements PaperlessServerStatsApi {
@override
Future<PaperlessServerStatisticsModel> getServerStatistics() async {
final response = await client.get(Uri.parse('/api/statistics/'));
final response = await client.get('/api/statistics/');
if (response.statusCode == 200) {
return PaperlessServerStatisticsModel.fromJson(
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
);
return PaperlessServerStatisticsModel.fromJson(response.data);
}
throw const PaperlessServerException.unknown();
}

View File

@@ -1,25 +1,26 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
Future<T> getSingleResult<T>(
String url,
T Function(Map<String, dynamic>) fromJson,
ErrorCode errorCode, {
required BaseClient client,
required Dio client,
int minRequiredApiVersion = 1,
}) async {
final response = await client.get(
Uri.parse(url),
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
url,
options: Options(
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
),
);
if (response.statusCode == HttpStatus.ok) {
return compute(
fromJson,
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>,
response.data as Map<String, dynamic>,
);
}
throw PaperlessServerException(
@@ -32,16 +33,17 @@ Future<List<T>> getCollection<T>(
String url,
T Function(Map<String, dynamic>) fromJson,
ErrorCode errorCode, {
required BaseClient client,
required Dio client,
int minRequiredApiVersion = 1,
}) async {
final response = await client.get(
Uri.parse(url),
headers: {'accept': 'application/json; version=$minRequiredApiVersion'},
url,
options: Options(headers: {
'accept': 'application/json; version=$minRequiredApiVersion'
}),
);
if (response.statusCode == HttpStatus.ok) {
final Map<String, dynamic> body =
jsonDecode(utf8.decode(response.bodyBytes));
final Map<String, dynamic> body = response.data;
if (body.containsKey('count')) {
if (body['count'] == 0) {
return <T>[];

View File

@@ -17,7 +17,8 @@ dependencies:
http: ^0.13.5
json_annotation: ^4.7.0
intl: ^0.17.0
dio: ^4.0.6
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -385,6 +385,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
dots_indicator:
dependency: transitive
description:
@@ -665,7 +673,7 @@ packages:
source: sdk
version: "0.0.0"
get_it:
dependency: "direct main"
dependency: transitive
description:
name: get_it
sha256: "290fde3a86072e4b37dbb03c07bec6126f0ecc28dad403c12ffe2e5a2d751ab7"
@@ -736,6 +744,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
hydrated_bloc:
dependency: "direct main"
description:
name: hydrated_bloc
sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
image:
dependency: "direct main"
description:
@@ -753,7 +769,7 @@ packages:
source: hosted
version: "3.2.0"
injectable:
dependency: "direct main"
dependency: transitive
description:
name: injectable
sha256: "7dab7d341feb40a0590d9ff6261aea9495522005e2c6763f9161a4face916f7b"
@@ -814,13 +830,21 @@ packages:
source: hosted
version: "0.6.5"
json_annotation:
dependency: transitive
dependency: "direct main"
description:
name: json_annotation
sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c"
url: "https://pub.dev"
source: hosted
version: "4.7.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d
url: "https://pub.dev"
source: hosted
version: "6.5.4"
lints:
dependency: transitive
description:
@@ -1221,7 +1245,7 @@ packages:
source: hosted
version: "4.2.4"
provider:
dependency: transitive
dependency: "direct main"
description:
name: provider
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
@@ -1417,6 +1441,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.6"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
source_map_stack_trace:
dependency: transitive
description:

View File

@@ -32,8 +32,6 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
get_it: ^7.2.0
injectable: ^2.1.0
encrypted_shared_preferences: ^3.0.0
permission_handler: ^9.2.0
pdf: ^3.8.1
@@ -79,6 +77,11 @@ dependencies:
rxdart: ^0.27.7
badges: ^2.0.3
flutter_colorpicker: ^1.0.3
provider: ^6.0.5
dio: ^4.0.6
hydrated_bloc: ^9.0.0
json_annotation: ^4.7.0
dev_dependencies:
integration_test:
@@ -92,6 +95,7 @@ dev_dependencies:
dependency_validator: ^3.0.0
intl_utils: ^2.7.0
flutter_lints: ^1.0.0
json_serializable: ^6.5.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec