mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 21:15:50 -06:00
feat: Split settings, encrypted user credentials, preparations for multi user support
This commit is contained in:
47
lib/core/config/hive/custpm_adapters/theme_mode_adapter.dart
Normal file
47
lib/core/config/hive/custpm_adapters/theme_mode_adapter.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
|
||||||
|
class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
|
||||||
|
@override
|
||||||
|
final int typeId = HiveTypeIds.themeMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeMode read(BinaryReader reader) {
|
||||||
|
switch (reader.readByte()) {
|
||||||
|
case 0:
|
||||||
|
return ThemeMode.system;
|
||||||
|
case 1:
|
||||||
|
return ThemeMode.dark;
|
||||||
|
case 2:
|
||||||
|
return ThemeMode.light;
|
||||||
|
default:
|
||||||
|
return ThemeMode.system;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, ThemeMode obj) {
|
||||||
|
switch (obj) {
|
||||||
|
case ThemeMode.system:
|
||||||
|
writer.writeByte(0);
|
||||||
|
break;
|
||||||
|
case ThemeMode.light:
|
||||||
|
writer.writeByte(1);
|
||||||
|
break;
|
||||||
|
case ThemeMode.dark:
|
||||||
|
writer.writeByte(2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is ThemeModeAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
39
lib/core/config/hive/hive_config.dart
Normal file
39
lib/core/config/hive/hive_config.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/custpm_adapters/theme_mode_adapter.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/global_app_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
||||||
|
|
||||||
|
class HiveBoxes {
|
||||||
|
HiveBoxes._();
|
||||||
|
static const globalSettings = 'globalSettings';
|
||||||
|
static const userSettings = 'userSettings';
|
||||||
|
static const authentication = 'authentication';
|
||||||
|
static const vault = 'vault';
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiveTypeIds {
|
||||||
|
HiveTypeIds._();
|
||||||
|
static const globalSettings = 0;
|
||||||
|
static const userSettings = 1;
|
||||||
|
static const themeMode = 2;
|
||||||
|
static const colorSchemeOption = 3;
|
||||||
|
static const authentication = 4;
|
||||||
|
static const clientCertificate = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiveBoxSingleValueKey {
|
||||||
|
HiveBoxSingleValueKey._();
|
||||||
|
static const value = 'value';
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerHiveAdapters() {
|
||||||
|
Hive.registerAdapter(ColorSchemeOptionAdapter());
|
||||||
|
Hive.registerAdapter(ThemeModeAdapter());
|
||||||
|
Hive.registerAdapter(GlobalAppSettingsAdapter());
|
||||||
|
Hive.registerAdapter(UserAppSettingsAdapter());
|
||||||
|
Hive.registerAdapter(AuthenticationInformationAdapter());
|
||||||
|
Hive.registerAdapter(ClientCertificateAdapter());
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
|
||||||
|
|
||||||
extension AddressableHydratedStorage on Storage {
|
|
||||||
ApplicationSettingsState get settings {
|
|
||||||
return ApplicationSettingsState.fromJson(read('ApplicationSettingsCubit'));
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationState get authentication {
|
|
||||||
return AuthenticationState.fromJson(read('AuthenticationCubit'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -69,10 +69,7 @@ class AppDrawer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => Navigator.of(context).push(
|
onTap: () => Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BlocProvider.value(
|
builder: (context) => const SettingsPage(),
|
||||||
value: context.read<ApplicationSettingsCubit>(),
|
|
||||||
child: const SettingsPage(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ class ConfirmBulkModifyLabelDialog extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(S.of(context)!.confirmAction),
|
title: Text(S.of(context)!.confirmAction),
|
||||||
content: RichText(
|
content: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
text: content,
|
text: content,
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(text: "\n\n"),
|
const TextSpan(text: "\n\n"),
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ class ConfirmBulkModifyTagsDialog extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text(S.of(context)!.confirmAction),
|
title: Text(S.of(context)!.confirmAction),
|
||||||
content: RichText(
|
content: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
text: _buildText(context),
|
text: _buildText(context),
|
||||||
children: [
|
children: [
|
||||||
const TextSpan(text: "\n\n"),
|
const TextSpan(text: "\n\n"),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -69,10 +70,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
await context.read<DocumentDetailsCubit>().downloadDocument(
|
await context.read<DocumentDetailsCubit>().downloadDocument(
|
||||||
downloadOriginal: downloadOriginal,
|
downloadOriginal: downloadOriginal,
|
||||||
locale: context
|
locale: context.read<GlobalAppSettings>().preferredLocaleSubtag,
|
||||||
.read<ApplicationSettingsCubit>()
|
|
||||||
.state
|
|
||||||
.preferredLocaleSubtag,
|
|
||||||
);
|
);
|
||||||
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||||
import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
class DocumentListItem extends DocumentItem {
|
class DocumentListItem extends DocumentItem {
|
||||||
static const _a4AspectRatio = 1 / 1.4142;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
|
|
||||||
DocumentListItem({
|
const DocumentListItem({
|
||||||
super.key,
|
super.key,
|
||||||
required super.document,
|
required super.document,
|
||||||
required super.isSelected,
|
required super.isSelected,
|
||||||
@@ -28,9 +25,7 @@ class DocumentListItem extends DocumentItem {
|
|||||||
required super.correspondents,
|
required super.correspondents,
|
||||||
required super.documentTypes,
|
required super.documentTypes,
|
||||||
required super.storagePaths,
|
required super.storagePaths,
|
||||||
}) {
|
});
|
||||||
print(tags.keys.join(", "));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -22,42 +25,48 @@ class VerifyIdentityPage extends StatelessWidget {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
title: Text(S.of(context)!.verifyYourIdentity),
|
title: Text(S.of(context)!.verifyYourIdentity),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: UserSettingsBuilder(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
builder: (context, settings) {
|
||||||
children: [
|
if (settings == null) {
|
||||||
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
|
return const SizedBox.shrink();
|
||||||
.paddedSymmetrically(horizontal: 16),
|
}
|
||||||
const Icon(
|
return Column(
|
||||||
Icons.fingerprint,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
size: 96,
|
|
||||||
),
|
|
||||||
Wrap(
|
|
||||||
alignment: WrapAlignment.spaceBetween,
|
|
||||||
runAlignment: WrapAlignment.spaceBetween,
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
Text(S
|
||||||
onPressed: () => _logout(context),
|
.of(context)!
|
||||||
child: Text(
|
.useTheConfiguredBiometricFactorToAuthenticate)
|
||||||
S.of(context)!.disconnect,
|
.paddedSymmetrically(horizontal: 16),
|
||||||
style: TextStyle(
|
const Icon(
|
||||||
color: Theme.of(context).colorScheme.error,
|
Icons.fingerprint,
|
||||||
|
size: 96,
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.spaceBetween,
|
||||||
|
runAlignment: WrapAlignment.spaceBetween,
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _logout(context),
|
||||||
|
child: Text(
|
||||||
|
S.of(context)!.disconnect,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
ElevatedButton(
|
||||||
),
|
onPressed: () => context
|
||||||
ElevatedButton(
|
.read<AuthenticationCubit>()
|
||||||
onPressed: () => context
|
.restoreSessionState(),
|
||||||
.read<AuthenticationCubit>()
|
child: Text(S.of(context)!.verifyIdentity),
|
||||||
.restoreSessionState(context
|
),
|
||||||
.read<ApplicationSettingsCubit>()
|
],
|
||||||
.state
|
).padded(16),
|
||||||
.isLocalAuthenticationEnabled),
|
|
||||||
child: Text(S.of(context)!.verifyIdentity),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).padded(16),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
|
||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
part 'authentication_state.dart';
|
part 'authentication_state.dart';
|
||||||
part 'authentication_cubit.g.dart';
|
|
||||||
|
|
||||||
class AuthenticationCubit extends Cubit<AuthenticationState>
|
class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||||
with HydratedMixin<AuthenticationState> {
|
|
||||||
final LocalAuthenticationService _localAuthService;
|
final LocalAuthenticationService _localAuthService;
|
||||||
final PaperlessAuthenticationApi _authApi;
|
final PaperlessAuthenticationApi _authApi;
|
||||||
final SessionManager _dioWrapper;
|
final SessionManager _dioWrapper;
|
||||||
@@ -42,15 +47,31 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
|
|||||||
clientCertificate: clientCertificate,
|
clientCertificate: clientCertificate,
|
||||||
authToken: token,
|
authToken: token,
|
||||||
);
|
);
|
||||||
|
final authInfo = AuthenticationInformation(
|
||||||
|
username: credentials.username!,
|
||||||
|
serverUrl: serverUrl,
|
||||||
|
clientCertificate: clientCertificate,
|
||||||
|
token: token,
|
||||||
|
);
|
||||||
|
final userId = "${credentials.username}@$serverUrl";
|
||||||
|
|
||||||
|
// Mark logged in user as currently active user.
|
||||||
|
final globalSettings = GlobalAppSettings.boxedValue;
|
||||||
|
globalSettings.currentLoggedInUser = userId;
|
||||||
|
await globalSettings.save();
|
||||||
|
|
||||||
|
// Save credentials in encrypted box
|
||||||
|
final encryptedBox = await _openEncryptedBox();
|
||||||
|
await encryptedBox.put(
|
||||||
|
userId,
|
||||||
|
authInfo,
|
||||||
|
);
|
||||||
|
encryptedBox.close();
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
wasLoginStored: false,
|
wasLoginStored: false,
|
||||||
authentication: AuthenticationInformation(
|
authentication: authInfo,
|
||||||
serverUrl: serverUrl,
|
|
||||||
clientCertificate: clientCertificate,
|
|
||||||
token: token,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,24 +79,27 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
|
|||||||
///
|
///
|
||||||
/// Performs a conditional hydration based on the local authentication success.
|
/// Performs a conditional hydration based on the local authentication success.
|
||||||
///
|
///
|
||||||
Future<void> restoreSessionState(bool promptForLocalAuthentication) async {
|
Future<void> restoreSessionState() async {
|
||||||
final json = HydratedBloc.storage.read(storageToken);
|
final globalSettings = GlobalAppSettings.boxedValue;
|
||||||
|
if (globalSettings.currentLoggedInUser == null) {
|
||||||
if (json == null) {
|
|
||||||
// If there is nothing to restore, we can quit here.
|
// If there is nothing to restore, we can quit here.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promptForLocalAuthentication) {
|
final userSettings = Hive.box<UserAppSettings>(HiveBoxes.userSettings)
|
||||||
|
.get(globalSettings.currentLoggedInUser!);
|
||||||
|
|
||||||
|
if (userSettings!.isBiometricAuthenticationEnabled) {
|
||||||
final localAuthSuccess = await _localAuthService
|
final localAuthSuccess = await _localAuthService
|
||||||
.authenticateLocalUser("Authenticate to log back in");
|
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
|
||||||
if (localAuthSuccess) {
|
if (localAuthSuccess) {
|
||||||
hydrate();
|
final authentication = await _readAuthenticationFromEncryptedBox(
|
||||||
if (state.isAuthenticated) {
|
globalSettings.currentLoggedInUser!);
|
||||||
|
if (authentication != null) {
|
||||||
_dioWrapper.updateSettings(
|
_dioWrapper.updateSettings(
|
||||||
clientCertificate: state.authentication!.clientCertificate,
|
clientCertificate: authentication.clientCertificate,
|
||||||
authToken: state.authentication!.token,
|
authToken: authentication.token,
|
||||||
baseUrl: state.authentication!.serverUrl,
|
baseUrl: authentication.serverUrl,
|
||||||
);
|
);
|
||||||
return emit(
|
return emit(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
@@ -86,44 +110,62 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hydrate();
|
|
||||||
return emit(
|
return emit(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
wasLoginStored: true,
|
wasLoginStored: true,
|
||||||
wasLocalAuthenticationSuccessful: false,
|
wasLocalAuthenticationSuccessful: false,
|
||||||
authentication: state.authentication,
|
authentication: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hydrate();
|
final authentication = await _readAuthenticationFromEncryptedBox(
|
||||||
if (state.isAuthenticated) {
|
globalSettings.currentLoggedInUser!);
|
||||||
|
if (authentication != null) {
|
||||||
_dioWrapper.updateSettings(
|
_dioWrapper.updateSettings(
|
||||||
clientCertificate: state.authentication!.clientCertificate,
|
clientCertificate: authentication.clientCertificate,
|
||||||
authToken: state.authentication!.token,
|
authToken: authentication.token,
|
||||||
baseUrl: state.authentication!.serverUrl,
|
baseUrl: authentication.serverUrl,
|
||||||
);
|
);
|
||||||
final authState = AuthenticationState(
|
emit(
|
||||||
authentication: state.authentication!,
|
AuthenticationState(
|
||||||
wasLoginStored: true,
|
authentication: authentication,
|
||||||
|
wasLoginStored: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return emit(authState);
|
|
||||||
} else {
|
} else {
|
||||||
return emit(AuthenticationState.initial);
|
return emit(AuthenticationState.initial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<AuthenticationInformation?> _readAuthenticationFromEncryptedBox(
|
||||||
|
String userId) {
|
||||||
|
return _openEncryptedBox().then((box) => box.get(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Box<AuthenticationInformation?>> _openEncryptedBox() async {
|
||||||
|
const secureStorage = FlutterSecureStorage();
|
||||||
|
final encryptionKeyString = await secureStorage.read(key: 'key');
|
||||||
|
if (encryptionKeyString == null) {
|
||||||
|
final key = Hive.generateSecureKey();
|
||||||
|
|
||||||
|
await secureStorage.write(
|
||||||
|
key: 'key',
|
||||||
|
value: base64UrlEncode(key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final key = await secureStorage.read(key: 'key');
|
||||||
|
final encryptionKeyUint8List = base64Url.decode(key!);
|
||||||
|
return await Hive.openBox<AuthenticationInformation>(
|
||||||
|
HiveBoxes.vault,
|
||||||
|
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await clear();
|
await Hive.box<AuthenticationInformation>(HiveBoxes.authentication).clear();
|
||||||
_dioWrapper.resetSettings();
|
_dioWrapper.resetSettings();
|
||||||
emit(AuthenticationState.initial);
|
emit(AuthenticationState.initial);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
AuthenticationState? fromJson(Map<String, dynamic> json) =>
|
|
||||||
AuthenticationState.fromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, dynamic>? toJson(AuthenticationState state) => state.toJson();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class AuthenticationState {
|
|||||||
);
|
);
|
||||||
|
|
||||||
bool get isAuthenticated => authentication != null;
|
bool get isAuthenticated => authentication != null;
|
||||||
|
|
||||||
AuthenticationState({
|
AuthenticationState({
|
||||||
required this.wasLoginStored,
|
required this.wasLoginStored,
|
||||||
this.wasLocalAuthenticationSuccessful,
|
this.wasLocalAuthenticationSuccessful,
|
||||||
@@ -31,9 +32,4 @@ class AuthenticationState {
|
|||||||
this.wasLocalAuthenticationSuccessful,
|
this.wasLocalAuthenticationSuccessful,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthenticationState.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$AuthenticationStateFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$AuthenticationStateToJson(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,32 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||||
|
|
||||||
part 'authentication_information.g.dart';
|
part 'authentication_information.g.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@HiveType(typeId: HiveTypeIds.authentication)
|
||||||
class AuthenticationInformation {
|
class AuthenticationInformation {
|
||||||
final String? token;
|
@HiveField(0)
|
||||||
final String serverUrl;
|
String? token;
|
||||||
final ClientCertificate? clientCertificate;
|
|
||||||
|
@HiveField(1)
|
||||||
|
String serverUrl;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
ClientCertificate? clientCertificate;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
String username;
|
||||||
|
|
||||||
AuthenticationInformation({
|
AuthenticationInformation({
|
||||||
this.token,
|
required this.username,
|
||||||
required this.serverUrl,
|
required this.serverUrl,
|
||||||
|
this.token,
|
||||||
this.clientCertificate,
|
this.clientCertificate,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isValid {
|
bool get isValid {
|
||||||
return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false);
|
return serverUrl.isNotEmpty && (token?.isNotEmpty ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationInformation copyWith({
|
|
||||||
String? token,
|
|
||||||
String? serverUrl,
|
|
||||||
ClientCertificate? clientCertificate,
|
|
||||||
bool removeClientCertificate = false,
|
|
||||||
}) {
|
|
||||||
return AuthenticationInformation(
|
|
||||||
token: token ?? this.token,
|
|
||||||
serverUrl: serverUrl ?? this.serverUrl,
|
|
||||||
clientCertificate: clientCertificate ??
|
|
||||||
(removeClientCertificate ? null : this.clientCertificate),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AuthenticationInformation.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$AuthenticationInformationFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$AuthenticationInformationToJson(this);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/type/types.dart';
|
import 'package:paperless_mobile/core/type/types.dart';
|
||||||
|
|
||||||
|
part 'client_certificate.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.clientCertificate)
|
||||||
class ClientCertificate {
|
class ClientCertificate {
|
||||||
static const bytesKey = 'bytes';
|
static const bytesKey = 'bytes';
|
||||||
static const passphraseKey = 'passphrase';
|
static const passphraseKey = 'passphrase';
|
||||||
|
|
||||||
|
@HiveField(0)
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
|
@HiveField(1)
|
||||||
final String? passphrase;
|
final String? passphrase;
|
||||||
|
|
||||||
ClientCertificate({required this.bytes, this.passphrase});
|
ClientCertificate({required this.bytes, this.passphrase});
|
||||||
|
|||||||
36
lib/features/settings/global_app_settings.dart
Normal file
36
lib/features/settings/global_app_settings.dart
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
|
||||||
|
part 'global_app_settings.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.globalSettings)
|
||||||
|
class GlobalAppSettings with ChangeNotifier, HiveObjectMixin {
|
||||||
|
@HiveField(0)
|
||||||
|
String preferredLocaleSubtag;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
ThemeMode preferredThemeMode;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
ColorSchemeOption preferredColorSchemeOption;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
bool showOnboarding;
|
||||||
|
|
||||||
|
@HiveField(4)
|
||||||
|
String? currentLoggedInUser;
|
||||||
|
|
||||||
|
GlobalAppSettings({
|
||||||
|
required this.preferredLocaleSubtag,
|
||||||
|
this.preferredThemeMode = ThemeMode.system,
|
||||||
|
this.preferredColorSchemeOption = ColorSchemeOption.classic,
|
||||||
|
this.showOnboarding = true,
|
||||||
|
this.currentLoggedInUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
static GlobalAppSettings get boxedValue =>
|
||||||
|
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
|
||||||
|
.get(HiveBoxSingleValueKey.value)!;
|
||||||
|
}
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
|
||||||
|
part 'color_scheme_option.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.colorSchemeOption)
|
||||||
enum ColorSchemeOption {
|
enum ColorSchemeOption {
|
||||||
|
@HiveField(0)
|
||||||
classic,
|
classic,
|
||||||
|
@HiveField(1)
|
||||||
dynamic;
|
dynamic;
|
||||||
}
|
}
|
||||||
|
|||||||
16
lib/features/settings/user_app_settings.dart
Normal file
16
lib/features/settings/user_app_settings.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
|
||||||
|
part 'user_app_settings.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: HiveTypeIds.userSettings)
|
||||||
|
class UserAppSettings with HiveObjectMixin {
|
||||||
|
@HiveField(0)
|
||||||
|
bool isBiometricAuthenticationEnabled;
|
||||||
|
|
||||||
|
UserAppSettings({
|
||||||
|
this.isBiometricAuthenticationEnabled = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import 'package:paperless_mobile/core/repository/saved_view_repository.dart';
|
|||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ class AccountSettingsDialog extends StatelessWidget {
|
|||||||
Future<void> _onLogout(BuildContext context) async {
|
Future<void> _onLogout(BuildContext context) async {
|
||||||
try {
|
try {
|
||||||
await context.read<AuthenticationCubit>().logout();
|
await context.read<AuthenticationCubit>().logout();
|
||||||
await context.read<ApplicationSettingsCubit>().clear();
|
await context.read<GlobalAppSettings>();
|
||||||
await context.read<LabelRepository>().clear();
|
await context.read<LabelRepository>().clear();
|
||||||
await context.read<SavedViewRepository>().clear();
|
await context.read<SavedViewRepository>().clear();
|
||||||
await HydratedBloc.storage.clear();
|
await HydratedBloc.storage.clear();
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_state.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/pages/application_settings_page.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/pages/security_settings_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
@@ -68,10 +70,7 @@ class SettingsPage extends StatelessWidget {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BlocProvider.value(
|
builder: (context) => page,
|
||||||
value: context.read<ApplicationSettingsCubit>(),
|
|
||||||
child: page,
|
|
||||||
),
|
|
||||||
maintainState: true,
|
maintainState: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class BiometricAuthenticationSetting extends StatelessWidget {
|
class BiometricAuthenticationSetting extends StatelessWidget {
|
||||||
const BiometricAuthenticationSetting({super.key});
|
const BiometricAuthenticationSetting({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
return UserSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
|
if (settings == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return SwitchListTile(
|
return SwitchListTile(
|
||||||
value: settings.isLocalAuthenticationEnabled,
|
value: settings.isBiometricAuthenticationEnabled,
|
||||||
title: Text(S.of(context)!.biometricAuthentication),
|
title: Text(S.of(context)!.biometricAuthentication),
|
||||||
subtitle: Text(S.of(context)!.authenticateOnAppStart),
|
subtitle: Text(S.of(context)!.authenticateOnAppStart),
|
||||||
onChanged: (val) async {
|
onChanged: (val) async {
|
||||||
@@ -19,12 +29,14 @@ class BiometricAuthenticationSetting extends StatelessWidget {
|
|||||||
S.of(context)!.authenticateToToggleBiometricAuthentication(
|
S.of(context)!.authenticateToToggleBiometricAuthentication(
|
||||||
val ? 'enable' : 'disable',
|
val ? 'enable' : 'disable',
|
||||||
);
|
);
|
||||||
await context
|
|
||||||
.read<ApplicationSettingsCubit>()
|
final isAuthenticated = await context
|
||||||
.setIsBiometricAuthenticationEnabled(
|
.read<LocalAuthenticationService>()
|
||||||
val,
|
.authenticateLocalUser(localizedReason);
|
||||||
localizedReason: localizedReason,
|
if (isAuthenticated) {
|
||||||
);
|
settings.isBiometricAuthenticationEnabled = val;
|
||||||
|
settings.save();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
|
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -15,7 +19,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
return GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(S.of(context)!.colors),
|
title: Text(S.of(context)!.colors),
|
||||||
@@ -25,7 +29,7 @@ class ColorSchemeOptionSetting extends StatelessWidget {
|
|||||||
settings.preferredColorSchemeOption,
|
settings.preferredColorSchemeOption,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => showDialog(
|
onTap: () => showDialog<ColorSchemeOption>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => RadioSettingsDialog<ColorSchemeOption>(
|
builder: (_) => RadioSettingsDialog<ColorSchemeOption>(
|
||||||
titleText: S.of(context)!.colors,
|
titleText: S.of(context)!.colors,
|
||||||
@@ -50,17 +54,13 @@ class ColorSchemeOptionSetting extends StatelessWidget {
|
|||||||
hintIcon: Icons.warning_amber,
|
hintIcon: Icons.warning_amber,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
initialValue: context
|
initialValue: settings.preferredColorSchemeOption,
|
||||||
.read<ApplicationSettingsCubit>()
|
|
||||||
.state
|
|
||||||
.preferredColorSchemeOption,
|
|
||||||
),
|
),
|
||||||
).then(
|
).then(
|
||||||
(value) {
|
(value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context
|
settings.preferredColorSchemeOption = value;
|
||||||
.read<ApplicationSettingsCubit>()
|
settings.save();
|
||||||
.setColorSchemeOption(value);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/src/widgets/framework.dart';
|
||||||
|
import 'package:flutter/src/widgets/placeholder.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
|
|
||||||
|
class GlobalSettingsBuilder extends StatelessWidget {
|
||||||
|
|
||||||
|
final Widget Function(BuildContext context, GlobalAppSettings settings)
|
||||||
|
builder;
|
||||||
|
const GlobalSettingsBuilder({super.key, required this.builder});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable:
|
||||||
|
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings).listenable(),
|
||||||
|
builder: (context, value, _) {
|
||||||
|
final settings = value.get(HiveBoxSingleValueKey.value)!;
|
||||||
|
return builder(context, settings);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
|
|
||||||
class LanguageSelectionSetting extends StatefulWidget {
|
class LanguageSelectionSetting extends StatefulWidget {
|
||||||
const LanguageSelectionSetting({super.key});
|
const LanguageSelectionSetting({super.key});
|
||||||
@@ -24,7 +27,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
return GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(S.of(context)!.language),
|
title: Text(S.of(context)!.language),
|
||||||
@@ -62,14 +65,12 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
|
|||||||
label: _languageOptions['pl']! + "*",
|
label: _languageOptions['pl']! + "*",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
initialValue: context
|
initialValue: settings.preferredLocaleSubtag,
|
||||||
.read<ApplicationSettingsCubit>()
|
|
||||||
.state
|
|
||||||
.preferredLocaleSubtag,
|
|
||||||
),
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.read<ApplicationSettingsCubit>().setLocale(value);
|
settings.preferredLocaleSubtag = value;
|
||||||
|
settings.save();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ class ThemeModeSetting extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
return GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(S.of(context)!.appearance),
|
title: Text(S.of(context)!.appearance),
|
||||||
@@ -19,10 +20,7 @@ class ThemeModeSetting extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (_) => RadioSettingsDialog<ThemeMode>(
|
builder: (_) => RadioSettingsDialog<ThemeMode>(
|
||||||
titleText: S.of(context)!.appearance,
|
titleText: S.of(context)!.appearance,
|
||||||
initialValue: context
|
initialValue: settings.preferredThemeMode,
|
||||||
.read<ApplicationSettingsCubit>()
|
|
||||||
.state
|
|
||||||
.preferredThemeMode,
|
|
||||||
options: [
|
options: [
|
||||||
RadioOption(
|
RadioOption(
|
||||||
value: ThemeMode.system,
|
value: ThemeMode.system,
|
||||||
@@ -40,7 +38,8 @@ class ThemeModeSetting extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.read<ApplicationSettingsCubit>().setThemeMode(value);
|
settings.preferredThemeMode = value;
|
||||||
|
settings.save();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
||||||
|
|
||||||
|
class UserSettingsBuilder extends StatelessWidget {
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
UserAppSettings? settings,
|
||||||
|
) builder;
|
||||||
|
|
||||||
|
const UserSettingsBuilder({
|
||||||
|
super.key,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<Box<UserAppSettings>>(
|
||||||
|
valueListenable:
|
||||||
|
Hive.box<UserAppSettings>(HiveBoxes.userSettings).listenable(),
|
||||||
|
builder: (context, value, _) {
|
||||||
|
final currentUser =
|
||||||
|
Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
|
||||||
|
.get(HiveBoxSingleValueKey.value)
|
||||||
|
?.currentLoggedInUser;
|
||||||
|
if (currentUser != null) {
|
||||||
|
final settings = value.get(currentUser);
|
||||||
|
return builder(context, settings);
|
||||||
|
} else {
|
||||||
|
return builder(context, null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
|
|||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
|
import 'package:hive_flutter/adapters.dart';
|
||||||
|
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.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/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
@@ -33,7 +35,10 @@ import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart'
|
|||||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||||
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/global_app_settings.dart';
|
||||||
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/user_app_settings.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
@@ -45,10 +50,35 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
|
||||||
|
String get defaultPreferredLocaleSubtag {
|
||||||
|
String preferredLocale = Platform.localeName.split("_").first;
|
||||||
|
if (!S.supportedLocales
|
||||||
|
.any((locale) => locale.languageCode == preferredLocale)) {
|
||||||
|
preferredLocale = 'en';
|
||||||
|
}
|
||||||
|
return preferredLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initHive() async {
|
||||||
|
await Hive.initFlutter();
|
||||||
|
registerHiveAdapters();
|
||||||
|
final globalSettingsBox =
|
||||||
|
await Hive.openBox<GlobalAppSettings>(HiveBoxes.globalSettings);
|
||||||
|
if (!globalSettingsBox.containsKey(HiveBoxSingleValueKey.value)) {
|
||||||
|
await globalSettingsBox.put(
|
||||||
|
HiveBoxSingleValueKey.value,
|
||||||
|
GlobalAppSettings(preferredLocaleSubtag: defaultPreferredLocaleSubtag),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
Bloc.observer = BlocChangesObserver();
|
await _initHive();
|
||||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
final globalSettings = Hive.box<GlobalAppSettings>(HiveBoxes.globalSettings)
|
||||||
|
.get(HiveBoxSingleValueKey.value)!;
|
||||||
|
|
||||||
await findSystemLocale();
|
await findSystemLocale();
|
||||||
packageInfo = await PackageInfo.fromPlatform();
|
packageInfo = await PackageInfo.fromPlatform();
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -70,11 +100,10 @@ void main() async {
|
|||||||
storageDirectory: hiveDir,
|
storageDirectory: hiveDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
final appSettingsCubit = ApplicationSettingsCubit(localAuthService);
|
|
||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
|
|
||||||
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
final languageHeaderInterceptor = LanguageHeaderInterceptor(
|
||||||
appSettingsCubit.state.preferredLocaleSubtag,
|
globalSettings.preferredLocaleSubtag,
|
||||||
);
|
);
|
||||||
// Manages security context, required for self signed client certificates
|
// Manages security context, required for self signed client certificates
|
||||||
final sessionManager = SessionManager([
|
final sessionManager = SessionManager([
|
||||||
@@ -108,9 +137,11 @@ void main() async {
|
|||||||
authApi,
|
authApi,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
);
|
);
|
||||||
await authCubit.restoreSessionState(
|
|
||||||
appSettingsCubit.state.isLocalAuthenticationEnabled,
|
if (globalSettings.currentLoggedInUser != null) {
|
||||||
);
|
await authCubit
|
||||||
|
.restoreSessionState();
|
||||||
|
}
|
||||||
|
|
||||||
if (authCubit.state.isAuthenticated) {
|
if (authCubit.state.isAuthenticated) {
|
||||||
final auth = authCubit.state.authentication!;
|
final auth = authCubit.state.authentication!;
|
||||||
@@ -125,8 +156,10 @@ void main() async {
|
|||||||
await localNotificationService.initialize();
|
await localNotificationService.initialize();
|
||||||
|
|
||||||
//Update language header in interceptor on language change.
|
//Update language header in interceptor on language change.
|
||||||
appSettingsCubit.stream.listen((event) => languageHeaderInterceptor
|
globalSettings.addListener(
|
||||||
.preferredLocaleSubtag = event.preferredLocaleSubtag);
|
() => languageHeaderInterceptor.preferredLocaleSubtag =
|
||||||
|
globalSettings.preferredLocaleSubtag,
|
||||||
|
);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
@@ -165,8 +198,6 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
BlocProvider<AuthenticationCubit>.value(value: authCubit),
|
BlocProvider<AuthenticationCubit>.value(value: authCubit),
|
||||||
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
|
BlocProvider<ConnectivityCubit>.value(value: connectivityCubit),
|
||||||
BlocProvider<ApplicationSettingsCubit>.value(
|
|
||||||
value: appSettingsCubit),
|
|
||||||
],
|
],
|
||||||
child: const PaperlessMobileEntrypoint(),
|
child: const PaperlessMobileEntrypoint(),
|
||||||
),
|
),
|
||||||
@@ -196,7 +227,7 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
child: GlobalSettingsBuilder(
|
||||||
builder: (context, settings) {
|
builder: (context, settings) {
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightDynamic, darkDynamic) {
|
builder: (lightDynamic, darkDynamic) {
|
||||||
|
|||||||
64
pubspec.lock
64
pubspec.lock
@@ -681,6 +681,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.0.0"
|
||||||
|
flutter_secure_storage_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_linux
|
||||||
|
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
|
flutter_secure_storage_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_macos
|
||||||
|
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
|
flutter_secure_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_platform_interface
|
||||||
|
sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
|
flutter_secure_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_web
|
||||||
|
sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
flutter_secure_storage_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_windows
|
||||||
|
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
flutter_staggered_grid_view:
|
flutter_staggered_grid_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -784,6 +832,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.2.3"
|
||||||
|
hive_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hive_flutter
|
||||||
|
sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
hive_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: hive_generator
|
||||||
|
sha256: "65998cc4d2cd9680a3d9709d893d2f6bb15e6c1f92626c3f1fa650b4b3281521"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ dependencies:
|
|||||||
in_app_review: ^2.0.6
|
in_app_review: ^2.0.6
|
||||||
freezed_annotation: ^2.2.0
|
freezed_annotation: ^2.2.0
|
||||||
animations: ^2.0.7
|
animations: ^2.0.7
|
||||||
|
hive_flutter: ^1.1.0
|
||||||
|
flutter_secure_storage: ^8.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
@@ -108,6 +110,7 @@ dev_dependencies:
|
|||||||
dart_code_metrics: ^5.4.0
|
dart_code_metrics: ^5.4.0
|
||||||
auto_route_generator: ^5.0.3
|
auto_route_generator: ^5.0.3
|
||||||
freezed: ^2.3.2
|
freezed: ^2.3.2
|
||||||
|
hive_generator: ^2.0.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
Reference in New Issue
Block a user